Let supersede() reissue an expired approval given a fresh window
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
This commit is contained in:
parent
71ae259877
commit
c93628a017
6 changed files with 294 additions and 7 deletions
103
workplans/APPROVAL-WP-0003-reissue-expired-approvals.md
Normal file
103
workplans/APPROVAL-WP-0003-reissue-expired-approvals.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
---
|
||||
id: APPROVAL-WP-0003
|
||||
type: workplan
|
||||
title: "Reissue: link a successor to an expired approval via supersede"
|
||||
domain: infotech
|
||||
repo: approval-engine
|
||||
status: finished
|
||||
flavor: implementation
|
||||
owner: claude
|
||||
topic_slug: netkingdom
|
||||
created: "2026-09-27"
|
||||
updated: "2026-09-27"
|
||||
origin: residual
|
||||
origin_ref: 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
|
||||
|
||||
```task
|
||||
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`/`approved` with no `validity`,
|
||||
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 `expired` **with** a `validity` dict (`not_before` optional,
|
||||
`expires_at` required, same validation as `create()`), it links a successor
|
||||
that carries the *new* window instead of the stale one. Without `validity`,
|
||||
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 bare `expired`) and
|
||||
`superseded_by` points at the successor — queryable forever, never deleted.
|
||||
Every other terminal status (`revoked`, `consumed`, `superseded` itself)
|
||||
still refuses `supersede` outright, `validity` or not: only `expired` gets
|
||||
this relaxation.
|
||||
- The compare-and-swap on the `UPDATE` now matches the exact status just read
|
||||
(`AND status=?` bound to the read row) instead of a hardcoded
|
||||
`IN ('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_digest`
|
||||
per admitted record; a reissued successor is a new `approval_id` and 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 `closed` and renders the raw `engine_status` text
|
||||
(`informed_decision/review.py:193`, `ui.py:151-152`). Once an originator
|
||||
reissues, the memo's approval flips from `expired` to `superseded` and 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 resolve `superseded_by` to a memo id, which
|
||||
isn't a 1:1 mapping today (a fresh memo version is cut for the new
|
||||
`approval_id` by whoever originates it, same as the existing T03-renewal
|
||||
pattern in `informed-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 `supersede` with a fresh
|
||||
window in one step (today it's a direct HTTP call any authenticated
|
||||
`approval:supersede` caller 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue