<reasoning_content>
*   Task: Refactor a job queue into three composable classes in `solution.py`.
    *   Class 1: `Job` (dataclass)
        *   Fields: `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
        *   Requirement: Use `@dataclass`.
    *   Class 2: `RetryPolicy` (Logic for retries)
        *   `__init__(self, max_attempts: int = 4)`
        *   `run(self, fn: Callable, data) -> tuple[bool, int]`
        *   Behavior: Call `fn(data)`, catch exceptions, retry until success or `max_attempts`.
        *   Note: Do *not* sleep.
    *   Class 3: `JobQueue` (Orchestrator)
        *   `add_job(self, job_id: str, data: dict, priority: int = 0) -> None`
        *   `process_job(self, job_id: str, processor: Callable) -> bool`
        *   Requirement: `process_job` must use `RetryPolicy`.
        *   `get_next_job(self) -> tuple[str, dict] | None`
        *   Ordering: Highest priority first. FIFO for same priority.
    *   Constraints: Standard library only. No external packages. No explanations.

    *   `Job`:
        ```python
        from dataclasses import dataclass
        from typing import Callable

        @dataclass
        class Job:
            id: str
            data: dict
            priority: int = 0
            retries: int = 0
        ```

    *   `RetryPolicy`:
        ```python
        class RetryPolicy:
            def __init__(self, max_attempts: int = 4):
                self.max_attempts = max_attempts

            def run(self, fn: Callable, data: dict) -> tuple[bool, int]:
                attempts = 0
                while attempts < self.max_attempts:
                    try:
                        fn(data)
                        return True, attempts + 1
                    except Exception:
                        attempts += 1
                return False, attempts
        ```

    *   `JobQueue`:
        *   Needs a way to store jobs. A list or a heap?
        *   Requirement: "highest-priority job's (id, data); FIFO order within the same priority".
        *   Since it's FIFO within priority, a heap usually needs a secondary counter or a list of lists. Or a list sorted by `(-priority, insertion_order)`.
        *   Actually, a simple list and sorting/popping or a priority queue with a counter is standard.
        *   Let's look at `get_next_job`: "return the highest-priority job's (id, data)".
        *   Let's use a list of jobs and maintain order or sort it.
        *   `add_job`: Store the job.
        *   `process_job`: Find the job by ID, use `RetryPolicy` to run the processor.
        *   Note on `process_job`: It takes `job_id`. Does it remove it? Usually "process" implies completion, but the signature says `-> bool`.
        *   Actually, looking at the instructions: `process_job` uses `RetryPolicy`. `get_next_job` returns the next one.

    *   *Refining `JobQueue` Storage:*
        To ensure FIFO within same priority, we can store `(priority, sequence_number, Job)`.
        Wait, the prompt says "highest-priority". Usually, higher numbers are higher priority? Or lowest? In most systems, high number = high priority. I'll assume higher number = higher priority.
        Actually, many systems (like Unix `nice`) use lower number = higher priority. But "highest-priority" usually implies the max value of a "priority" field unless specified.
        Let's use a list and `sort`.

    *   *Refining `process_job`:*
        If I call `process_job(job_id, processor)`, I need to find the job in the queue. If it succeeds, should I remove it? The prompt doesn't explicitly say to remove it, but usually, a queue process removes it. However, `get_next_job` is the one that retrieves.
        Wait, `get_next_job` returns `(id, data)`. If I call `get_next_job`, it should probably remove it from the internal storage.
        If `process_job` is called with an ID, it probably finds the job in the storage, runs the retry logic, and returns success.

    *   *Wait, let's re-read:*
        `add_job` adds it.
        `process_job` takes `job_id`.
        `get_next_job` returns highest priority.
        If I call `get_next_job`, I should probably pop it.

    *   *Storage Logic for FIFO + Priority:*
        ```python
        self.jobs = [] # list of Job objects
        # ...
        def add_job(self, job_id, data, priority):
            self.jobs.append(Job(job_id, data, priority))

        def get_next_job(self):
            if not self.jobs: return None
            # Sort by priority descending, then by original index (FIFO)
            # But since we append, original index is position in list.
            # Actually, a simple sort is enough if we track insertion order.
            # Or just sort by (-priority, insertion_index).
            self.jobs.sort(key=lambda x: (-x.priority, self.insertion_counter))
            # No, that's messy.
        ```
        Alternative: Just sort every time?
        `self.jobs.sort(key=lambda x: (-x.priority, some_global_counter))`

        Actually, let's look at the requirements again.
        `get_next_job` returns `(id, data)`.
        Does `process_job` modify the queue?
        If `process_job` is called, the job is likely being processed.
        If `get_next_job` is called, the job is "popped" or "peeked"?
        "return the highest-priority job's (id, data)" usually implies popping in a queue context.

    *   *Structure Correction:*
        `JobQueue` should hold a list of `Job` objects.
        `add_job`: `self.jobs.append(Job(...))`
        `get_next_job`:
            Sort list by `(-priority, insertion_index)`.
            Pop the first one.
            Return `(id, data)`.
        `process_job`:
            Find job by `id`.
            Use `RetryPolicy`.
            Return success.

    *   *Refining `process_job` requirement:*
        "must USE RetryPolicy (compose, don't reimplement retry logic)".
        So `JobQueue` needs an instance of `RetryPolicy`.

    *   *Refining `RetryPolicy.run`*:
        `fn(data)` is called. Catch `Exception`. Return `(bool, int)`.

    *   *Refining `Job` dataclass:*
        `id`, `data`, `priority`, `retries`.

    *   *Sorting logic for `JobQueue`:*
        To maintain FIFO for same priority:
        When adding a job, we can assign a `sequence` number.
        `self.jobs.append({'job': Job(...), 'seq': self.seq_counter})`
        `self.seq_counter += 1`
        `get_next_job`:
        `self.jobs.sort(key=lambda x: (-x['job'].priority, x['seq']))`
        `item = self.jobs.pop(0)`

    *   *Wait, `process_job` signature:*
        `process_job(self, job_id: str, processor: Callable) -> bool`
        If it takes an ID, it doesn't necessarily mean it's the "next" one. It just means find the one with that ID.

    *   *Implementation details:*
        `RetryPolicy`:
        ```python
        class RetryPolicy:
            def __init__(self, max_attempts: int = 4):
                self.max_attempts = max_attempts
            def run(self, fn: Callable, data: dict) -> tuple[bool, int]:
                attempts = 0
                while attempts < self.max_attempts:
                    try:
                        fn(data)
                        return True, attempts + 1
                    except Exception:
                        attempts += 1
                return False, attempts
        ```

        `JobQueue`:
        ```python
        class JobQueue:
            def __init__(self):
                self.jobs = []
                self.retry_policy = RetryPolicy()
                self.counter = 0
            def add_job(self, job_id, data, priority=0):
                self.jobs.append({'job': Job(job_id, data, priority), 'seq': self.counter})
                self.counter += 1
            def get_next_job(self):
                if not self.jobs: return None
                self.jobs.sort(key=lambda x: (-x['job'].priority, x['seq']))
                entry = self.jobs.pop(0)
                return (entry['job'].id, entry['job'].data)
            def process_job(self, job_id, processor):
                for i, entry in enumerate(self.jobs):
                    if entry['job'].id == job_id:
                        success, _ = self.retry_policy.run(processor, entry['job'].data)
                        return success
                return False
        ```

    *   *Reviewing constraints:*
        - Dataclass? Yes.
        - Fields correct? Yes.
        - RetryPolicy logic? Yes (no sleep).
        - JobQueue logic? Yes (compose RetryPolicy).
        - FIFO within priority? Yes (counter).
        - Std lib only? Yes.
        - No explanations? Yes.

    *   *Final check on `process_job`*:
        Should it remove the job? The prompt doesn't say. Usually, `process_job` is a command. `get_next_job` is the fetcher.
        If `process_job` is called, and it's successful, should it be removed?
        Usually, `process_job` is called *after* `get_next_job` or as a standalone action.
        Given `get_next_job` returns the ID and data, the caller probably handles the processing. But `process_job` also exists.
        If `process_job` exists, it might be used to process a job still in the queue.
        I will leave the job in the queue during `process_job` unless it's specified otherwise, but usually, a "process" command might imply removal on success. However, without explicit instructions to remove on success in `process_job`, I will stick to the signature provided.

    Wait, one detail: `Job` has a `retries` field.
    Does `RetryPolicy` update it?
    The prompt says `RetryPolicy.run` returns `(success, attempts_made)`. It doesn't say it updates the `Job` object.
    The `Job` object has a `retries` field, but `RetryPolicy` is a separate class.
    If I'm just composing, `JobQueue` might need to update the `retries` field of the `Job` instance?
    The instructions for `RetryPolicy.run` don't mention updating the object, just returning the count.
    I'll keep it simple. If the prompt asks for `retries` in the dataclass, I'll include it. If the prompt doesn't say to update it, I won't guess.

    *Actually*, looking at `JobQueue.process_job`:
    It takes `job_id`. It doesn't take the `Job` object.
    So I find the job, call `run`, and return the bool.

    *Wait, `get_next_job` implementation detail:*
    If I sort every time `get_next_job` is called, it's O(N log N). For a "queue", it's better to use a heap, but a heap doesn't easily support "FIFO for same priority" without a tie-breaker. A `heapq` with `(-priority, sequence_number)` is standard.
    However, `process_job` needs to find a job by `id`. A heap is bad for searching by ID.
    A list is better for searching by ID.
    Since the prompt doesn't specify scale, a list is fine.

    *Final Polish:*
    - `from dataclasses import dataclass`
    - `from typing import Callable`
    - `Job` dataclass.
    - `RetryPolicy`.
    - `JobQueue`.

    Wait, `JobQueue` needs to be able to find the job for `process_job`.
    If I use `self.jobs.sort(...)` in `get_next_job`, the list order changes.
    Is `process_job` supposed to remove the job?
    If I call `get_next_job`, I get the data. If I then call `process_job`, it should probably still be there or I should have already popped it?
    Let's assume `get_next_job` is "pop the best".
    Let's assume `process_job` is "find by ID and run".
    If `get_next_job` pops it, `process_job` won't find it.
    This suggests two different ways to handle jobs.
    1. Get the "next" one, then the user processes it.
    2. Specify an ID and tell the queue to process it.
    If this is the case, `get_next_job` MUST pop it.

    Wait, if `get_next_job` pops it, then `process_job` will only work for jobs that haven't been "gotten" yet. This makes sense.

    One small thing: `Job` dataclass `retries` field. `RetryPolicy` returns `attempts_made`. I should probably update the `retries` field on the `Job` object if `process_job` is called, just to be safe, although not explicitly requested. Actually, I'll stick strictly to the requested behavior.

    *Correcting `RetryPolicy` signature:*
    `run(self, fn: Callable, data) -> tuple[bool, int]`
    The prompt says `data` (no type hint in prompt for `data`, but `Job.data` is `dict`).

    *Refining `JobQueue.get_next_job`*:
    `return the highest-priority job's (id, data); FIFO order within the same priority.`
    This means if priority 10 is highest, and I have two priority 10 jobs, the one added first comes first.

    ```python
    from dataclasses import dataclass
    from typing import Callable

    @dataclass
    class Job:
        id: str
        data: dict
        priority: int = 0
        retries: int = 0

    class RetryPolicy:
        def __init__(self, max_attempts: int = 4):
            self.max_attempts = max_attempts

        def run(self, fn: Callable, data: dict) -> tuple[bool, int]:
            attempts = 0
            while attempts < self.max_attempts:
                try:
                    fn(data)
                    return True, attempts + 1
                except Exception:
                    attempts += 1
            return False, attempts

    class JobQueue:
        def __init__(self):
            self.jobs = []
            self.retry_policy = RetryPolicy()
            self.counter = 0

        def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
            self.jobs.append({
                'job': Job(job_id, data, priority),
                'seq': self.counter
            })
            self.counter += 1

        def process_job(self, job_id: str, processor: Callable) -> bool:
            for item in self.jobs:
                if item['job'].id == job_id:
                    success, _ = self.retry_policy.run(processor, item['job'].data)
                    return success
            return False

        def get_next_job(self) -> tuple[str, dict] | None:
            if not self.jobs:
                return None
            # Sort by priority descending, then sequence ascending
            self.jobs.sort(key=lambda x: (-x['job'].priority, x['seq']))
            entry = self.jobs.pop(0)
            return (entry['job'].id, entry['job'].data))
    ```
    Wait, `tuple[str, dict] | None` syntax. Python 3.10+.
    Standard library only.

    One tiny fix: `Job` fields order.
    `id`, `data`, `priority`, `retries`.

    One more thing: `RetryPolicy.run(self, fn: Callable, data)`.
    The prompt doesn't specify the type of `data` in the signature of `run`, but I'll assume it's the same as `Job.data`.

    Check: "FIFO order within the same priority".
    My `seq` logic ensures this.
    Check: "highest-priority".
    My `-x['job'].priority` ensures that the largest number is processed first. If "highest" meant smallest number (like Unix nice), I'd remove the `-`. But "highest priority" almost always means the highest value in a priority field.

    Final check on `process_job`. Should it remove the job?
    If `process_job` is called, and it succeeds, typically it should be removed.
    But `get_next_job` also exists.
    If `get_next_job` is called, the job is removed from the internal list.
    If `process_job` is called, it searches the internal list.
    If `get_next_job` is used, the caller gets the data and they are responsible for processing.
    If `process_job` is used, the queue handles the execution and the caller just gets a success bool.
    This implies that a job is either "handled by `get_next_job`" OR "handled by `process_job`".
    In both cases, the job should probably be removed from the `self.jobs` list if successful.
    However, the prompt doesn't say "remove on success". It says `process_job` returns `bool`.
    I will stick to the minimum requirements to avoid over-engineering. I'll leave it in the list for `process_job` because it's not specified.

    Actually, I'll make `process_job` remove it on success, or just let it be.
    Wait, `process_job` signature: `process_job(self, job_id: str, processor: Callable) -> bool`.
    If I don't remove it, and someone calls `process_job` twice, it will run twice.
    If I do remove it, it's safer.
    But let's look at `get_next_job` again.
    `get_next_job` returns `(id, data)`. This is clearly a "pop" operation.
    If `get_next_job` is used, the job is gone from the queue.
    If `process_job` is used, the job is still in the queue? That's weird.
    Usually, `process_job` is a way to process a specific job *that was already retrieved*? No, it takes `job_id`.
    I'll just implement it as described. If it's not told to remove, I won't remove.

    Wait, `RetryPolicy.run` returns `(success, attempts_made)`.
    I'll make sure the logic is exactly as described.

    One last thing: `from typing import Callable` is part of standard library (since 3.5).

    ```python
    from dataclasses import dataclass
    from typing import Callable

    @dataclass
    class Job:
        id: str
        data: dict
        priority: int = 0
        retries: int = 0

    class RetryPolicy:
        def __init__(self, max_attempts: int = 4):
            self.max_attempts = max_attempts

        def run(self, fn: Callable, data: dict) -> tuple[bool, int]:
            attempts = 0
            while attempts < self.max_attempts:
                try:
                    fn(data)
                    return True, attempts + 1
                except Exception:
                    attempts += 1
            return False, attempts

    class JobQueue:
        def __init__(self):
            self.jobs = []
            self.retry_policy = RetryPolicy()
            self.counter = 0

        def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
            self.jobs.append({
                'job': Job(job_id, data, priority),
                'seq': self.counter
            })
            self.counter += 1

        def process_job(self, job_id: str, processor: Callable) -> bool:
            for item in self.jobs:
                if item['job'].id == job_id:
                    success, _ = self.retry_policy.run(processor, item['job'].data)
                    return success
            return False

        def get_next_job(self) -> tuple[str, dict] | None:
            if not self.jobs:
                return None
            # Sort: highest priority first (-priority), then earliest seq first
            self.jobs.sort(key=lambda x: (-x['job'].priority, x['seq']))
            entry = self.jobs.pop(0)
            return (entry['job'].id, entry['job'].data))
    ```
    One small thing: `tuple[str, dict] | None` is only valid in 3.10+. I'll use it as requested.

    Wait, `RetryPolicy` signature: `run(self, fn: Callable, data)`. No type hint for `data`. I'll omit it to be safe or just use `dict`. The prompt says `data`.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable

@dataclass
class Job:
    id: str
    data: dict
    priority: int = 0
    retries: int = 0

class RetryPolicy:
    def __init__(self, max_attempts: int = 4):
        self.max_attempts = max_attempts

    def run(self, fn: Callable, data: dict) -> tuple[bool, int]:
        attempts = 0
        while attempts < self.max_attempts:
            try:
                fn(data)
                return True, attempts + 1
            except Exception:
                attempts += 1
        return False, attempts

class JobQueue:
    def __init__(self):
        self.jobs = []
        self.retry_policy = RetryPolicy()
        self.counter = 0

    def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
        self.jobs.append({
            'job': Job(job_id, data, priority),
            'seq': self.counter
        })
        self.counter += 1

    def process_job(self, job_id: str, processor: Callable) -> bool:
        for item in self.jobs:
            if item['job'].id == job_id:
                success, _ = self.retry_policy.run(processor, item['job'].data)
                return success
        return False

    def get_next_job(self) -> tuple[str, dict] | None:
        if not self.jobs:
            return None
        
        # Sort by highest priority first (desc), then by sequence number (asc) for FIFO
        self.jobs.sort(key=lambda x: (-x['job'].priority, x['seq']))
        entry = self.jobs.pop(0)
        return (entry['job'].id, entry['job'].data))
```
</content>