> For the complete documentation index, see [llms.txt](https://help.resouris.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://help.resouris.com/job-planning/plan-history-and-wte-temporal.md).

# Plan history and WTE over time

**Landed:** 2026-08-02. **What's new:** Job plans now retain a linked chain of every revision (via `supersedes_id`), staff WTE is tracked per-spell over time (via `staff_wte_spell`), and signed quarterly reconciliations pin the WTE-at-signing so numbers can't drift after signing.

**Scope frame:** this page covers the backend and JSON API. ✅ **The admin UI has since shipped** — see [Plan history — the UI](/job-planning/hcs-history-ui.md) for the screens, including the WTE-history view at `/ui/admin/staff/{staff_id}/wte-history`.

## Why this exists

Two silent correctness bugs in the pre-P44 HCS Job Planning surface:

1. **Retroactive WTE changes distorted history.** `staff.wte_fraction` was a single value column — when a worker went from 1.0 → 0.6 mid-year, every KPI recompute silently overwrote the historical picture. Signed reconciliations for Trust finance / CQC became audit-of-record surfaces whose underlying numbers changed after signing.
2. **Plan supersession without linkage.** Creating a new plan for a worker who already had an active plan produced two coexisting plans. Reconciliation reads whichever it happens to pick, producing inconsistent numbers.

Both bugs are silent — no error fires, the numbers just drift. Cohort C readers + Cohort D JSON APIs expose *what actually happened over time*.

## The supersedes chain

Every plan revision points at its predecessor via `job_plan.supersedes_id`. The chain forms a linked list from the newest revision back to the origin draft. `job_plan.supersede_plan` writes the new plan + transitions the old plan to `state='superseded'` atomically in one transaction so the two states can never diverge.

The audit trail carries a `reason` from a 5-value closed set:

* `role_change` — worker moved role.
* `wte_change` — worker moved WTE at a point where the plan reflected the old WTE.
* `department_transfer` — worker moved department mid-cycle.
* `cycle_close` — cycle closed and the plan needs a fresh cycle\_id linkage.
* `manual_correction` — operator authored a manual correction, not driven by any of the above.

Every supersede emits a `job_plan.superseded` audit event with `plan_id, superseded_by_plan_id, superseded_utc, reason, staff_id`. Consumers can filter or aggregate by reason downstream (e.g. "which supersedes were driven by WTE changes this quarter?").

## The WTE spell timeline

`staff_wte_spell` is a temporal table with one open spell per worker (guaranteed by a partial UNIQUE on `(tenant_id, staff_id) WHERE effective_until IS NULL`). Each row carries:

* `effective_from` (date, required) — when the WTE became effective.
* `effective_until` (date, nullable) — when it ended. `NULL` for the currently-open spell.
* `wte_fraction` (numeric(3,2), 0 < wte ≤ 1.00).
* `reason` (5-value closed set): `initial`, `wte_change`, `promotion`, `return_from_leave`, `correction`.
* `notes` (≤ 500 chars).
* `created_by_user_id` + `created_utc` for audit.

The backfill inserts one open spell per existing staff at the current `staff.wte_fraction` value with `reason='initial'` and a notes marker so consumers can distinguish "we know this was the historical WTE" from "this is just the current value we backfilled at MIG-213".

`wte_at_date(staff_id, on_date)` reads the spell covering `on_date`. Fail-closed to `1.00` on pre-migration DBs, no matching spell, or transient error — so a lookup failure never silently under-pro-rates a legitimate PT plan.

## Reconciliation pinning

Signed quarterly reconciliations now carry a `staff_wte_snapshot` column. The reconciler writes the WTE-at-quarter-start value at compute time; once signed (`signed_utc IS NOT NULL`), the row is immutable at the DB layer alongside planned and delivered PA figures.

This means a retroactive WTE correction — say, someone belatedly notices a worker was actually 0.6 not 1.0 from January onward — will not silently change last quarter's signed reconciliation numbers. The historical audit-of-record stays honest.

## Cycle coverage

`GET /v1/admin/cycles/{cycle_id}/coverage` answers: "for this cycle, which active staff in the cycle's department don't yet have a plan linked to it?"

Each returned row also carries `hasPlanInOtherCycle: bool` — the signal to the operator that this worker already has a plan against a different cycle, so the right action is *supersede*, not *author a fresh plan*.

The response also carries `totalMissing` + `totalDepartmentStaff` so a coverage-% widget can render "N of M staff missing" directly without re-querying.

## JSON API surface

Three read-only endpoints, all gated on the existing `admin.job_plan_monitoring.read` permission:

| Endpoint                                     | What it returns                                                                                                                              |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /v1/admin/job-plans/{plan_id}/history`  | The supersedes chain for a plan, oldest-first, each entry a rich snapshot (state, WTE at active\_from, band grade at time, category totals). |
| `GET /v1/admin/staff/{staff_id}/wte-history` | The full `staff_wte_spell` list for a worker, newest-first. Open spell first.                                                                |
| `GET /v1/admin/cycles/{cycle_id}/coverage`   | Aggregate cycle-coverage snapshot: department + missing rows + total-department-staff denominator.                                           |

All three follow the same **empty-envelope contract**: unknown ID / access denied / pre-migration DB → the wrapper still returns with an empty list + zero counters, never 404. Consumers compare the echoed ID against the list to distinguish "missing" from "exists but empty".

Numeric values render as decimal-strings (`"0.60"` not `0.6`) with two-decimal-place pinning so a consumer can diff `wteFractionAtTime` across revisions without normalising `Decimal('0.6')` vs `Decimal('0.60')`.

## Migration + rollback posture

* **Additive-only MIGs.** MIG-213 creates `staff_wte_spell`; MIG-214 adds one column to the reconciliation snapshot. Both include backfills that idempotently re-apply.
* **`staff.wte_fraction` stays in place** as a "current value mirror" until every reader is temporal-aware. Drop is a v1.8+ concern.
* **Zero new permissions, zero new roles, zero new closed sets at manifest level.** The reason-code sets on MIG-213 + the audit event payload are subordinate CHECKs, not manifest-level closed sets.
* **Reverse is safe**: `DROP TABLE staff_wte_spell` and `ALTER TABLE job_plan_quarterly_reconciliation DROP COLUMN staff_wte_snapshot`. The MIG-204 immutability guard on signed reconciliations is preserved either way.

## Still not built

⚠️ Not scheduled, and listed here because knowing what is absent is as useful as knowing what is present. The admin UI that used to head this list has shipped — see [Plan history — the UI](/job-planning/hcs-history-ui.md).

* **Dropping `staff.wte_fraction`.** Kept as a current-value mirror while data-plane operators migrate.
* **Cross-cycle plan continuation UX.** Draft plan warns if it should reference a superseded predecessor.
* **Department-transfer flow** (staff moving mid-cycle).
* **Trainee-grade plans** under Guidelines §7.
* **Band history** (a `staff_band_spell` temporal table, which does not exist). `bandGradeAtTime` in the API response therefore reflects the CURRENT `staff.band_grade`, not the band held at the time asked about — worth knowing before quoting it historically.
* **KPI 16-20** additions for the history surface.
* **Working-day-aware appeal SLA** (still v1.8+ per P43.H2 close note).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://help.resouris.com/job-planning/plan-history-and-wte-temporal.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
