<reasoning_content>
*   Refactor a job queue into three composable classes in `solution.py`.
    *   Requirement 1: `Job` dataclass (`id`, `data`, `priority`, `retries`).
    *   Requirement 2: `RetryPolicy` class (retry with backoff logic, `run` method returns `(success, attempts_made)`, no actual sleep).
    *   Requirement 3: `JobQueue` class (orchestrates `Job` and `RetryPolicy`, `add_job`, `process_job` using `RetryPolicy`, `get_next_job` with priority/FIFO logic).
    *   Constraint: Standard library only. No explanations.

    *   Imports: `dataclass`, `Callable`, `heapq` (for priority queue), `typing`.
    *   `Job`:
        ```python
        @dataclass(order=False) # Handled by priority and a sequence counter
        class Job:
            id: str
            data: dict
            priority: int = 0
            retries: int = 0
        ```
        *Correction*: Since `JobQueue` needs FIFO within same priority, I need an entry counter or similar to handle the priority queue comparison properly. `heapq` sorts by smallest first, so priority should be handled such that higher numbers are higher priority? Usually, in priority queues, lower numbers are higher priority, but the prompt says "highest-priority". Usually, high number = high priority? Or low number? I'll assume high number = higher priority, but `heapq` is a min-heap. So I'll store negative priority.

    *   `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`:
        *   Storage: Need a way to track jobs. A list for `heapq` is good for priority.
        *   `add_job`: Add to list.
        *   `get_next_job`: Pop from heap.
        *   `process_job`: Call `RetryPolicy.run`.

    Wait, `JobQueue` needs to handle FIFO for same priority.
    `heapq` works on tuples. If I use `(-priority, counter, job_id, data)`, it works.

    Let's refine `JobQueue`:
    ```python
    import heapq
    from dataclasses import dataclass
    from typing import Callable, Tuple, Optional

    @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 = [] # Min-heap
            self.counter = 0
            self.retry_policy = RetryPolicy()

        def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
            # Use negative priority for max-heap behavior using heapq (min-heap)
            # Use counter for FIFO
            heapq.heappush(self.jobs, (-priority, self.counter, job_id, data))
            self.counter += 1

        def process_job(self, job_id: str, processor: Callable) -> bool:
            # This method takes a job_id. Does it find the job in the heap or a dict?
            # Requirement says "process_job(self, job_id: str, ...)".
            # Usually, jobs are popped first. If process_job is called with an ID,
            # but the ID is already popped, it's tricky.
            # Or maybe process_job is called AFTER get_next_job?
            # Wait, the prompt says "process_job ... must USE RetryPolicy".
            # It doesn't say it pulls from the queue. It takes a job_id.
            # If it takes a job_id, it needs a way to find the data associated with that ID.
            # But get_next_job removes it.
            # Let's assume process_job is intended to be used on a job retrieved.
            # But the signature is `process_job(self, job_id: str, ...)`.
            # This implies the queue must maintain a registry of jobs.
            pass
    ```

    Re-reading: `process_job(self, job_id: str, processor: Callable) -> bool`.
    If I use a heap for `get_next_job`, finding a specific `job_id` in a heap is $O(N)$.
    However, usually, a job queue processes things in order.
    Maybe `process_job` is supposed to find the job, process it, and remove it?
    Or maybe `process_job` is meant to be called on a job object? No, the signature says `job_id: str`.

    Wait, if `get_next_job` returns `(id, data)`, and then the user calls `process_job(id, ...)`, the `JobQueue` needs to still have that data or a reference.
    But `get_next_job` usually "consumes" the job.
    If it's consumed, `process_job` can't find it unless it's stored elsewhere.

    Let's rethink.
    Maybe `JobQueue` stores jobs in a dictionary `self.jobs = {job_id: Job}` and a priority queue `self.heap = []`?
    Actually, if `get_next_job` returns `(id, data)`, and `process_job` takes `job_id`, `process_job` must access the job.
    If `get_next_job` removes it, then `process_job` must happen *before* or *during* the pop? No, the user calls them.

    Alternative interpretation:
    `get_next_job` returns the highest priority job *without* removing it? "return the highest-priority job's (id, data)". It doesn't say "remove".
    But usually "get next" implies popping.

    Let's look at the requirements again.
    1. `Job`: dataclass.
    2. `RetryPolicy`: `run` method.
    3. `JobQueue`: `add_job`, `process_job`, `get_next_job`.

    If `get_next_job` doesn't remove, then `process_job` should probably remove it?
    Or, `get_next_job` removes it, and `process_job` is called on a job that is *still* in some internal storage?

    Let's try this structure:
    - `self.queue`: a list (heap) of `(-priority, counter, job_id)`.
    - `self.jobs`: a dictionary `{job_id: Job}`.
    - `add_job`: adds to both.
    - `get_next_job`: pops from `self.queue`, retrieves `job_id`, returns `(job_id, job.data)`.
    - `process_job`: this is the problem. If it's already popped from the heap, how does `process_job` know the data?

    Wait, maybe `process_job` is supposed to find the highest priority job, process it, and remove it?
    No, the signature is `process_job(self, job_id: str, ...)`.

    Let's look at the logic again.
    `add_job` -> Adds to queue.
    `get_next_job` -> Returns the highest priority one.
    `process_job` -> Takes an ID, uses `RetryPolicy`.

    Maybe `get_next_job` does **not** remove the item.
    Then `process_job` must remove it? Or it doesn't have to remove it?
    Usually, if `get_next_job` doesn't remove, it's "peek".
    If `get_next_job` does remove, `process_job` is impossible unless `process_job` is called *before* `get_next_job` or `process_job` receives the data directly (but it doesn't, it receives `job_id`).

    Actually, there is a simpler way.
    `JobQueue` has a dictionary of all jobs added.
    `get_next_job` finds the one with highest priority from the dictionary? That's $O(N)$.
    Using a heap is better.

    Let's assume:
    1. `add_job`: Adds to a dictionary `self.jobs` and a heap `self.heap`.
    2. `get_next_job`: Pops from `self.heap`, retrieves `job_id`, returns `(job_id, data)`. *Crucially*, it does NOT remove from the dictionary.
    3. `process_job`: Uses the dictionary to find `data` for `job_id`, runs it via `RetryPolicy`.

    Wait, if `get_next_job` is called twice, it returns the same job? That's not a "queue".
    A "queue" usually consumes.
    If it consumes, then `process_job` must be called *before* `get_next_job`? No, that's backwards.

    What if `process_job` *is* the thing that retrieves and removes?
    No, the signature is `process_job(self, job_id: str, ...)`.

    Let's look at common patterns for these types of coding tasks.
    Usually, `get_next_job` is the consumer.
    If `get_next_job` returns `(id, data)`, the caller now has the data.
    But the `process_job` method is also provided.
    Maybe `process_job` is meant to be used *instead* of `get_next_job`? No, both are required.

    Let's try this:
    - `JobQueue` keeps a dictionary `self.jobs = {}`.
    - `add_job`: `self.jobs[job_id] = Job(...)`.
    - `get_next_job`: Finds the max priority in `self.jobs` and returns it. (Does not remove).
    - `process_job`: Finds `job_id` in `self.jobs`, calls `RetryPolicy`, and removes it from `self.jobs`?

    Actually, the requirement says "FIFO order within the same priority".
    If I don't remove in `get_next_job`, then `process_job` must be the one to remove.
    But `get_next_job` is "get **next**".

    Let's try another interpretation.
    `JobQueue` maintains a `list` of `Job` objects.
    `get_next_job` pops the highest priority.
    `process_job` is intended to be used on a `job_id` that *exists* in the queue.
    If `get_next_job` popped it, it's gone.
    Unless... `process_job` is meant to work on the `Job` objects *before* they are popped?

    Wait! "orchestrates Jobs using RetryPolicy".
    Maybe `process_job` doesn't remove anything?
    Maybe `process_job` is called, it performs the work, and `get_next_job` is just a helper to see what's next?

    Actually, I'll go with the most standard Queue behavior:
    `get_next_job` removes the highest priority item and returns its ID and data.
    If `process_job` is called with an ID that was *already* popped by `get_next_job`, it would fail.
    Therefore, `process_job` must be called *before* `get_next_job` or `get_next_job` must not remove the item.

    However, "Queue" implies removal.
    Let's look at the signature of `process_job` again: `process_job(self, job_id: str, processor: Callable) -> bool`.
    If I call `add_job`, then `process_job`, then `get_next_job`.
    If `process_job` removes the job, then `get_next_job` will see the next one.
    This makes sense.

    So:
    `add_job`: Add to heap and dictionary.
    `process_job`: Find in dictionary, run with `RetryPolicy`, remove from heap and dictionary.
    `get_next_job`: Pop from heap, remove from dictionary, return.

    Wait, if `process_job` removes it, `get_next_job` won't see it. That's correct.
    But if `get_next_job` removes it, `process_job` won't find it.
    So the user must choose to call one or the other.

    Let's refine the implementation:
    `Job`: dataclass.
    `RetryPolicy`: `run` method.
    `JobQueue`:
        `self.jobs = {}` (id -> Job)
        `self.heap = []` (priority queue)
        `self.counter = 0`
        `add_job`: put in both.
        `process_job`: get from `self.jobs`, run, remove from both.
        `get_next_job`: pop from `self.heap`, remove from `self.jobs`, return.

    Wait, `get_next_job` returns `tuple[str, dict] | None`.
    If `self.heap` is empty, return `None`.

    One small detail: `Job` has a `retries` field. `RetryPolicy` doesn't seem to update the `Job` object's `retries` field, it just returns the count of attempts made. The prompt doesn't say to update the `Job` object, but `Job` has a `retries` field, so maybe it should?
    "calls `fn(data)`, retries on any exception ... returns `(success, attempts_made)`".
    The `Job` dataclass has `retries`. I'll keep it in the dataclass but since `RetryPolicy` doesn't take a `Job` object (it takes `data`), I'll just leave the `retries` field as is unless I can easily update it.
    Actually, `RetryPolicy.run` takes `data`. It doesn't take the `Job` object. So it can't update the `retries` field of the `Job` object.

    Implementation details:
    `heapq` is a min-heap.
    For `highest-priority`, if priority 10 > priority 5:
    Store as `-priority`.
    For FIFO: store as `counter`.
    Heap element: `(-priority, counter, job_id)`.

    One more thing: `RetryPolicy.run`
    "retries on any exception until success or max_attempts reached".
    "Do NOT actually sleep".
    Backoff? The prompt says "retry-with-backoff" in the description but then says "Do NOT actually sleep". This usually means I don't need to implement the exponential math, just the loop logic. But wait, if I don't sleep, how is it backoff? Usually, it means the *timing* of the retries is ignored, but the *logic* of retry is kept. I will just implement the retry loop.

    Wait, "retry-with-backoff" ... "Do NOT actually sleep".
    I'll just do the loop.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Tuple, Optional, Dict, List
    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:
                try:
                    fn(data)
                    return True, attempts + 1
                except Exception:
                    attempts += 1
            return False, attempts

    class JobQueue:
        def __init__(self):
            self.jobs: Dict[str, Job] = {}
            self.heap: List[Tuple[int, int, str]] = []
            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, _ = self.retry_policy.run(processor, job.data)
            if success:
                # Remove if successful? The prompt doesn't specify.
                # But usually processing completes a job.
                # However, if it fails, does it stay? 
                # Let's assume it's removed regardless or only on success?
                # Let's keep it simple: only remove if successful? 
                # No, usually process_job implies it's done.
                # I'll remove it if it was attempted.
                pass
            
            # Let's look at the prompt again. 
            # "orchestrates Jobs using RetryPolicy"
            # "process_job ... must USE RetryPolicy"
            # I'll just return the success boolean.
            return success

        def get_next_job(self) -> Optional[Tuple[str, dict]] :
            if not self.heap:
                return None
            _, _, job_id = heapq.heappop(self.heap)
            job = self.jobs.pop(job_id)
            return (job.id, job.data)
    ```
    Actually, if `process_job` removes it from the dictionary/heap, then `get_next_job` works correctly as a "next" queue.
    If `process_job` doesn't remove it, `get_next_job` will eventually return it.
    If `get_next_job` removes it, then `process_job` can't be called on it.

    Actually, the most logical flow for a "Job Queue" class is:
    1. `add_job` puts it in.
    2. `get_next_job` takes the best one out.
    3. The user then calls `process_job`? But `process_job` takes a `job_id`. If it's already popped, `process_job` can't find it.

    Wait, the only way `process_job(job_id)` works after `get_next_job()` is if `get_next_job` does NOT remove it.
    But if `get_next_job` doesn't remove it, then `get_next_job` isn't a queue, it's a "peek" or "find best".
    But "get next" usually implies "pop".

    Let's look at the requirements again.
    `process_job(self, job_id: str, ...)`.
    If I assume `process_job` is the one that removes it, then the user flow is:
    `add_job` -> `process_job` -> `get_next_job`.
    This works. `process_job` is "Process this specific job (and remove it)". `get_next_job` is "Give me the next available job (and remove it)".

    One more thing: `RetryPolicy` says `max_attempts`.
    If `max_attempts` is 4, and it fails 4 times, `attempts_made` is 4.
    My loop:
    `attempts = 0`
    `while attempts < 4`:
    `try: ... return True, 1`
    `except: attempts += 1`
    After 4 fails: `attempts` is 4. Loop ends. Returns `False, 4`.
    Correct.

    Final check on `JobQueue.get_next_job`:
    "return the highest-priority job's (id, data); FIFO order within the same priority."
    My heap: `(-priority, counter, job_id)`.
    - Higher priority (e.g. 10) becomes -10.
    - Smaller number comes first in min-heap.
    - So -10 comes before -5. Correct.
    - `counter` ensures FIFO. Correct.

    Wait, `process_job` signature: `process_job(self, job_id: str, processor: Callable) -> bool`.
    If I remove the job from `self.jobs` inside `process_job`, it works perfectly.

    Wait, one more check: `RetryPolicy.run(self, fn: Callable, data)`.
    The prompt says `data`, not `data: dict` in the signature description, but `Job.data` is `dict`. I'll use `data: dict`.

    Wait, `Job` dataclass:
    `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    I should ensure `retries` is handled? `RetryPolicy` returns `attempts_made`. I can update `job.retries`. But `RetryPolicy.run` doesn't take the `job` object.
    However, `process_job` *does* have access to the `job` object. I can update `job.retries` there.
    But the prompt doesn't explicitly say to update it, only that `RetryPolicy` should return the count.

    Actually, the instructions say:
    `run(self, fn: Callable, data) -> tuple[bool, int]`
    It doesn't say `RetryPolicy` is responsible for updating the `Job` object. It just says it returns the tuple.

    Final structure:
    ```python
    from dataclasses import dataclass
    from typing import Callable, Tuple, Optional, Dict, List
    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:
                try:
                    fn(data)
                    return True, attempts + 1
                except Exception:
                    attempts += 1
            return False, attempts

    class JobQueue:
        def __init__(self):
            self.jobs: Dict[str, Job] = {}
            self.heap: List[Tuple[int, int, str]] = []
            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)
            job.retries = attempts
            # Remove from heap and dict only if we want it "processed"
            # Since it's a queue, processing usually removes it.
            # But since get_next_job also removes it, 
            # I will remove it only if successful? Or always?
            # If I don't remove it, then get_next_job will find it.
            # If I do remove it, then get_next_job won't find it.
            # To make both methods useful, I'll remove it regardless of success.
            
            # To remove from heap, I'd have to find the index. O(N).
            # Better to just remove from dict and let the heap pop it later?
            # No, then get_next_job might return a job that was already processed.
            
            # Let's reconsider: maybe process_job doesn't remove it?
            # "orchestrates Jobs".
            # Actually, usually in these tasks, process_job is for specific IDs.
            # I'll remove it from the dictionary.
            del self.jobs[job_id]
            # To remove from heap efficiently, I can't easily. 
            # I'll use a "removed" set.
            return success
    ```
    Wait, `heapq` doesn't support easy removal.
    Maybe I shouldn't remove it in `process_job` and instead `get_next_job` is the only one that removes?
    But then `process_job` can be called on any job.
    Let's look at the requirements again. "orchestrates Jobs".
    If `get_next_job` pops it, it's gone.
    If `process_job` is called, it's processed.
    This implies `process_job` and `get_next_job` are two different ways to interact with the queue.
    One is "Pop the best and let me know", the other is "Process this specific one".
    If `process_job` is called, it should probably be removed from the "available" pool.

    Let's simplify. I'll keep the dictionary and the heap.
    `add_job`: Add to both.
    `process_job`: Find in dict, run, remove from dict. (Heap will still have it, but `get_next_job` will check if it's still in dict).
    `get_next_job`: Pop from heap, check if still in dict. If not (already processed), pop again.

    This is a standard way to handle removals in a heap.

    Wait, one more thing: `Job` dataclass `retries`.
    `RetryPolicy.run` returns `attempts_made`.
    I should probably update `job.retries = attempts_made` in `process_job`.

    One more detail: `tuple[bool, int]` -> `Tuple[bool, int]` for Python < 3.9?
    The prompt says `tuple[bool, int]`. I'll use the modern syntax or standard `Tuple`. I'll stick to `Tuple` to be safe or just use `tuple` if it's Python 3.9+. Actually, the prompt uses `tuple[bool, int]`, so I'll use that.

    Wait, the prompt says `tuple[str, dict] | None`. This is Python 3.10+ syntax. I'll use that.

    One final check on `RetryPolicy.run`:
    "Do NOT actually sleep".
    If I don't sleep, what is "backoff"?
    Usually, backoff implies `time.sleep(2**attempts)`.
    If I'm told "Do NOT actually sleep", it means I should just skip the `sleep` call.
    The logic of "retry until success or max_attempts" is the core part.

    Wait, `Job` dataclass: `priority: int = 0`.
    In `JobQueue.add_job`, the default is 0.
    `JobQueue.get_next_job`: "highest-priority job".
    If priority 10 is higher than 0, then `(-10, ...)` is smaller than `(0, ...)`.
    So `heapq.heappop` will return -10 first. Correct.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Tuple, Optional, Dict, List
    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:
                try:
                    fn(data)
                    return True, attempts + 1
                except Exception:
                    attempts += 1
            return False, attempts

    class JobQueue:
        def __init__(self):
            self.jobs: Dict[str, Job] = {}
            self.heap: List[tuple[int, int, str]] = []
            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)
            job.retries = attempts
            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.pop(job_id)
                    return (job.id, job.data)
            return None
    ```

    Wait, `Job` dataclass `retries`.
    If `process_job` removes it from `self.jobs`, `get_next_job` won't see it. Correct.
    If `get_next_job` pops it, `process_job` won't see it. Correct.
    The `while` loop in `get_next_job` handles the case where a job was removed via `process_job`.

    Double check: `tuple` vs `Tuple`. Python 3.9+ allows `tuple[...]`.
    The prompt uses `tuple[...]`, so I will use `tuple`.
</reasoning_content>

<content>
```python
from dataclasses import dataclass
from typing import Callable, Dict, List
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:
            try:
                fn(data)
                return True, attempts + 1
            except Exception:
                attempts += 1
        return False, attempts

class JobQueue:
    def __init__(self):
        self.jobs: Dict[str, Job] = {}
        self.heap: List[tuple[int, int, str]] = []
        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 negative priority for max-heap behavior with heapq (min-heap)
        # Use counter to ensure FIFO order for equal priorities
        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_made = self.retry_policy.run(processor, job.data)
        job.retries = attempts_made
        
        # Remove from registry as it has been processed/attempted
        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.pop(job_id)
                return (job.id, job.data)
        return None
```
</content>