<reasoning_content>
*   Target: Refactor a job queue into three composable classes in `solution.py`.
    *   Requirements:
        1.  `Job` (dataclass): `id`, `data`, `priority`, `retries`.
        2.  `RetryPolicy`: `max_attempts`, `run(fn, data)` method. Handles retries/backoff (no sleeping). Returns `(success, attempts_made)`.
        3.  `JobQueue`: `add_job`, `process_job` (using `RetryPolicy`), `get_next_job` (priority-based, FIFO for same priority).
    *   Constraints:
        *   Use standard library only.
        *   No explanations, only code.
        *   `JobQueue` must compose `RetryPolicy`.

    *   `dataclasses`
    *   `typing` (`Callable`, `Tuple`, `Dict`, `Optional`)
    *   `heapq` (for priority queue management).

    *   *Class 1: `Job`*
        ```python
        @dataclass(order=False) # Need custom logic for priority
        class Job:
            id: str
            data: dict
            priority: int = 0
            retries: int = 0
        ```
        *Correction*: For `heapq` to work well with priority, usually we want highest priority first (or lowest depending on definition). Usually, "highest priority" means higher number or lower number? Standard priority queues usually treat lowest number as highest priority (like min-heap), but the prompt says "highest-priority", usually implying a larger integer if not specified, or I should ensure the sorting logic is correct. Let's use a tuple in the heap: `(-priority, counter, job)`.

    *   *Class 2: `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
        ```

    *   *Class 3: `JobQueue`*
        ```python
        class JobQueue:
            def __init__(self):
                self.jobs = [] # Heap
                self.counter = 0 # For FIFO tie-breaking
                self.retry_policy = RetryPolicy()

            def add_job(self, job_id: str, data: dict, priority: int = 0):
                job = Job(id=job_id, data=data, priority=priority)
                # heapq is a min-heap. To get "highest priority" (largest number),
                # use negative priority.
                heapq.heappush(self.jobs, (-priority, self.counter, job))
                self.counter += 1

            def process_job(self, job_id: str, processor: Callable) -> bool:
                # Find the job? Actually the prompt implies identifying it by ID.
                # But get_next_job returns the one to process.
                # Usually, process_job finds a specific job by ID.
                # Wait, the prompt says `process_job(self, job_id: str, ...)`.
                # I need to find the job in the list.
                for i, (neg_p, count, job) in enumerate(self.jobs):
                    if job.id == job_id:
                        success, _ = self.retry_policy.run(processor, job.data)
                        # Should I remove it? Prompt doesn't say.
                        return success
                return False

            def get_next_job(self) -> tuple[str, dict] | None:
                if not self.jobs:
                    return None
                neg_p, count, job = heapq.heappop(self.jobs)
                return job.id, job.data
        ```

    *   *Wait, look at `process_job` again.*
        `process_job(self, job_id: str, processor: Callable) -> bool`
        Does it remove it? Does it find it in the internal structure?
        If I use a heap, searching for an ID is $O(N)$.
        However, usually in these prompts, `process_job` is intended to be called on the job currently being handled or something similar.
        Let's check the flow: `get_next_job` pops it. `process_job` takes an ID.
        If `get_next_job` pops it, it's no longer in the queue.
        If I pop it to get it, then `process_job` won't find it in the heap unless I put it back or handle the storage differently.

        Actually, let's look at the requirement:
        `get_next_job` returns `(id, data)`.
        `process_job` takes `job_id`.
        This suggests `JobQueue` might store jobs in a dictionary for ID lookup but a heap for priority? Or maybe `process_job` doesn't remove it?
        Or, maybe `process_job` is meant to be called on the job *returned* by `get_next_job`? But the signature is `job_id: str`.

        Let's think:
        1. User calls `add_job`.
        2. User calls `get_next_job` -> returns `id`.
        3. User calls `process_job(id, ...)`.

        If `get_next_job` pops it, it's gone from the heap.
        If `process_job` is called with that ID, it won't be in the heap.
        Conclusion: `JobQueue` must store jobs in a way that allows both priority ordering and ID lookup. A dictionary `jobs_by_id` and a heap `heap` is standard.

    *   *Revised `JobQueue` Storage:*
        ```python
        self.queue = [] # heap: (-priority, count, job_id))
        self.jobs = {} # id -> Job object
        ```
        Wait, if I pop from heap, I still have the object in `self.jobs`.
        But `get_next_job` says "return the highest-priority job". If it's popped, it's "next". If it's processed, is it removed from `self.jobs`?

        Let's re-read: "return the highest-priority job's (id, data)".
        If I call `get_next_job` twice, I should get different jobs. So it must pop.
        If `process_job` is called, it uses the `RetryPolicy`.

        Let's refine `process_job`:
        It takes `job_id`. It finds the job object. It uses `RetryPolicy.run`.
        Does it remove it? If `get_next_job` already popped it, it's already "out" of the queue.

        Actually, if `get_next_job` removes it, and `process_job` is called afterwards, `process_job` doesn't need to "find" it in the queue, it just needs to access the data.

        Let's look at the signature of `process_job` again: `process_job(self, job_id: str, processor: Callable) -> bool`.
        If `get_next_job` already popped it, the `JobQueue` instance still needs to know what that `job_id` refers to, or the user is responsible for the data.
        But the class `JobQueue` owns the `RetryPolicy`.
        So `JobQueue` needs to keep a record of jobs even after they are "popped" until they are "processed"? Or maybe `get_next_job` doesn't pop?
        "return the highest-priority job". Usually, a "queue" `get_next` implies popping. If it doesn't pop, it's a "peek". But it says "Queue".

        Let's assume:
        1. `add_job`: Adds to a dict and a heap.
        2. `get_next_job`: Pops from heap. To do this, it needs the `job_id`. It returns `(id, data)`.
        3. `process_job`: Takes `job_id`. It looks up the job in the dict (which still exists because we only popped from the heap). Once processed, we should probably remove it from the dict.

    *   *Wait, simpler approach:*
        Maybe `process_job` is supposed to find the job in the queue, and `get_next_job` is just a peek? No, "Queue" usually means pop.
        Let's look at the `RetryPolicy` requirement: `run(self, fn: Callable, data)`. It takes `data`.
        So `process_job` needs to find the `data` associated with `job_id`.

        Implementation Plan:
        `self.jobs`: Dictionary `{job_id: Job}`
        `self.heap`: List of `(-priority, counter, job_id))`
        `add_job`: Add to both.
        `get_next_job`: Pop from heap, return `(id, data)`.
        `process_job`: Find in `self.jobs`, run policy, then `del self.jobs[job_id]`.

    *   *One small detail*: If `get_next_job` pops it, it's no longer in the heap. If the user then calls `process_job`, it's still in `self.jobs`. If `process_job` fails, what happens? The prompt doesn't say. I'll just return the bool.

    *   *Refining `RetryPolicy`*:
        `run(self, fn: Callable, data) -> tuple[bool, int]`
        `attempts_made` = total count of tries.

    ```python
    from dataclasses import dataclass
    from typing import Callable, Dict, Tuple, Optional
    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)
            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 it was successfully processed (or reached max retries), 
            # we remove it from the tracking dict.
            # Actually, the prompt doesn't specify if process_job removes it.
            # But usually, once processed, it's done.
            # However, if process_job is called on a job already popped 
            # by get_next_job, we need to keep it in self.jobs.
            # To be safe and clean: only remove if success is True? 
            # Or always? Let's just do the processing.
            
            # Re-reading: "orchestrates Jobs". 
            # If I pop in get_next_job, it's "taken".
            # If I process it, it's "done".
            
            # Let's check if process_job should remove it.
            # If I don't remove it, and the user calls process_job again, 
            # it will run again.
            
            if success:
                self.jobs.pop(job_id, None)
            return success

        def get_next_job(self) -> Tuple[str, dict] | None:
            if not self.heap:
                return None
            neg_p, count, job_id = heapq.heappop(self.heap)
            job = self.jobs.get(job_id)
            if not job:
                return None
            return job.id, job.data
    ```

    *Correction on `process_job`*: 
    If `get_next_job` pops it from the heap, it's "out". 
    If `process_job` is then called, and `success` is false, should it go back?
    The prompt doesn't specify logic for "re-queuing".
    I will stick to the instructions: `process_job` must USE `RetryPolicy`.

    *Wait*, there's a simpler way to implement `JobQueue` if I don't strictly need a dictionary.
    If `get_next_job` returns the `Job` object or its data, but `process_job` only takes `job_id`, I *must* have a way to map `job_id` to `data`.
    So the `self.jobs` dict is necessary.

    *Wait*, if I `pop` from heap in `get_next_job`, and `process_job` fails, should it go back? 
    Most job queues work like:
    1. Get job (moves from Queue to "Active").
    2. Process job.
    3. If failed, move to "Retry" or back to "Queue".
    
    Since the prompt doesn't specify re-queueing logic, I won't add it. I'll just remove it from the internal storage once `process_job` is called, or keep it until it's processed. 
    
    Actually, `get_next_job` says "return the highest-priority job's (id, data)".
    If it's popped, it's gone from the heap. 
    If `process_job` is called, it finds it in the dict.
    
    Let's check the `RetryPolicy` return: `(success, attempts_made)`.
    
    Final Structure:
    - `Job` dataclass.
    - `RetryPolicy` with `run`.
    - `JobQueue` with `add_job`, `process_job`, `get_next_job`.

    *One detail*: `priority` - "highest-priority". 
    If I have priority 10 and priority 1, is 10 higher? Usually yes.
    `heapq` is a min-heap. So `-priority` ensures 10 comes before 1.

    One more check on `RetryPolicy.run`: "Do NOT actually sleep."
    OK.

    Wait, `process_job` signature: `process_job(self, job_id: str, processor: Callable) -> bool`.
    If I `pop` in `get_next_job`, and then the user calls `process_job`, and it fails, the job is lost (not in heap, and if I remove it from dict, it's gone). 
    If I *don't* remove it from `self.jobs` until `process_job` is called, but `get_next_job` already popped it from the heap...
    
    Maybe `get_next_job` shouldn't pop?
    "return the highest-priority job's (id, data)".
    In many APIs, `get_next` without `pop` is `peek`. But it's a `JobQueue`. 
    If I don't pop, `get_next_job` always returns the same thing. That's a Peek.
    If I do pop, the `JobQueue` is behaving like a queue.
    
    Let's assume it's a Queue (pop). I'll keep the jobs in `self.jobs` dict until `process_job` is successful. Or just leave them there?
    Actually, let's just keep the dict updated. `add` adds to both. `get_next_job` pops from heap. `process_job` removes from dict.

    *Wait*, what if `get_next_job` is called, the user gets an ID, but then doesn't call `process_job`? The job is stuck in the dict.
    That's fine, it's a queue.

    Let's refine `process_job` to be simple:
    1. Find job in `self.jobs`.
    2. Run `retry_policy.run`.
    3. If success, delete from `self.jobs`.
    4. Return success.

    Actually, if I delete from `self.jobs` only on success, and it fails, the user could call `process_job` again with the same ID. That seems like reasonable behavior for a `process_job` method.

    Wait, I should check the `RetryPolicy` logic one more time.
    `run` returns `(success, attempts_made)`.
    `attempts_made` starts at 0. 
    First try: `attempts` is 0. `fn` succeeds. returns `True, 1`.
     First try: `fn` fails. `attempts` becomes 1.
     Second try: `fn` succeeds. returns `True, 2`.
     ...
     Max attempts reached (e.g. 4):
     Try 1 fails (attempts=1)
     Try 2 fails (attempts=2)
     Try 3 fails (attempts=3)
     Try 4 fails (attempts=4). Loop ends. Returns `False, 4`.
    
    This matches requirements.

    One small detail: `priority` order.
     If `priority` is 0, and I use `-priority`, it's 0.
     If `priority` is 10, it's -10.
     Min-heap puts -10 before 0.
     Correct.

    One more check: `FIFO order within the same priority`.
     `heapq` sorts by the first element of the tuple, then the second.
     So `(-priority, self.counter, job_id)` handles this perfectly.

    Final Code Structure:
    ```python
    from dataclasses import dataclass
    from typing import Callable, Dict, Tuple, Optional
    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:
            job = self.jobs.get(job_id)
            if not job:
                return False
            
            success, _ = self.retry_policy.run(processor, job.data)
            if success:
                self.jobs.pop(job_id, None)
            return success

        def get_next_job(self) -> Tuple[str, dict] | None:
            if not self.heap:
                return None
            neg_p, count, job_id = heapq.heappop(self.heap)
            job = self.jobs.get(job_id)
            if not job:
                return None
            return job.id, job.data
    ```

    Double check requirements:
    - `Job` dataclass: yes.
    - `RetryPolicy`: `__init__`, `run` (no sleep), returns `(success, attempts_made)`: yes.
    - `JobQueue`: `add_job`, `process_job` (use `RetryPolicy`), `get_next_job` (priority + FIFO): yes.
    - Only standard library: yes.
    - `solution.py` name: prompt asks for solution.py, but I just provide code.

    One detail: `Tuple` vs `tuple`.
    The prompt uses `tuple[bool, int]`. This is Python 3.9+ syntax. I should use that.
    Actually, I'll use `tuple` and `dict` as requested in the prompt's type hints.
    Wait, the prompt's type hints: `tuple[bool, int]`, `tuple[str, dict] | None`.
    These are Python 3.10+ types.
    I will use `from __future__ import annotations` or just stick to the syntax provided in the prompt.

    Wait, the prompt uses `dict`, `tuple`, `list`, `str` as types.
    I will use the `tuple[bool, int]` style.

    One small thing: `retries` in `Job` dataclass.
    The prompt says: `Job` dataclass with fields `id: str`, `data: dict`, `priority: int = 0`, `retries: int = 0`.
    I will include it even if not explicitly used in the logic, as requested by the definition.

    Final check on `get_next_job` return type: `tuple[str, dict] | None`.
    If heap is empty, return `None`.

    Final check on `process_job` behavior:
    If a job is popped from heap by `get_next_job`, it is no longer in `self.heap`.
    If `process_job` is called, it looks in `self.jobs`.
    If it's successful, it removes from `self.jobs`.
    If it fails, it stays in `self.jobs` (but not in `self.heap`).
    Is this okay? If it fails, the user could call `process_job` again? Yes.
    If it fails, it's not in the heap, so `get_next_job` won't see it again.
    This matches the behavior of a queue where you "take" a job. If you fail to process it, it's out of the queue (unless you manually re-add it). This is a standard pattern.

    Actually, I'll make sure `process_job` doesn't strictly depend on the job being in the heap. It just needs to be in the `self.jobs` storage.

    One final check: `dataclasses.dataclass` is used. `Callable` is used. `heapq` is used. `typing` is used for `Callable`.

    One last detail: the prompt asks for `tuple[bool, int]` and `tuple[str, dict] | None`. These are valid in Python 3.10+. I'll assume the environment supports it.

    Wait, `from typing import Callable`.
    I'll also include `from dataclasses import dataclass`.

    One minor thing: `RetryPolicy.run` signature: `fn: Callable, data`.
    Is it `data: dict` or `data`? Prompt says `data`. I'll use `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:
                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:
            job = self.jobs.get(job_id)
            if not job:
                return False
            
            success, _ = self.retry_policy.run(processor, job.data)
            if success:
                self.jobs.pop(job_id, None)
            return success

        def get_next_job(self) -> tuple[str, dict] | None:
            if not self.heap:
                return None
            _, _, job_id = heapq.heappop(self.heap)
            job = self.jobs.get(job_id)
            if not job:
                return None
            return job.id, job.data
    ```
    The logic for `process_job` is fine. If it's not in `self.jobs` it returns `False`. If it is, it processes it. If success, it removes it from the dict.

    Wait, the prompt asks for:
    `run(self, fn: Callable, data) -> tuple[bool, int]`
    My code uses `data` without type hint inside `run`, which is fine.

    Actually, I'll add `from __future__ import annotations` to be safe with the `| None` syntax in older 3.x versions, but the prompt implies 3.10+.

    Final look at `JobQueue.process_job`:
    "must USE RetryPolicy (compose, don't reimplement retry logic)".
    My code uses `self.retry_policy.run(processor, job.data)`. This is composition.

    Final look at `JobQueue.get_next_job`:
    "return the highest-priority job's (id, data); FIFO order within the same priority."
    `-priority` ensures high numbers come first. `self._counter` ensures FIFO for same priority. `heapq.heappop` ensures we get the smallest (most negative) value first. Correct.

    Wait, what if `get_next_job` pops a `job_id` that was already removed from `self.jobs`?
    `job = self.jobs.get(job_id)` would be None.
    Then I return None.
    This is safe.

    One last check on `RetryPolicy.run` return.
     `(success, attempts_made)`.
     If it succeeds on the first try, `attempts` is 0, `attempts + 1` is 1.
     If it fails 4 times, `attempts` becomes 4. returns `False, 4`.
     Perfect.
</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) -> 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 to turn min-heap into max-heap behavior
        # Counter ensures FIFO order for identical priorities
        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:
            self.jobs.pop(job_id, None)
        return success

    def get_next_job(self) -> tuple[str, dict] | None:
        if not self.heap:
            return None
        
        _, _, job_id = heapq.heappop(self.heap)
        job = self.jobs.get(job_id)
        if not job:
            return None
        
        return job.id, job.data
```
</content>