<reasoning_content>
*   Goal: Refactor a job queue into three composable classes in `solution.py`.
    *   Constraints: Use only standard library. No external packages. No explanations.

    *   Fields: `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    *   Decorator: `@dataclass`.

    *   `__init__(self, max_attempts: int = 4)`.
    *   `run(self, fn: Callable, data) -> tuple[bool, int]`.
    *   Logic: Calls `fn(data)`, retries on any exception until success or `max_attempts` reached.
    *   Return: `(success, attempts_made)`.
    *   Requirement: Do NOT sleep.

    *   `add_job(self, job_id: str, data: dict, priority: int = 0) -> None`.
    *   `process_job(self, job_id: str, processor: Callable) -> bool`.
        *   Must *use* `RetryPolicy`.
    *   `get_next_job(self) -> tuple[str, dict] | None`.
        *   Highest priority first.
        *   FIFO within same priority.

    *   For priority queue with FIFO within same priority: Use `heapq`.
    *   `heapq` is a min-heap. To make it a max-priority queue, we can store `(-priority, insertion_counter, job_id, data)`.
    *   Wait, the requirements for `JobQueue` don't explicitly say I must store the `Job` objects in the queue, but `add_job` takes params. It's better to store `Job` objects or a representation in the heap.
    *   Actually, `Job` is a dataclass. `add_job` adds a job. `get_next_job` returns the highest-priority job.
    *   To handle FIFO for same priority: `heapq.heappush(self._queue, (-priority, self._counter, job))`.
    *   Wait, the `process_job` method takes a `job_id`. This implies the jobs are stored somewhere accessible by ID, or `process_job` assumes the job is being processed after being retrieved.
    *   Looking at `process_job(self, job_id, processor)`: it needs to find the job by ID. So I'll need a dictionary for lookup and a heap for priority.
    *   However, `get_next_job` returns `(id, data)`.
    *   Let's re-read: `process_job` takes `job_id`. This implies there's a way to look up the job object by its ID.
    *   `JobQueue` structure:
        *   `self._jobs: dict[str, Job]` (to find jobs by ID for `process_job`).
        *   `self._priority_queue: list` (heap for getting next job).
        *   `self._counter: int` (to ensure FIFO for same priority in heap).

    *   `Job`: `dataclass(id, data, priority, retries)`.
    *   `RetryPolicy`:
        ```python
        def run(self, fn, data):
            attempts = 0
            while attempts < self.max_attempts:
                attempts += 1
                try:
                    fn(data)
                    return True, attempts
                except Exception:
                    continue
            return False, attempts
        ```
    *   `JobQueue`:
        *   `add_job`: create `Job`, put in `self._jobs`, put in heap.
        *   `get_next_job`: pop from heap, remove from `self._jobs`, return info.
        *   `process_job`: lookup `job` by `job_id` in `self._jobs`, use `RetryPolicy.run(processor, job.data)`. If success, maybe remove? The prompt doesn't say what happens to the job after `process_job`. Usually, it's removed or updated. If `process_job` returns `bool`, I'll assume it returns whether the execution (with retries) was successful.

    *   If `process_job` is called with `job_id`, I need to find that job.
    *   If `get_next_job` is called, I pop the job.
    *   Wait, if `get_next_job` pops the job, then `process_job` might be called on a job that was already popped?
    *   Usually, in these patterns, `get_next_job` retrieves it, and then you process it. But the signature `process_job(self, job_id, processor)` suggests the queue *manages* the job's lifecycle.
    *   If `get_next_job` is "get the next job", it typically removes it from the queue. If it's removed, `process_job(job_id, ...)` won't find it in the priority queue, but it might find it in an "active" or "pending" state.
    *   However, looking at the requirement: `get_next_job` returns `(id, data)`. `process_job` uses `RetryPolicy`.
    *   Let's assume `add_job` adds to internal storage. `get_next_job` finds the next one to process. `process_job` is a method to actually run the logic on a specific job.
    *   If `process_job` is intended to be used *after* `get_next_job`, the job must still be accessible.
    *   Wait, if `get_next_job` removes the job from the queue, `process_job` can't find it by ID unless it's stored in a "processing" dict.
    *   Let's look at standard patterns:
        1. `add_job` -> put in queue.
        2. `get_next_job` -> pop from queue, return info.
        3. `process_job` -> takes a specific ID.
    *   Actually, a simpler interpretation: `JobQueue` contains the jobs. `get_next_job` peeks or pops? "return the highest-priority job's (id, data)". Usually implies popping.
    *   If `process_job` is part of the queue, maybe it's meant to be:
        `job_id, data = queue.get_next_job()`
        `queue.process_job(job_id, processor)`
    *   For this to work, `process_job` needs the job to be in the `self._jobs` dict.
    *   If `get_next_job` pops it from the heap, it should still stay in `self._jobs` until `process_job` is done? Or maybe `get_next_job` just returns the info and `process_job` handles the rest?
    *   Actually, many implementations treat `get_next_job` as "take it out of the queue and give it to me".
    *   Let's refine `JobQueue`:
        *   `self._jobs: dict[str, Job]`
        *   `self._heap: list` (contains `(-priority, counter, job_id)`)
        *   `self._counter: int`
        *   `add_job`: `Job(id, data, priority)`, `self._jobs[id] = job`, `heapq.heappush(self._heap, (-priority, counter, id))`
        *   `get_next_job`: `heapq.heappop(self._heap)`, returns `(id, job.data)`. Wait, if I pop it, it's gone from the heap. If `process_job` is then called, it needs to find it.
        *   If `process_job` is supposed to handle the retry logic for a specific ID, then the job must exist.
        *   If `process_job` is successful, `del self._jobs[job_id]`.
        *   If `process_job` fails after all retries, what happens? The prompt doesn't say. I'll just return the success status.

    *Let's reconsider the `get_next_job` vs `process_job` flow:*
    If I `get_next_job`, I get the ID. Then I call `process_job(id, processor)`.
    If `process_job` is supposed to be able to find the job, I shouldn't delete it from `self._jobs` inside `get_next_job`. I should only delete it from `self._jobs` once it's processed or if it's truly gone.
    But `get_next_job` is usually a "pop" operation.
    
    Let's look at the requirement for `get_next_job`: "return the highest-priority job's (id, data)".
    And `process_job`: "must USE RetryPolicy".
    
    Let's try this:
    - `add_job`: Adds to `self._jobs` (dict) and `self._heap` (heap).
    - `get_next_job`: Pops from `self._heap`. Finds the job in `self._jobs`. Returns `(id, data)`.
    - `process_job`: Finds job in `self._jobs`. Uses `RetryPolicy`. Returns `True/False`. After processing, removes it from `self._jobs`.
    
    Wait, if `get_next_job` pops it, it's no longer in the heap. If I then call `process_job`, it works because it's in `self._jobs`.
    This seems consistent.

    *   `Job`: `dataclass`
    *   `RetryPolicy`: `run` method.
    *   `JobQueue`:
        *   `add_job(job_id, data, priority)`
        *   `get_next_job() -> tuple[str, dict] | None`
        *   `process_job(job_id, processor) -> bool`
    *   `heapq` for priority.
    *   `self._counter` for FIFO.

    *   `RetryPolicy.run(self, fn, data)`:
        *   `attempts = 0`
        *   `while attempts < self.max_attempts`:
        *   `attempts += 1`
        *   `try: fn(data); return True, attempts`
        *   `except Exception: pass`
        *   `return False, attempts`

    *   `JobQueue.get_next_job()`:
        *   If heap empty, return `None`.
        *   `priority, count, job_id = heappop`.
        *   `job = self._jobs[job_id]`.
        *   Return `(job_id, job.data)`.
        *   *Crucial:* Should I remove it from `self._jobs` here? If I don't, and `process_job` is called, I can find it. If I do, `process_job` won't find it. So I will keep it in `self._jobs` until `process_job` is called. But wait, if `get_next_job` is called twice, I'll get the same job twice if I don't remove it from the dict? No, `heappop` removes it from the heap. So `get_next_job` will only return a job once.
        *   *Wait*, if `get_next_job` returns a job, but the user *never* calls `process_job`, that job is stuck in `self._jobs` forever? That's okay for this exercise.
        *   *However*, what if `process_job` is called with an ID that was already popped? `process_job` needs to find it. If I pop it from the heap, it's no longer "next". If I call `get_next_job` again, I get the *next* one. This is correct.

    *   What if `process_job` is called with an ID that is *not* in `self._jobs`? The prompt doesn't specify. I'll just return `False`.

    *   Wait, the requirement for `JobQueue.process_job`: "must USE RetryPolicy".
    *   It also says `process_job` returns `bool`.
    *   One detail: "return the highest-priority job's (id, data); FIFO order within the same priority."
    *   Heap items: `(-priority, count, job_id)`.

    - `Job` (dataclass)
    - `RetryPolicy` (max_attempts, run)
    - `JobQueue` (add_job, process_job, get_next_job)

    Let's refine `process_job`:
    ```python
    def process_job(self, job_id: str, processor: Callable) -> bool:
        if job_id not in self._jobs:
            return False
        job = self._jobs[job_id]
        policy = RetryPolicy(self.max_retries) # Wait, where does max_retries come from?
    ```
    Ah, the `JobQueue` doesn't have a `max_retries` in its `__init__` in the prompt. But `RetryPolicy` does. The prompt says "orchestrates Jobs using RetryPolicy". It doesn't say `JobQueue` must take `max_attempts` in `__init__`, but to use `RetryPolicy` effectively, it should probably have an instance or be configured.
    Actually, the prompt says: `process_job(...) -> bool`. It doesn't specify how `RetryPolicy` is instantiated. I'll instantiate it inside `process_job` or at `JobQueue.__init__`.
    Let's look at the prompt again: "`JobQueue` — orchestrates Jobs using RetryPolicy".
    I will add `max_attempts` to `JobQueue.__init__` or just default it to 4 to be safe and compliant with the pattern.
    Actually, I'll just instantiate `RetryPolicy(max_attempts=4)` inside `process_job` or `JobQueue`.

    Wait, looking at `process_job` signature: `process_job(self, job_id: str, processor: Callable) -> bool`.
    If I instantiate `RetryPolicy` inside `process_job`, it satisfies "must USE RetryPolicy".

    Let's check the `Job` fields again: `retries: int = 0`.
    Does `process_job` need to update the `job.retries` field?
    The `RetryPolicy.run` returns `attempts_made`.
    The `Job` has a `retries` field. Maybe I should update it?
    The prompt says: `Job` has `retries: int = 0`.
    It doesn't explicitly say to update it, but it's good practice. However, `RetryPolicy.run` returns `attempts_made`.
    Actually, `attempts_made` is the total attempts in that one `run` call.
    Let's keep it simple to avoid over-engineering.

    - `Job`: `id`, `data`, `priority`, `retries`.
    - `RetryPolicy`: `max_attempts`, `run(fn, data) -> (success, attempts)`.
    - `JobQueue`: `add_job`, `process_job`, `get_next_job`.

    One detail: `get_next_job` returns `tuple[str, dict] | None`.
    The heap should store `(-priority, counter, job_id)`.
    `self._jobs` will store the `Job` object.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Any
    import heapq

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

    class JobQueue:
        def __init__(self):
            self._jobs: dict[str, Job] = {}
            self._heap = []
            self._counter = 0
            self._retry_policy = RetryPolicy()

        def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
            job = Job(id=job_id, data=data, priority=priority)
            self._jobs[job_id] = job
            heapq.heappush(self._heap, (-priority, self._counter, job_id))
            self._counter += 1

        def process_job(self, job_id: str, processor: Callable) -> bool:
            if job_id not in self._jobs:
                return False
            job = self._jobs[job_id]
            success, attempts = self._retry_policy.run(processor, job.data)
            # The prompt doesn't say to update job.retries, but let's be safe.
            # Actually, the prompt says "retries: int = 0" in Job.
            # Let's just focus on returning the bool.
            if success:
                del self._jobs[job_id]
            return success

        def get_next_job(self) -> tuple[str, dict] | None:
            if not self._heap:
                return None
            _, _, job_id = heapq.heappop(self._heap)
            if job_id in self._jobs:
                job = self._jobs[job_id]
                return job.id, job.data
            return None # Should not happen with correct logic
    ```

    *Self-Correction on `get_next_job` and `process_job`*:
    If `get_next_job` pops from the heap, but `process_job` is responsible for deleting from `self._jobs`, then the job is correctly handled.
    But if `get_next_job` is called, and then `process_job` is called and it fails, should the job go back to the queue?
    The prompt doesn't say. In most job queue systems, if a job fails, it's either dead-lettered or put back. But the instructions are very specific about what the methods do.
    `process_job` returns `bool`.
    `get_next_job` returns `(id, data)`.
    If I pop from heap in `get_next_job`, and the user fails to process it, it's gone from the queue. This is a common way to implement "at-most-once" or "at-least-once" (if you re-add).
    Given the instructions, I will stick to the simplest implementation:
    1. `add_job` adds to `_jobs` and `_heap`.
    2. `get_next_job` pops from `_heap` and returns `(id, data)`. It *does not* remove from `_jobs`.
    3. `process_job` uses `RetryPolicy` on `_jobs[job_id]`. If successful, it removes from `_jobs`.

    *Wait*, if `get_next_job` doesn't remove from `_jobs`, and `process_job` only removes on *success*, then if a job fails, it stays in `_jobs` but is no longer in `_heap`. That's fine.
    However, if `get_next_job` is called, it pops the ID from the heap. If I call `get_next_job` again, I get the next ID.
    This means `get_next_job` acts as the "fetch" mechanism.

    *One small problem*: What if `get_next_job` is called, we get the ID, and then we call `process_job`, but `process_job` fails? The job is still in `self._jobs` but not in `self._heap`. It's basically lost to the queue (not being retried automatically by the queue itself, but by the `RetryPolicy` during the `process_job` call). This matches the requirement.

    *Wait*, if `process_job` fails, should I delete it from `self._jobs`?
    If I don't, the `_jobs` dict will grow indefinitely with failed jobs.
    If I do, then `process_job` follows the "run once (with retries) then it's gone" logic.
    The requirement for `process_job` is: "returns `bool`".
    The requirement for `get_next_job` is: "return the highest-priority job's (id, data)".
    
    Let's refine `process_job` to delete the job regardless of success or failure to prevent memory leaks, as the `RetryPolicy` has already exhausted its attempts.

    ```python
    def process_job(self, job_id: str, processor: Callable) -> bool:
        if job_id not in self._jobs:
            return False
        job = self._jobs.pop(job_id) # Remove it immediately
        success, _ = self._retry_policy.run(processor, job.data)
        return success
    ```
    Wait, if I pop it from `_jobs` in `process_job`, I must ensure `get_next_job` can find it.
    If `get_next_job` pops from the heap, it gets the ID. Then the user calls `process_job(id, processor)`.
    This works perfectly.

    Final plan for `JobQueue`:
    - `__init__`: `self._jobs = {}`, `self._heap = []`, `self._counter = 0`, `self._policy = RetryPolicy()`.
    - `add_job(id, data, priority)`: 
        - `job = Job(id, data, priority)`
        - `self._jobs[id] = job`
        - `heappush(self._heap, (-priority, self._counter, id))`
        - `self._counter += 1`
    - `get_next_job()`:
        - `if not self._heap: return None`
        - `_, _, job_id = heappop(self._heap)`
        - `if job_id in self._jobs: return job_id, self._jobs[job_id].data`
        - `return None`
    - `process_job(id, processor)`:
        - `if id not in self._jobs: return False`
        - `job = self._jobs.pop(id)`
        - `success, _ = self._policy.run(processor, job.data)`
        - `return success`

    *Wait*, one edge case: if `get_next_job` pops an ID that was already removed from `_jobs` (e.g., by a previous `process_job` call that was somehow called directly), `get_next_job` should return `None`. My code handles this.

    *Another edge case*: `get_next_job` returns `(id, data)`. The user then calls `process_job(id, processor)`.
    This seems the most logical flow.

    *Wait*, a detail: `Job` dataclass has `retries`. Should I use it?
    The prompt says: `Job` has `retries: int = 0`.
    `RetryPolicy.run` returns `attempts_made`.
    If I want to be very thorough, I could do `job.retries = attempts_made - 1`, but the prompt doesn't require it. I'll leave it as is to stay strictly within the requested logic.

    *Wait*, the `get_next_job` signature: `tuple[str, dict] | None`.
    My code: `return job.id, job.data`. Correct.

    *Final check on `RetryPolicy.run`*:
    "calls `fn(data)`, retries on any exception until success or `max_attempts` reached, returns `(success, attempts_made)`."
    My code:
    ```python
    attempts = 0
    while attempts < self.max_attempts:
        attempts += 1
        try:
            fn(data)
            return True, attempts
        except Exception:
            pass
    return False, attempts
    ```
    If `max_attempts` is 4:
    - `attempts` = 0. Loop 1: `attempts` = 1. Try. Fail.
    - `attempts` = 1. Loop 2: `attempts` = 2. Try. Fail.
    - `attempts` = 2. Loop 3: `attempts` = 3. Try. Fail.
    - `attempts` = 3. Loop 4: `attempts` = 4. Try. Fail.
    - `attempts` = 4. Loop ends. Return `False, 4`.
    This is correct.

    Wait, `Job` is a dataclass. I need `from dataclasses import dataclass`.
    I need `from typing import Callable, Any` or just use `Callable` if I'm on Python 3.9+. The prompt uses `tuple[bool, int]` and `tuple[str, dict] | None`, which are Python 3.10+ syntax. I'll use `from typing import Callable`.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Any
    import heapq

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

    class JobQueue:
        def __init__(self):
            self._jobs: dict[str, Job] = {}
            self._heap: list = []
            self._counter: int = 0
            self._retry_policy = RetryPolicy()

        def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
            job = Job(id=job_id, data=data, priority=priority)
            self._jobs[job_id] = job
            heapq.heappush(self._heap, (-priority, self._counter, job_id))
            self._counter += 1

        def process_job(self, job_id: str, processor: Callable) -> bool:
            if job_id not in self._jobs:
                return False
            job = self._jobs.pop(job_id)
            success, _ = self._retry_policy.run(processor, job.data)
            return success

        def get_next_job(self) -> tuple[str, dict] | None:
            while self._heap:
                _, _, job_id = heapq.heappop(self._heap)
                if job_id in self._jobs:
                    job = self._jobs[job_id]
                    return job.id, job.data
            return None
    ```
    The `while` in `get_next_job` is a safety measure in case `process_job` was called manually on a job that was still in the heap. This is a robust way to handle it.

    Wait, the `Job` dataclass definition in the prompt:
    `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    Note the order. Dataclass fields with default values must come after fields without.
    `id` (no default)
    `data` (no default)
    `priority` (default 0)
    `retries` (default 0)
    This is correct.

    `get_next_job` returns `(id, data)`.
    `process_job` takes `job_id`.
    If `get_next_job` is called, it pops the item from the heap.
    If `process_job` is called, it pops the item from the dict.
    This is a consistent way to ensure each job is processed once.

    Wait, one small detail: `process_job` returning `bool`.
    `RetryPolicy.run` returns `(success, attempts_made)`.
    So `success, _ = self._retry_policy.run(...)` and `return success` is correct.

    The prompt says: "Write only the Python code, no explanations."

    ```python
    from dataclasses import dataclass
    from typing import Callable, Any
    import heapq

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

    class JobQueue:
        def __init__(self):
            self._jobs: dict[str, Job] = {}
            self._heap: list = []
            self._counter: int = 0
            self._retry_policy = RetryPolicy()

        def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
            job = Job(id=job_id, data=data, priority=priority)
            self._jobs[job_id] = job
            heapq.heappush(self._heap, (-priority, self._counter, job_id))
            self._counter += 1

        def process_job(self, job_id: str, processor: Callable) -> bool:
            if job_id not in self._jobs:
                return False
            job = self._jobs.pop(job_id)
            success, _ = self._retry_policy.run(processor, job.data)
            return success

        def get_next_job(self) -> tuple[str, dict] | None:
            while self._heap:
                _, _, job_id = heapq.heappop(self._heap)
                if job_id in self._jobs:
                    job = self._jobs[job_id]
                    return job.id, job.data
            return None
    ```
    Final thought: Is there any ambiguity in "FIFO order within the same priority"?
    In a min-heap, if we use `(-priority, counter, job_id)`, for a fixed priority, the `counter` (which increments) will be the tie-breaker. A smaller `counter` means it was added earlier. So `heappop` will return the smallest `counter`. This is exactly FIFO. Perfect.

    One more check: `JobQueue.process_job` must *USE* `RetryPolicy`. I'm using it.
    `JobQueue.get_next_job` must return `tuple[str, dict] | None`. I'm doing that.
    `JobQueue.add_job` takes `job_id, data, priority`. I'm doing that.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable, Any
import heapq

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

class JobQueue:
    def __init__(self):
        self._jobs: dict[str, Job] = {}
        self._heap: list = []
        self._counter: int = 0
        self._retry_policy = RetryPolicy()

    def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
        job = Job(id=job_id, data=data, priority=priority)
        self._jobs[job_id] = job
        # Use negative priority for max-heap behavior and counter for FIFO tie-breaking
        heapq.heappush(self._heap, (-priority, self._counter, job_id))
        self._counter += 1

    def process_job(self, job_id: str, processor: Callable) -> bool:
        if job_id not in self._jobs:
            return False
        
        job = self._jobs.pop(job_id)
        success, _ = self._retry_policy.run(processor, job.data)
        return success

    def get_next_job(self) -> tuple[str, dict] | None:
        while self._heap:
            _, _, job_id = heapq.heappop(self._heap)
            if job_id in self._jobs:
                job = self._jobs[job_id]
                return job.id, job.data
        return None
```
</content>