Version. 0.2 · Date. 2026-08-08 · Author. Claude.ai (v0.1 drafting) / Claude Code (v0.2 amendment) / Marvin Percival (approving).
Supersedes. v0.1 at record 43841fe, which stands as a sibling.
Target. /Users/dunin7/loomworks, branch main. Surface only — Step 1 found no engine gap.
Baseline. Surface 0dd7dd9, engine ad83960.
What changed from v0.1. Step 1's read resolved a contradiction in v0.1's own text, and found the one-time-reveal guarantee to be stronger than v0.1 assumed. §2's halt condition did not trigger.
v0.1's §4 put "expiry" out of scope. The issuance endpoint's request model requires expires_at. Those cannot both hold — a form cannot omit a required field.
The intent, clarified (Operator, 2026-08-08): "§4 was meant to exclude expiry MANAGEMENT (rotation policy, renewal, notifications), not the required field on the issuance form."
So the form collects expires_at as a dated field with a sensible default the Operator can change. Out of scope remains: rotation policy, renewal flows, expiry-driven notification, and usage auditing.
Why collected rather than defaulted invisibly. A credential granting unmoderated write access should have its lifetime visible at the moment of issuing. The alternative — hardcoding a default in the surface — was rejected on its own terms: it buries a policy decision in a form where the Operator cannot see what they are agreeing to. Defaulting it server-side was also rejected: it would require an engine change on a CR whose target is the surface.
Stronger than v0.1 assumed. The guarantee does not depend on the screen behaving well:
IssuedContributionCredentialResponse extends the list model with claim_token and claim_url.ContributionCredentialResponse — what the list route returns — has neither field.There is nothing to re-display. The credential is unreachable after issuance because the response model that would carry it is not used anywhere else — a boundary in the type system rather than a rule the UI is trusted to follow.
Gate item 1 therefore changes shape. It is no longer "prove the surface shows it once"; it is "prove the surface does not reintroduce a path back" — including not caching the issuance response in any store, context, history entry, or query string where it could be re-read. That is what the test must demonstrate.
| | Route | Notes |
|---|---|---|
| Issue | POST /engagements/{eid}/contribution-credentials | 201; body recipient_label (optional) + expires_at (required) |
| List | GET /engagements/{eid}/contribution-credentials | never carries a token |
| Revoke | POST /engagements/{eid}/contribution-credentials/{cid}/revoke | exists — §2's halt did not trigger |
All three are Operator-only through _require_operator. No engine work is needed, so this CR is surface-only as v0.1 anticipated.
The list returns status, recipient_label, expires_at, created_at, claimed_by_person_id, claimed_at, revoked_at — enough to carry a revoke control and to show whether a credential has been claimed or already revoked.
v0.1's §3 (the reveal, the revoke, Operator authority), §4's remaining exclusions, and §6's other gate items are unchanged.
| Step | What | Mode | State |
|---|---|---|---|
| 0 | Pre-flight | Auto | done |
| 1 | Read per §2; halt if no revoke | Auto, conditional halt | done — no halt |
| 2 | One-time-reveal screen + tests | Auto | pending |
| 3 | Revoke control + confirmation + tests | Auto | pending |
| 4 | Test that no path back exists — reload, navigation, list, history, and no cached response | Auto | pending |
| 5 | Surface sweep | Auto | pending |
| A | Checkpoint | Checkpoint | pending |
| 6 | Tag cr-2026-188-b47-credential-issuance. Push. | Auto | pending |
DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-188 — B-47 credential issuance — v0.2 — 2026-08-08