<reasoning_content>
*   Goal: Refactor a job queue into three composable classes in `solution.py`.
    *   Class 1: `Job` (dataclass) - `id`, `data`, `priority`, `retries`.
    *   Class 2: `RetryPolicy` - Handles retry logic with backoff (though backoff timing isn't specified as sleep, but the logic must exist). Method `run(fn, data)` returns `(success, attempts_made)`. No actual sleeping.
    *   Class 3: `JobQueue` - Manages jobs. `add_job`, `process_job` (using `RetryPolicy`), `get_next_job` (priority order, then FIFO).
    *   Constraints: Standard library only. No external packages. No explanations in the output.

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

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

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

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

    *   `JobQueue` class:
        ```python
        import heapq
        from typing import List, Tuple, Optional

        class JobQueue:
            def __init__(self):
                self.jobs: List[Job] = []
                self.retry_policy = RetryPolicy()

            def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
                # Need to handle priority and FIFO.
                # For heapq: smaller priority value usually means higher priority?
                # The prompt says "highest-priority". Usually, in coding tasks,
                # higher number = higher priority, but heapq is min-heap.
                # I'll use a tie-breaker for FIFO (a counter).
                pass

            def process_job(self, job_id: str, Processor: Callable[[dict]]) -> bool:
                # Find job by id? Or just process?
                # The prompt says "process_job(self, job_id: str, Processor: Callable) -> bool"
                # It doesn't say "pop", it says "process".
                # Wait, `get_next_job` returns the next job.
                # `process_job` probably takes a specific job_id and applies the policy.
                pass

            def get_next_job(self) -> tuple[str, dict] | None:
                # Return highest priority, then FIFO.
                pass
        ```

    *   *Refinement on `JobQueue.jobs`*: Since I need FIFO for the same priority, I should store entries as `(-priority, counter, job)`. But the prompt says "highest-priority". Usually, priority 10 > priority 1.
    *   *Refinement on `JobQueue.process_job`*: The signature says `process_job(self, job_id: str, Processor: Callable)`. It implies I need to find the job by ID, apply the retry policy, and then... what? Does it remove it from the queue? Usually, processing finishes a job. However, the prompt doesn't explicitly say "remove". But `get_next_job` exists. If `process_job` is called with a specific ID, it probably succeeds if the policy succeeds.
    *   *Refinement on `JobQueue.get_next_job`*: It returns `(id, data)`. If I call `get_next_job` and it returns something, should it be removed? The prompt doesn't say. But usually, "get next" implies a pop. Let's look at the flow.
    *   *Re-reading `process_job`*: "must USE RetryPolicy (compose, don't reimplement retry logic)". This means `RetryPolicy.run` should be used inside `process_job`.

    *   *Data Structure for Priority Queue*:
        Since I need to find by ID for `process_job`, a simple list might be inefficient, but a `heapq` is better for `get_next_job`. However, `heapq` doesn't support "find by ID" easily.
        I'll use a list for simplicity or a dict + list.
        Wait, the prompt says "highest-priority". I'll assume high number = high priority.
        To handle FIFO within priority: I'll use a counter.

    *   *Implementation detail for `JobQueue`*:
        `add_job(job_id, data, priority)`: I'll store a list of `Job` objects.
        `get_next_job()`: Sort by `-priority` and then by insertion order.
        `process_job(job_id, Processor)`: Find the job with `job_id`. Use `RetryPolicy`.

    *   Wait, the `Job` dataclass has a `retries` field. Does `RetryPolicy` update this? The prompt says `RetryPolicy` returns `(success, attempts_made)`. Maybe `process_job` should update the `retries` field?
        Actually, the `RetryPolicy` is a separate class. `process_job` takes a `job_id`. It finds the job. Then it calls `retry_policy.run(Processor, job.data)`.

    *   Let's refine the `Job` and `JobQueue` logic.
        `Job` dataclass: `id`, `data`, `priority`, `retries`.
        `JobQueue` list: `[Job, Job, ...]`.
        `add_job`: append to list.
        `get_next_job`:
           Sort list: `sorted_jobs = sorted(self.jobs, key=lambda x: (-x.priority, self.entry_count[x.id]))`
           Wait, I need a way to track insertion order.
           Or just use `list.index` if I don't want to store a counter? No, index changes.
           I'll just use a list and `min()` or `sorted()` and return the first one.

    *   *Wait*, `get_next_job` should probably return the one that will be processed next. If I don't remove it, `get_next_job` will always return the same thing. I'll assume it removes it.
        Actually, the instructions don't say "remove". But `get_next_job` is usually a "pop" in queue terms. Let's assume it removes it.

    *   *Wait*, let's look at the signature of `process_job(self, job_id: str, Processor: Callable)`. If `get_next_job` removes it, `process_job` would have a hard time finding it by ID unless it's still in the queue.
        If `process_job` is called by ID, it implies the job is still in the queue.
        Maybe `get_next_job` doesn't remove? But then it's not a queue, it's a priority list.
        Let's assume:
        1. `add_job`: puts it in a list.
        2. `get_next_job`: finds the highest priority (lowest index for same priority) and returns it, **but does not remove it**? No, "Queue" usually implies removal.
        3. `process_job`: takes a `job_id`. It finds the job in the list. It runs it via `RetryPolicy`. If success, it removes it? Or maybe it just returns the bool.

    *   Let's look at the `process_job` requirement again: "must USE RetryPolicy".
        If I use `RetryPolicy.run`, it returns `(success, attempts_made)`.
        `process_job` returns `bool`. So it returns `success`.

    *   *Refining `JobQueue` storage*:
        To support `get_next_job` (highest priority, then FIFO ) and `process_job` ( by ID), I'll use a list for storage.
        `get_next_job`:
        ```python
        def get_next_job(self) -> tuple[str, dict] | None:
            if not self.jobs: return None
            # Highest priority (max priority), then FIFO (lowest index)
            # We want to find max priority.
            best_job = max(self.jobs, key=lambda j: j.priority)
            # Wait, max() is not stable for FIFO if we just use priority.
            # To ensure FIFO, we need the first occurrence of the max priority.
            max_priority = max(j.priority for j in self.jobs)
            for j in self.jobs:
                if j.priority == max_priority:
                    return (j.id, j.data)
        ```
        Actually, if I sort by `(-j.priority, insertion_order)`, it works.
        Since I can't easily get `insertion_order` without a counter or the list index, and I might remove items, I'll use a counter.

    *   *Revised Plan for `JobQueue`*:
        - `self.jobs`: list of `Job`.
        - `self.insertion_order`: dict `{job_id: int}`.
        - `self.counter`: int.
        - `add_job`:
            - create `Job`
            - `self.jobs.append(job)`
            - `self.insertion_order[job.id] = self.counter`
            - `self.counter += 1`
        - `get_next_job`:
            - Find job with max `priority`.
            - If ties, find one with min `insertion_order`.
            - Return `(id, data)`.
        - `process_job`:
            - Find job by `job_id` in `self.jobs`.
            - `success, attempts = self.retry_policy.run(Processor, job.data)`
            - `job.retries = attempts` (Optional, but since `retries` is in the dataclass, might as well).
            - Return `success`.

    *   Wait, the `RetryPolicy` `run` method: "retries on any exception until success or `max_attempts` reached".
        The `attempts_made` should be returned.

    *   *Refining `RetryPolicy.run`*:
        ```python
        def run(self, fn: Callable[[dict]], 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
        ```

    *   *Refining `JobQueue.get_next_job`*:
        If I return `(id, data)` but don't remove it, the "Queue" behaves like a "Priority List".
        However, a "Queue" usually removes. If I remove it, `process_job` by `job_id` might fail if the user called `get_next_job` first.
        But the prompt doesn't say `get_next_job` removes it.
        Let's check the wording: "return the highest-priority job's (id, data)". It doesn't say "remove".
        However, if it doesn't remove, and I call `get_next_job` twice, I get the same thing.
        Standard behavior for a `Queue`'s `get` method is removal.
        I'll implement it so it doesn't remove, as the prompt doesn't specify. If I were to remove it, I'd have to return it.
        Actually, I'll look at the requirements again.
        `process_job(self, job_id: str, Processor: Callable) -> bool`.
        If `process_job` is the intended way to finish a job, and `get_next_job` is just to peek, that makes sense.
        BUT, if I don't remove it in `process_job`, the queue grows forever.
        Usually, `process_job` should remove the job on success.
        Let's look at common patterns.
        Actually, I'll keep it simple:
        `get_next_job`: find the best one, return it. (No removal).
        `process_job`: find it by ID, run it, and if successful, I'll remove it from the internal list so it's not processed again.

    *   *Wait*, "FIFO order within the same priority".
        If I use `max(self.jobs, key=lambda j: j.priority)`, and there's a tie, `max` returns the *first* one encountered. This is perfect for FIFO!
        `max` returns the first occurrence of the maximum value.
        So:
        ```python
        def get_next_job(self) -> tuple[str, dict] | None:
            if not self.jobs:
                return None
            best_job = max(self.jobs, key=lambda j: j.priority)
            return (best_job.id, best_job.data)
        ```
        This is clean and satisfies the priority + FIFO requirement.

    *   *Wait*, `process_job` removal:
        If `process_job` succeeds, should I remove it?
        The prompt doesn't say. But a queue usually removes processed items.
        I'll remove it from `self.jobs` only if it succeeds. Or maybe I should remove it regardless of success?
        If `RetryPolicy` reaches `max_attempts` and fails, it's still a failed job.
        Let's just remove it regardless of success to keep the queue clean, OR just leave it.
        Actually, the prompt doesnings don't specify removal at all for `process_job`.
        I will simply not remove it, or only remove it if it succeeds.
        Let's go with: `process_job` finds the job, executes, and if `success` is True, it removes it from the internal list.

    *   *Wait*, the requirement: `process_job` must use `RetryPolicy`.
        ```python
        def process_job(self, job_id: str, Processor: Callable) -> bool:
            job = next((j for j in self.jobs if j.id == job_id), None)
            if not job:
                return False
            success, attempts = self.retry_policy.run(Processor, job.data)
            job.retries = attempts
            if success:
                self.jobs.remove(job)
            return success
        ```

    *   One small thing: `Priority` order. "Highest-priority".
        Usually, higher number = higher priority. `max(..., key=lambda j: j.priority)` handles this.

    *    `RetryPolicy` needs `attempts_made` to be returned.
    * `Job` needs `retries`.
    * `JobQueue` needs `add_job`, `process_job`, `get_next_job`.
    * `RetryPolicy.run` takes `fn` and `data`.
    * No imports other than `dataclass`, `Callable`, `Optional`, `Tuple` (or just use `tuple` in Python 3.9+).

    *   *Refining `RetryPolicy`*:
        The prompt says `retries` is a field in `Job`.
        `RetryPolicy` is supposed to "encapsulate retry-with-backoff".
        Does `RetryPolicy` need to know about the `Job` object to update `retries`?
        The `run` method signature is `run(self, fn: Callable, data) -> tuple[bool, int]`.
        It doesn't take the `Job` object. It takes `data`.
        So `process_job` must be the one to update `job.retries` using the returned integer.

    *   *Refining `JobQueue`*:
        Should `process_job` remove the job from the queue?
        In a real queue, `process_job` implies the job is being handled. If it's handled, it should be removed.
        However, `get_next_job` is also there.
        If `get_next_job` is used to see what's next, and `process_job` is used to actually do it, then `process_job` must remove it.

    *   *Wait*, what if `process_job` is called with a `job_id` that was already "popped" by `get_next_job`?
        If `get_next_job` doesn't pop, then `process_job` can still find it.
        If `get_next_job` does pop, then `process_job` would fail.
        Given the instructions, I'll assume `get_next_job` is a peek (no removal) and `process_job` is the action (removal on success). This is a safe way to interpret "Queue" when "get" and "process" are separate.

    Actually, looking at the `Job` dataclass: `retries` is a field.
    If `process_job` is called multiple times for the same ID (though that's unlikely), `retries` would increment.

    Wait, the requirement says: `process_job` must use `RetryPolicy`.
    If `process_job` finds a job that has `retries` already?
    Actually, the `RetryPolicy` doesn't care about the `Job`'s `retries` field. It just does its own loop.
    So `job.retries` might just be for record-keeping.

    One detail: `tuple[bool, int]` - this syntax requires Python 3.9+. I'll use `typing.Tuple` or just standard `tuple` if I assume modern Python. The prompt asks for `tuple[bool, int]`.

    *   *Final check on `RetryPolicy.run`*:
        "retries on any exception until success or `max_attempts` reached".
        The `attempts_made` should be the total number of times `fn(data)` was called.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Tuple, Optional, List

    @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[[dict]], 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: List[Job] = []
            self.retry_policy = RetryPolicy()

        def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
            new_job = Job(id=job_id, data=data, priority=priority)
            self.jobs.append(new_job)

        def process_job(self, job_id: str, Processor: Callable[[dict]]) -> bool:
            # Find the job in the list
            job = None
            for j in self.jobs:
                if j.id == job_id:
                    job = j
                    break
            
            if not job:
                return False
            
            success, attempts = self.retry_policy.run(Processor, job.data)
            job.retries = attempts
            
            if success:
                self.jobs.remove(job)
            
            return success

        def get_next_job(self) -> tuple[str, dict] | None:
            if not self.jobs:
                return None
            # Highest priority, then FIFO (first occurrence of max priority )
            best_job = max(self.jobs, key=lambda j: j.priority)
            return (best_job.id, best_job.data)
    ```

    Actually, `max()` is stable in the sense that if multiple items have the same max value, it returns the first one.
    `max([1, 2, 3, 2], key=lambda x: x)` -> returns `3`.
    `max([1, 3, 3, 2], key=lambda x: x)` -> returns the first `3`.
    Perfect.

    Wait, `tuple[bool, int]` might not work in some environments if `tuple` isn't subscriptable. But the prompt uses it in the description. I'll use `typing.Tuple` to be safe or just use the bracket notation.

    Let's re-read the `RetryPolicy.run` description: `retries on any exception until success or max_attempts reached`.
    This means if `max_attempts` is 4:
    Attempt 1: Fail
    Attempt 2: Fail
    Attempt 3: Fail
    Attempt 4: Fail
    Return `(False, 4)`.
    My code does this correctly.

    Let's check `process_job` again.
    If I don't remove the job, and `get_next_job` doesn't remove it, the queue is just a list that gets processed by ID.
    The prompt says `JobQueue` "orchestrates Jobs".
    Usually, `process_job` would remove the job from the queue once it's processed (either success or final failure).
    If I don't remove it, `get_next_job` will always return the same job until it's removed.
    I'll remove it on success.

    One final thing: `process_job` return type is `bool`.
    Success = True, Failure = False.

    Wait, one more look at `get_next_job`. "highest-priority".
    Usually, in PriorityQueues, a priority of 10 is higher than 1.
    So `max(..., key=lambda j: j.priority)` is correct.

    Wait, `RetryPolicy` has `max_attempts`. Default 4.
    If it succeeds on the first try, `attempts_made` is 1.
    If it succeeds on the 4th try, `attempts_made` is 4.
    If it fails all 4 tries, `attempts_made` is 4.

    Wait, I should check the `RetryPolicy` signature: `run(self, fn: Callable, data)`.
    I'll use `Callable` from `typing`.

    Final check on `JobQueue.process_job`:
    If `job_id` is not found, return `False`? Or should it raise? The signature says `-> bool`. So `False` is appropriate.

    Wait, the prompt says "Write only the Python code, no explanations."
    I will provide the `solution.py` file content.

    One detail: `tuple[str, dict] | None`. This is Python 3.10+ syntax.
    I will use `Optional[Tuple[str, dict]]` or just the newer syntax if I'm sure.
    Given the prompt uses `tuple[bool, int]`, I'll use the `tuple[...]` notation.

    Wait, the prompt says: `process_job(self, job_id: str, Processor: Callable) -> bool`.
    It doesn't say "remove it".
    I'll stick to the simplest implementation:
    - `add_job`: appends to a list.
    - `get_next_job`: finds max priority and returns id/data.
    - `process_job`: finds by id, runs via policy, updates `retries`, removes if success.

    Actually, I'll remove it from the list in `process_job` regardless of success? No, usually retries means it's still a "job" until it's finished. If it fails all retries, it's a failed job. I'll remove it on success. If it fails all retries, it stays in the queue? That's weird.
    Let's think. If it's a queue, and `process_job` is called, it's being processed.
    If it fails all retries, it's "done" (but failed).
    I'll remove it from the list on **any** completion of `RetryPolicy.run`.
    Because `RetryPolicy.run` is the "orchestrator" of the retry logic.
    Actually, I'll remove it only on success to be safe, or I'll just leave it in and let the user handle it.
    Actually, the most common pattern for these tasks is that `process_job` completes the job's lifecycle.
    I'll remove it regardless of success because it has been "processed".

    Wait, let me re-read: "orchestrates Jobs using RetryPolicy".
    If `RetryPolicy.run` returns `(success, attempts)`, then `process_job` knows if it succeeded.
    I'll remove it on success.

    One more check: `FIFO order within the same priority`.
    I'll use `self.jobs.index(best_job)` to ensure it's the first one?
    No, `max()` already returns the first one if there's a tie.

    Let's double check:
    `max([Job(id='a', priority=1), Job(id='b', priority=1)], key=lambda j: j.priority)`
    In Python, `max` returns the first element encountered in case of a tie.
    So `max(self.jobs, key=lambda j: j.priority)` will return the one with the lowest index among those with the highest priority. This is FIFO.

    Wait, the prompt uses `tuple[bool, int]` and `tuple[str, dict] | None`.
    I should ensure the types are correct.

    ```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[[dict]], data: dict) -> tuple[bool, int]:
            attempts_made = 0
            while attempts_made < self.max_attempts:
                try:
                    fn(data)
                    return True, attempts_made + 1
                except Exception:
                    attempts_made += 1
            return False, attempts_made

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

        def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
            self.jobs.append(Job(id=job_id, data=data, priority=priority)))

        def process_job(self, job_id: str, Processor: Callable[[dict]]) -> bool:
            job = next((j for j in self.jobs if j.id == job_id), None)
            if not job:
                return False
            
            success, attempts = self.retry_policy.run(Processor, job.data)
            job.retries = attempts
            
            if success:
                self.jobs.remove(job)
            return success

        def get_next_job(self) -> tuple[str, dict] | None:
            if not self.jobs:
                return None
            best_job = max(self.jobs, key=lambda j: j.priority)
            return (best_job.id, best_job.data)
    ```

    Wait, `process_job` signature says `Processor: Callable`. I'll make sure it's handled correctly.
</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[[dict]], data: dict) -> tuple[bool, int]:
        attempts_made = 0
        while attempts_made < self.max_attempts:
            try:
                fn(data)
                return True, attempts_made + 1
            except Exception:
                attempts_made += 1
        return False, attempts_made

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

    def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
        new_job = Job(id=job_id, data=data, priority=priority)
        self.jobs.append(new_job)

    def process_job(self, job_id: str, Processor: Callable[[dict]]) -> bool:
        # Locate the specific job by ID
        job = next((j for j in self.jobs if j.id == job_id), None)
        if not job:
            return False
        
        # Execute using the RetryPolicy
        success, attempts = self.retry_policy.run(Processor, job.data)
        job.retries = attempts
        
        # Remove from queue if successfully processed
        if success:
            self.jobs.remove(job)
            
        return success

    def get_next_job(self) -> tuple[str, dict] | None:
        if not self.jobs:
            return None
        
        # highest-priority (max value), then FIFO (first occurrence in list)
        best_job = max(self.jobs, key=lambda j: j.priority)
        return (best_job.id, best_job.data)
```
</content>