<reasoning_content>
*   Goal: Refactor a job queue into three composable classes in `solution.py`.
    *   Language: Python (Standard Library only).

    *   **Class 1: `Job`**
        *   Type: `dataclass`.
        *   Fields: `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.

    *   **Class 2: `RetryPolicy`**
        *   `__init__(self, max_attempts: int = 4)`.
        *   `run(self, fn: Callable, data) -> tuple[bool, int]`.
        *   Logic: Calls `fn(data)`, retries on exception up to `max_attempts`.
        *   Return: `(success, attempts_made)`.
        *   Constraint: No actual sleeping.

    *   **Class 3: `JobQueue`**
        *   `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`.
            *   Priority-based.
            *   FIFO within the same priority.

    *   `Job`:
        ```python
        from dataclasses import dataclass
        @dataclass
        class Job:
            id: str
            data: dict
            priority: int = 0
            retries: int = 0
        ```

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

    *   `JobQueue`:
        *   Needs to store jobs.
        *   Needs priority queueing.
        *   `heapq` is good for priority queues.
        *   However, `heapq` is a min-heap. The prompt says "highest-priority". Usually, higher priority number = higher priority? Or lower number = higher priority?
        *   Standard convention in task queues: Higher priority number = processed first? Or lower number = processed first (like Linux niceness)?
        *   Wait, "highest-priority job". Usually, in these coding challenges, if `priority` is an integer, we need to decide if 10 > 1.
        *   Let's look at the prompt: `priority: int = 0`.
        *   Standard Python `heapq` is a min-heap. To get a max-priority queue, we often negate the priority.
        *   "FIFO order within the same priority": To handle this with `heapq`, we use a counter as a tie-breaker.
        *   `add_job`: Store the job in the heap.
        *   `get_next_job`: Pop from heap.
        *   `process_job`: This is a bit tricky. If `process_job` takes a `job_id`, I need to be able to find that job in the queue.
        *   Standard queue implementation:
            *   `self.jobs = []` (heap)
            *   `self.job_map = {}` (to find job by id)
            *   `self.counter = 0` (for FIFO tie-breaking)
        *   Wait, `process_job` parameters: `job_id: str`, `processor: Callable`.
        *   Does `process_job` remove the job? Usually, "process" implies it's being worked on. If it fails, what happens? The `RetryPolicy` handles the retries *during* the call to `process_job`.
        *   `process_job` logic:
            1. Find job by ID.
            2. Use `RetryPolicy.run(processor, job.data)`.
            3. If success, remove job from queue.
            4. If fails, maybe update `retries`? The prompt doesn't explicitly say what to do with failed jobs after `process_job` returns, other than that it returns a `bool`.
        *   Let's refine the storage. If I use a heap, I can't easily "find" a specific `job_id` to process it unless it's the one at the top.
        *   Wait, `get_next_job` returns `(id, data)`.
        *   `process_job` takes `job_id`. This implies the user might want to process a specific job that isn't necessarily the next one. But usually, a queue processes sequentially.
        *   If `process_job` is called for a specific `job_id`, I need a way to access it. A dictionary `self._jobs = {}` would work. But `get_next_job` needs priority.
        *   So: `self._all_jobs = {}` (id -> Job) and `self._priority_heap = []` (heap entries).
        *   When a job is added: `self._all_jobs[job_id] = job`, `heapq.heappush(self._priority_heap, (-priority, counter, job_id))`.
        *   When `get_next_job` is called: Pop from heap. But wait, what if the job was already processed or removed? We need to check if the `job_id` is still in `_all_jobs`.
        *   When `process_job(job_id, processor)` is called:
            1. `job = self._all_jobs.get(job_id)`
            2. If not found, return `False`.
            3. `policy = RetryPolicy()` (Wait, should policy be an instance variable or created inside? The prompt says "orchestrates Jobs using RetryPolicy". It's better to have a policy instance or create one. Let's assume a default one or create one inside `__init__`.)
            4. `success, attempts = self.retry_policy.run(processor, job.data)`
            5. If success: `del self._all_jobs[job_id]`.
            6. Return `success`.

    *   Let's re-read: `process_job(self, job_id: str, processor: Callable) -> bool`.
        *   If `process_job` is for a specific ID, I must keep a map.
        *   `get_next_job` needs to find the highest priority.
        *   What about "FIFO order within same priority"?
            *   If I use `(-priority, counter, job_id)`, the heap will sort by priority (descending) then counter (ascending), which is FIFO for same priority.

    *   `Job` dataclass: ok.
    *   `RetryPolicy`: `run(fn, data)` -> `(success, attempts)`. ok.
    *   `JobQueue`:
        *   `add_job`: `self._jobs[job_id] = Job(...)`, `heappush(self._heap, (-priority, self._counter, job_id))`.
        *   `get_next_job`: `while self._heap: ... pop ... check if id in jobs ... return`.
        *   `process_job`: `job = self._jobs.get(job_id)`, `success, _ = self.policy.run(processor, job.data)`, `if success: del self._jobs[job_id]`, `return success`.

    *Refinement on `process_job`*:
    Should `process_job` remove the job from the heap? The heap contains `job_id`. If I delete from `self._jobs`, the `get_next_job` logic (which checks if id in `self._jobs`) will naturally skip it when it eventually pops from the heap. This is a common way to handle "lazy deletion" in heaps.

    *Refinement on `RetryPolicy` instantiation*:
    The prompt says "`JobQueue` ... orchestrates Jobs using RetryPolicy". It doesn't say where `RetryPolicy` comes from. I'll initialize it in `JobQueue.__init__`.

    *Wait, priority order*:
    "highest-priority job's (id, data); FIFO order within the same priority".
    If `priority` 10 is higher than 0:
    Heap order: `(-priority, counter, job_id)`.
    Example:
    Job A: p=10, c=1
    Job B: p=10, c=2
    Job C: p=0, c=3
    Heap elements: `(-10, 1, 'A'), (-10, 2, 'B'), (0, 3, 'C')`.
    `heappop` will return `(-10, 1, 'A')` first. Correct.

    *   `from dataclasses import dataclass`
    *   `from typing import Callable`
    *   `import heapq`

    *   `RetryPolicy.run` returns `(success, attempts_made)`.
    *   `JobQueue.process_job` returns `bool`.
    *   `JobQueue.get_next_job` returns `tuple[str, dict] | None`.

    *One detail*: `RetryPolicy.run(self, fn: Callable, data)`. The `data` passed to `fn` should be the `job.data`.

    ```python
    from dataclasses import dataclass
    from typing import Callable
    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) -> tuple[bool, int]:
            attempts = 0
            while attempts < self.max_attempts:
                attempts += 1
                try:
                    fn(data)
                    return True, attempts
                except Exception:
                    pass
            return False, attempts

    class JobQueue:
        def __init__(self):
            self._jobs = {}  # id -> Job
            self._heap = []  # (-priority, counter, id)
            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:
            job = self._jobs.get(job_id)
            if not job:
                return False
            
            success, _ = self._retry_policy.run(processor, job.data)
            if success:
                del self._jobs[job_id]
            return success

        def get_next_job(self) -> tuple[str, dict] | None:
            while self._heap:
                priority_neg, counter, job_id = heapq.heappop(self._heap)
                if job_id in self._jobs:
                    job = self._jobs[job_id]
                    # Note: we don't delete it here because 'process_job' 
                    # is what actually processes it. 
                    # However, usually, in a queue, 'get_next_job' 
                    # implies taking it out. 
                    # But the prompt says 'get_next_job' returns (id, data).
                    # If I pop it from the heap, it's "taken".
                    # But 'process_job' needs to find it by ID. 
                    # If 'get_next_job' pops it from the heap, 
                    # 'process_job' won't find it in the heap but 
                    # it will find it in self._jobs.
                    # Let's check the interaction.
                    # If get_next_job pops from heap, it's gone from heap.
                    # If process_job is called, it's still in self._jobs.
                    # Once process_job finishes, it deletes from self._jobs.
                    # This seems consistent.
                    return job.id, job.data
            return None
    ```

    Wait, if `get_next_job` pops from the heap, but `process_job` is the one that deletes from `_jobs`, then `get_next_job` will return the same job again if called twice before `process_job` is called. That's not how queues work. `get_next_job` should likely return the next available job and *not* necessarily remove it from the system, or `process_job` is intended to be the primary way to consume.

    However, the requirement `get_next_job` returns `(id, data)`. If it's a queue, `get_next_job` usually removes the item. But if it removes the item from the heap, then `process_job(job_id)` must still work.

    Let's look at the `process_job` signature again: `process_job(self, job_id: str, processor: Callable) -> bool`.
    If `get_next_job` removes the job from the heap, and `process_job` is called with that ID, `process_job` must be able to find the job. My `self._jobs` dictionary preserves the job until `process_job` succeeds.

    *Wait*, if `get_next_job` removes from heap, and then I call `get_next_job` again, I get the *next* one. This is correct.
    If I call `process_job(id)` on the job I just got, it finds it in `self._jobs`, runs it, and deletes it from `self._jobs`.

    *One edge case*: If `process_job` fails (after all retries), what happens? The job stays in `self._jobs` but is no longer in the heap (because `get_next_job` popped it). This means the job is lost/stuck.
    Is that intended? Usually, if a job fails, it might be requeued or moved to a dead-letter queue. The prompt doesn't specify.
    Given the instructions, the simplest implementation is: `get_next_job` pops from heap to return, `process_job` removes from `_jobs` on success.

    *Correction on `get_next_job`*: If `get_next_job` pops from the heap, it essentially "claims" the job. If `process_job` is meant to be the consumer, the flow is:
    1. `job_id, data = queue.get_next_job()`
    2. `queue.process_job(job_id, processor)`

    If `get_next_job` pops from the heap, the job is removed from the priority order. If `process_job` then fails, the job is still in `self._jobs` but won't be returned by `get_next_job` because it's not in the heap. This is a valid way to handle "in-progress" jobs.

    *Wait*, I found a better way for `get_next_job`. If I want the job to be available for `process_job` but also follow priority, I'll keep the heap-based approach. The only thing is: if `process_job` is called for a `job_id` that was already popped by `get_next_job`, it still works because `_jobs` still has it.

    Let's double check `process_job` requirement: "must USE RetryPolicy".
    My `process_job` does `self._retry_policy.run(processor, job.data)`. This is correct.

    *Final check on `get_next_job` FIFO*:
    `heapq.heappush(self._heap, (-priority, self._counter, job_id))`
    If priority is 0, and counter is 1, 2, 3...
    Heap elements: `(0, 1, 'id1'), (0, 2, 'id2'), (0, 3, 'id3')`.
    `heappop` returns `(0, 1, 'id1')` then `(0, 2, 'id2')`. This is FIFO. Correct.

    *Final check on `process_job`*:
    The prompt doesn't say `process_job` should take the `Job` object, it takes `job_id`. So `self._jobs[job_id]` is necessary.

    One detail: `RetryPolicy.run` returns `(success, attempts_made)`.
    `JobQueue.process_job` returns `bool`.
    So `return success` is correct.

    ```python
    from dataclasses import dataclass
    from typing import Callable
    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: dict) -> 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:
            job = self._jobs.get(job_id)
            if not job:
                return False
            success, _ = self._retry_policy.run(processor, job.data)
            if success:
                del self._jobs[job_id]
            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
    ```

    Wait, if `get_next_job` pops it from the heap, and `process_job` is called, then `process_job` deletes it from `self._jobs`.
    What if `process_job` is *never* called? The job stays in `self._jobs` but isn't in the heap.
    What if `process_job` is called for a `job_id` that was *not* the next job? `process_job` still works because it looks in `self._jobs`. This allows random access processing.

    One small fix: In `RetryPolicy.run`, `data` might not be a `dict` in a generic sense, but the prompt says `data: dict` for `Job`, so `Callable` signature should be fine.

    Also, the prompt says: `get_next_job(self) -> tuple[str, dict] | None`.
    My implementation returns `tuple[str, dict] | None`.

    `Priority: 0` is the default.
    If I add Job(id='a', p=0) and Job(id='b', p=1).
    `heapq.heappush(..., (-0, 0, 'a'))`
    `heapq.heappush(..., (-1, 1, 'b'))`
    Heap: `[(-1, 1, 'b'), (0, 0, 'a')]`
    `heappop` returns `(-1, 1, 'b')`.
    So `priority=1` is higher than `priority=0`. This is standard.

    `max_attempts` is 4.
    `attempts = 0`
    `while attempts < 4:`
    1. `attempts = 1`, try, fail
    2. `attempts = 2`, try, fail
    3. `attempts = 3`, try, fail
    4. `attempts = 4`, try, fail
    `return False, 4`
    Correct.

    One minor thing: the `retries` field in `Job` is not actually used in the current logic (the `RetryPolicy` handles the retries locally during the `run` call). The prompt asks for the field to exist, so I'll keep it.

    The prompt says: "`run(self, fn: Callable, data) -> tuple[bool, int]`".
    My implementation: `def run(self, fn: Callable, data: dict) -> tuple[bool, int]:`.
    Since `Job.data` is `dict`, this is fine.

    Let's make sure `process_job` uses `RetryPolicy` as requested.
    `self._retry_policy.run(processor, job.data)` — Yes.

    Final verification of `get_next_job` return type: `tuple[str, dict] | None`.
    In Python 3.10+, `|` is allowed for types. The prompt uses it in the description, so it's safe to use.

    One tiny detail: `process_job` signature: `processor: Callable`.
    In `RetryPolicy.run`, `fn(data)` is called. This matches.

    - `Job` is a dataclass.
    - `RetryPolicy` handles retries without sleep.
    - `JobQueue` uses `RetryPolicy`.
    - `JobQueue` handles priority and FIFO.
    - Standard library only.
    - `get_next_job` returns `(id, data)`.
    - `process_job` returns `bool`.

    Wait, I should ensure `JobQueue`'s `_retry_policy` is an instance of `RetryPolicy` as requested by "orchestrates Jobs using RetryPolicy".

    One thought: Should `JobQueue` have a way to configure `RetryPolicy`? The prompt doesn't specify, so a default `RetryPolicy()` in `__init__` is the most logical.

    ```python
    from dataclasses import dataclass
    from typing import Callable
    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: dict) -> 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:
            job = self._jobs.get(job_id)
            if not job:
                return False
            success, _ = self._retry_policy.run(processor, job.data)
            if success:
                del self._jobs[job_id]
            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
    ```
    This looks solid.

    Is `job_id in self._jobs` enough?
    If `process_job` deletes from `_jobs`, then `get_next_job` correctly skips it.
    If `get_next_job` pops from `_heap`, the job is removed from the "next in line" consideration.
    The only potential issue is that `get_next_job` *removes* it from the priority queue, but `process_job` *consumes* it. This is standard behavior for a "get" operation in many queue implementations (though sometimes `get` is "peek" and `remove` is "pop"). However, in the context of a job queue, `get_next_job` returning the job and then `process_job` being called on it is a standard workflow.

    Wait, if `get_next_job` pops from the heap, then the next time I call `get_next_job`, I get a *different* job. This is correct.
    If I call `process_job` for the job I just got, it works.
    If I call `process_job` for a job I *didn't* get yet, it also works (it's still in `_jobs`).
    This covers all bases.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable
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: dict) -> 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
        # Use -priority for max-heap behavior and self._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:
        job = self._jobs.get(job_id)
        if not job:
            return False
        
        success, _ = self._retry_policy.run(processor, job.data)
        if success:
            del self._jobs[job_id]
        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>