<reasoning_content>
*   Goal: Refactor a job queue into three composable classes: `Job`, `RetryPolicy`, and `JobQueue`.
    *   Language: Python (standard library only).
    *   Output: Only the Python code, no explanations.

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

        *   Constructor: `__init__(self, max_attempts: int = 4)`.
        *   Method: `run(self, fn: Callable, data) -> tuple[bool, int]`.
        *   Behavior: Call `fn(data)`, retry on exception until success or `max_attempts` reached. No `time.sleep()`. Return `(success, attempts_made)`.

        *   Methods:
            *   `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`.
        *   Ordering for `get_next_job`: Highest priority first, then FIFO for same priority.

    *   `Job`: Simple dataclass.
    *   `RetryPolicy`: Needs a loop to catch exceptions and count attempts.
    *   `JobQueue`:
        *   Storage: A list or a heap? Since there's a priority and FIFO requirement, a sorted list or a priority queue is appropriate.
        *   `add_job`: Create a `Job` instance and store it.
        *   `process_job`: Find the job by ID. Use a `RetryPolicy` instance to run the processor.
        *   `get_next_job`: Return the top job and remove it.

    *   `Job` needs to be comparable for a heap if I use `heapq`, or I can just sort a list. Since `priority` is "highest-priority first", and `heapq` is a min-heap, I'll use negative priority or a custom sort.
    *   Wait, `get_next_job` should return the highest priority.
    *   FIFO for same priority.
    *   Let's use a list and sort it, or use a `heapq`. To handle FIFO with `heapq`, I need a counter to break ties.

    *   *Refined JobQueue Storage:*
        A list of `Job` objects.
        `add_job`: append and sort? Or just append and sort on `get_next_job`.
        Actually, to maintain FIFO for same priority easily, I can store a sequence number.

    *   *Requirement Check:*
        - `Job` dataclass: `id`, `data`, `priority`, `retries`.
        - `RetryPolicy` `run`: `(success, attempts_made)`.
        - `JobQueue` `process_job`: Must use `RetryPolicy`.

    *   *Refining `RetryPolicy.run`*:
        ```python
        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
        ```
        Wait, if `max_attempts` is 4, and it fails 4 times, it returns `False, 4`. If it succeeds on the first try, it returns `True, 1`.

    *   *Refining `JobQueue`*:
        Need a way to find a job by ID for `process_job`. A dictionary `{id: Job}` might be useful, but `get_next_job` needs to find the highest priority.
        Maybe a list of jobs and a dict of IDs?

        Let's look at the requirements again.
        `add_job`: takes `job_id`, `data`, `priority`.
        `process_job`: takes `job_id`. This implies the job must exist in the queue.
        `get_next_job`: returns the highest priority job (id, data).

    *   *Storage Strategy*:
        A list `self.jobs` stores `Job` objects.
        To handle FIFO + Priority:
        When adding: `self.jobs.append(Job(...))`.
        When getting next: Sort by `(-priority, sequence_number)`.

    *   Wait, `process_job` doesn't specify if the job is removed. Usually, `process_job` executes it. Does it remove it? The prompt doesn't say "remove", but usually processing a job in a queue implies it's done. However, `get_next_job` returns the "highest-priority job". Usually, `get_next_job` removes it.
    *   If `get_next_job` removes it, `process_job` might be called on a job that is "active".
    *   Let's re-read:
        - `add_job`: adds to queue.
        - `process_job`: uses `RetryPolicy` to run a processor on a specific `job_id`.
        - `get_next_job`: returns the highest priority job's (id, data).

    *   Let's use a list to store `Job` objects. To support FIFO for same priority, I'll add a `sequence` counter.

    *   *Correction on `process_job`*:
        The prompt says "must USE RetryPolicy (compose, don't reimplement retry logic)".
        This means `process_job` should instantiate (or use a member) `RetryPolicy` and call its `run` method.

    *   *Refining `Job` attributes*:
        The prompt says `retries` is a field in `Job`. Should `RetryPolicy` update the `retries` field? The `run` method returns `attempts_made`. Maybe I should update the `Job` object's `retries` field?
        Actually, the prompt doesn't explicitly say to update it, but `retries` exists in the dataclass.

    *   *Code structure*:
    ```python
    from dataclasses import dataclass
    from typing import Callable, Dict, Tuple, List, 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: List[Job] = []
            self.counter = 0

        def add_job(self, job_id: str, data: dict, priority: int = 0) -> None:
            # Use a list and sorting/ordering logic
            # To handle FIFO for same priority, we can't just sort by priority.
            # We need a sequence number or just trust the insertion order.
            # Since standard list append preserves order, if we sort stably, 
            # or if we include an entry index.
            job = Job(id=job_id, data=data, priority=priority)
            self.jobs.append(job)

        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
            
            policy = RetryPolicy()
            success, attempts = policy.run(Processor, job.data)
            job.retries = attempts # Update retries if needed
            return success

        def get_next_job(self) -> Optional[Tuple[str, dict]] :
            if not self.jobs:
                return None
            
            # Sort: priority descending, then insertion order (FIFO)
            # Since Python's sort is stable, we only need to sort by priority descending.
            self.jobs.sort(key=lambda x: x.priority, reverse=True)
            top_job = self.jobs.pop(0)
            return (top_job.id, top_job.data)
    ```

    Wait, if I sort inside `get_next_job` and then pop, and the list is only sorted by priority, the stable sort ensures that items with the same priority maintain their original order (FIFO). This is perfect.

    Wait, `retries` field in `Job`. Should it be incremented or set to the total attempts? The `RetryPolicy.run` returns `attempts_made`. I'll set `job.retries = attempts`.

    Let's double check `process_job`. "must USE RetryPolicy (compose, don't reimplement retry logic)".
    Yes, `policy.run(Processor, job.data)` does exactly that.

    One detail: `retries` in `Job` dataclass defaults to 0.
    One detail: `RetryPolicy.run` signature: `fn: Callable, data`. The prompt says `data`. I should probably use a type hint like `dict` or just `Any`. The prompt says `data: dict` in `Job`, so `data` in `run` is likely `dict`.

    Let's refine the `JobQueue.jobs` handling.
    If I use `self.jobs.sort(key=lambda x: x.priority, reverse=True)`, then `pop(0)` will get the highest priority. Because Python's `sort` is stable, jobs added earlier with the same priority will stay earlier in the list.

    Wait, `retries` field in `Job`. Does it mean "number of retries already failed" or "total attempts"? Usually, "retries" means how many times we tried *again*.
    If `max_attempts` is 4:
    Try 1 (Success) -> `attempts_made` = 1. `retries` = 1? Or 0?
    Actually, the prompt says "retries: int = 0".
    If it fails 3 times and succeeds on the 4th: `attempts_made` = 4.
    I will set `job.retries = attempts`.

    One more look at `RetryPolicy.run`: "calls `fn(data)`, retries on any exception until success or `max_attempts` reached".
    My loop:
    `attempts = 0`
    `while attempts < self.max_attempts:`
    `  try: fn(data); return True, attempts + 1`
    `  except: attempts += 1`
    `return False, attempts`
    Example: `max_attempts = 4`.
    Iter 1: fails. `attempts` becomes 1.
    Iter 2: fails. `attempts` becomes 2.
    Iter 3: fails. `attempts` becomes 3.
    Iter 4: fails. `attempts` becomes 4.
    Loop ends. Returns `(False, 4)`. Correct.
    Example: Success on first try.
    Iter 1: succeeds. Returns `(True, 1)`. Correct.

    Wait, the prompt says `retries` field in `Job` is `int = 0`.
    And `RetryPolicy` `max_attempts` is `int = 4`.
    The prompt doesn't specify how to handle the `retries` field in `Job` when `process_job` is called. I will assume it should be updated with the count of attempts made.

    - `Job`: dataclass, correct fields.
    - `RetryPolicy`: `__init__`, `run` (no sleep), `(success, attempts_made)`.
    - `JobQueue`: `add_job`, `process_job` (uses `RetryPolicy`), `get_next_job` (highest priority, FIFO).
    - Standard library only.
    - No explanations.

    One small detail: `get_next_job` return type `tuple[str, dict] | None`.
    I should use `Optional` or `|` (Python 3.10+). The prompt uses `|` style in the description (`tuple[str, dict] | None`), so I'll use `|` and `list[...]` type hints which are standard in 3.10+.

    One more thing: `process_job` needs to find the job by ID.
    If `get_next_job` removes the job from the list, `process_job` might be called on a job that isn't in the queue anymore?
    Actually, the instructions define them as separate methods. Usually, in these patterns, `process_job` is called on a job currently "in" the queue or just retrieved.
    If I use a list, `process_job` will look through `self.jobs`.

    Wait, `JobQueue.process_job` signature: `process_job(self, job_id: str, Processor: Callable) -> bool`.
    It doesn't say "remove".
    `JobQueue.get_next_job` signature: `get_next_job(self) -> tuple[str, dict] | None`.
    It doesn't explicitly say "remove", but "return the highest-priority job" usually implies removal in a queue context. If I don't remove it, `get_next_job` will always return the same job.
    Actually, the word "Queue" implies removal. I will remove it in `get_next_job`.

    Final check on `RetryPolicy.run`:
    `fn` is a `Callable`.
    `data` is `dict`.
    Returns `tuple[bool, int]`.

    Let's double check the priority logic.
    "return the highest-priority job's (id, data); FIFO order within the same priority."
    If `self.jobs` is a list.
    `add_job` appends to list.
    `get_next_job` sorts by `priority` descending (stable).
    `pop(0)` gets the highest priority and earliest added.
    Correct.

    Wait, if I sort every time `get_next_job` is called, it's $O(N \log N)$.
    If I keep it sorted, `add_job` is $O(N)$.
    Since the instructions don't specify performance, the simplest implementation is usually preferred.

    One detail: `retry` vs `attempts_made`.
    If a job succeeds on attempt 1, `attempts_made` is 1.
    If it fails all 4 times, `attempts_made` is 4.
    The `Job` dataclass has a `retries` field. I'll update `job.retries = attempts_made`.

    Wait, `process_job` signature: `process_job(self, job_id: str, Processor: Callable) -> bool`.
    I'll find the job by ID, and if found, run it.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Dict, Tuple, List, 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_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: List[Job] = []

        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) -> bool:
            job = next((j for j in self.jobs if j.id == jobid), None)
            # Oh, the param is job_id.
            # Wait, I should find it first.
            ...
    ```

    Let's refine `process_job` to find the job correctly.
    ```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 job is None:
            return False
        
        policy = RetryPolicy()
        success, attempts = policy.run(Processor, job.data)
        job.retries = attempts
        return success
    ```

    Wait, I should check if I need to import `Callable` or `tuple` or `dict` from `typing`.
    In modern Python (3.9+), `list`, `dict`, `tuple` are subscriptable.
    `Callable` is still in `typing`.

    The prompt says `tuple[bool, int]`. This is Python 3.9+ syntax.
    I will use `from typing import Callable`.

    One final check on `get_next_job`:
    "return the highest-priority job's (id, data)"
    If `self.jobs` is `[Job(id='a', priority=1), Job(id='b', priority=2)]`.
    Sorted descending: `[Job(id='b', priority=2), Job(id='a', priority=1)]`.
    Pop(0) -> `Job(id='b', priority=2)`.
    Correct.
    If `self.jobs` is `[Job(id='a', priority=2), Job(id='b', priority=2)]`.
    Sorted descending (stable): `[Job(id='a', priority=2), Job(id='b', priority=2)]`.
    Pop(0) -> `Job(id='a', priority=2)`.
    Correct (FIFO).

    One minor thing: `retries: int = 0`.
    Should `process_job` update `job.retries`?
    The prompt doesn't say. But the field exists. I'll update it just in case.

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

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

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

        def run(self, fn: Callable, data: dict) -> tuple[bool, int]:
            attempts_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: list[Job] = []

        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) -> bool:
            job = next((j for j in self.jobs if j.id == job_id), None)
            if job is None:
                return False
            
            policy = RetryPolicy()
            success, attempts = policy.run(Processor, job.data)
            job.retries = attempts
            return success

        def get_next_job(self) -> tuple[str, dict] | None:
            if not self.jobs:
                return None
            
            # Stable sort preserves FIFO for same priority
            self.jobs.sort(key=lambda x: x.priority, reverse=True)
            job = self.jobs.pop(0)
            return (job.id, job.data)

    ```
    Wait, `tuple[str, dict] | None` requires Python 3.10+.
    The prompt uses it, so I'll use it.

    Wait, `self.jobs.sort(key=lambda x: x.priority, reverse=True)`
    If `priority` is 10, 5, 10.
    Sorted reverse: 10 (first one), 10 (second one), 5.
    Correct.
</reasoning_content>

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

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

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

    def run(self, fn: Callable, data: dict) -> tuple[bool, int]:
        attempts_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: list[Job] = []

    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) -> bool:
        job = next((j for j in self.jobs if j.id == job_id), None)
        if job is None:
            return False
        
        policy = RetryPolicy()
        success, attempts = policy.run(Processor, job.data)
        job.retries = attempts
        return success

    def get_next_job(self) -> tuple[str, dict] | None:
        if not self.jobs:
            return None
        
        # Python's sort is stable, so sorting by priority descending 
        # maintains the FIFO order for jobs with the same priority.
        self.jobs.sort(key=lambda x: x.priority, reverse=True)
        job = self.jobs.pop(0)
        return (job.id, job.data)
```
</content>