expired stays a terminal exit from valid (state machine unchanged), but supersede() now also accepts it as a source status when the caller supplies a validity window, linking a new successor approval to the expired original instead of requiring a caller to mint an unlinked replacement from scratch. Without a fresh window, an expired source still conflicts exactly as before. The original's status becomes superseded (not left as a bare dead-end expired) and superseded_by points at the successor, so the link is documented rather than hidden -- informed-decision's overview already renders this automatically, no changes needed there. Prompted by the operator missing infd-20260927-b01's approval window during today's key-cape login outage (APPROVAL-WP-0003). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 169987@bnt-lap001 Assistant-Session: 322ef1ef-9048-4021-8570-b6d6f6347999
4.8 KiB
| id | type | title | domain | repo | status | flavor | owner | topic_slug | created | updated | origin | origin_ref |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| APPROVAL-WP-0003 | workplan | Reissue: link a successor to an expired approval via supersede | infotech | approval-engine | finished | implementation | claude | netkingdom | 2026-09-27 | 2026-09-27 | residual | informed-decision/INFD-IN-0005 |
APPROVAL-WP-0003 — Reissue expired approvals
Why
Operator (Bernd) missed approving infd-20260927-b01 (informed-decision) due
to the key-cape login bugs fixed same-day. Asked, as a follow-up: an easy way
for the approval's originator to bring back a decision that ran out of its
timeframe, with the reissue linked to the original so the original stays
documented rather than disappearing into a bare expired dead end.
expired is a deliberately terminal exit from valid
(SCOPE.md "In Scope") — the closed state machine does not reopen it in
place, and nothing here changes that. What this adds is a link: a fresh,
separately-approvable successor approval tied to the expired one, using the
supersession mechanism that already exists for exactly this kind of "retire
this, point at that" relationship (supersede, previously usable only from
requested/approved).
What changed
id: APPROVAL-WP-0003-T01
status: done
priority: high
Engine.supersede(approval_id, successor_id=None, validity=None)
(approval_engine/store.py):
- Unchanged behaviour: called from
requested/approvedwith novalidity, the successor inherits the original's window verbatim — the existing correcting-a-binding-while-still-open use case is untouched (test_ordinary_supersession_of_an_active_approval_still_copies_the_window). - New: called from
expiredwith avaliditydict (not_beforeoptional,expires_atrequired, same validation ascreate()), it links a successor that carries the new window instead of the stale one. Withoutvalidity, an expired source still conflicts exactly as before — a caller cannot silently mint a successor that inherits an already-past window, and cannot accidentally reissue-by-omission. - The original's status becomes
superseded(not left as bareexpired) andsuperseded_bypoints at the successor — queryable forever, never deleted. Every other terminal status (revoked,consumed,supersededitself) still refusessupersedeoutright,validityor not: onlyexpiredgets this relaxation. - The compare-and-swap on the
UPDATEnow matches the exact status just read (AND status=?bound to the read row) instead of a hardcodedIN ('requested','approved'), which is what let the same code path serve both cases without a second SQL statement.
approval_engine/api.py: POST /v1/approvals/{id}/supersede now passes a
validity field from the request body through to supersede, under the same
approval:supersede scope — no new endpoint or scope, since reissue is a
supersession from a different source status, not a new operation.
Tests: tests/test_reissue.py (7 cases: link+retire, missing validity
conflicts, expires_at/not_before ordering, other terminal statuses still
refuse even with validity, and the no-regression case above) and two
HTTP-level cases in tests/test_api.py. All confirmed to fail against the
prior code and pass after. Full suite: 169 passed (was 160).
What did not need to change
- flex-auth: unrelated — its policies pin
approval_id/binding_digestper admitted record; a reissued successor is a newapproval_idand needs its own admission when a memo is cut for it, same as any new approval. - informed-decision: its overview already groups any non-open engine
status as
closedand renders the rawengine_statustext (informed_decision/review.py:193,ui.py:151-152). Once an originator reissues, the memo's approval flips fromexpiredtosupersededand the overview reflects that automatically — no code change needed there. Linking the old memo to the new memo in the UI (so a reviewer can click through) is a separate, smaller follow-up if wanted, tracked as a residual below — informed-decision would need to resolvesuperseded_byto a memo id, which isn't a 1:1 mapping today (a fresh memo version is cut for the newapproval_idby whoever originates it, same as the existing T03-renewal pattern ininformed-decision/docs/evidence/2026-09-16-t03-renewed-review-handoff.json).
Residual
Not built here, deliberately out of scope for this pass:
- A convenience client/CLI for providers to call
supersedewith a fresh window in one step (today it's a direct HTTP call any authenticatedapproval:supersedecaller can already make — no new capability is missing, just no wrapper script). - informed-decision surfacing a "reissued as" link on the overview row pointing at the new memo.