informed-decision/docs/specs/ProductRequirementsDocument.md
tegwick 5c33d17330 Apply GH-DEC-2026-013 and GH-DEC-2026-014; close INFD-IN-0002
Two rulings landed and both corrected something.

GH-DEC-2026-013 accepted our binding-versus-awareness argument, wrote it into
the record as its §6, and did not change the outcome — it sharpened the defect.
Two different facts share one field named tenant: the act-scope, a property of
the act that our binding slice commits, and the principal's membership, a
property of the person that approval-engine exact-matches. Gate House's
correction stands: a binding slice that must commit the scope being entered
should commit that scope, not borrow a membership claim to stand in for it. Our
schema already does — binding.target IS the act-scope and is inside view_hash —
so no field was added, only a statement (PR-08) and a provenance record (PR-09),
since key-cape emits tenant as a bare string.

key-cape had already implemented registration-bound tenancy on 2026-09-09,
correct under both candidate rulings, so the fail-closed-at-first-use risk that
made us withhold the client strings was already retired. IN-0002 closed. The one
remaining input to T07 is the deployed origin.

GH-DEC-2026-014 granted commitment-only evidence and bounded it. It satisfies
non-alteration and NOT reconstructability, and must not be described otherwise
anywhere. It also corrected our wording of the gap: we wrote that it leaves us
able to erase the content, which understates it. Commitment-only moves integrity
out of our control and leaves availability entirely inside it — the party that
can withhold the content is the party the evidence is about. Limit 3's condition
reduced, not removed.

The grant carries a condition we did not propose and would not have thought of:
the path must assert that committed content exists and where custody sits, so
non-production is a finding attributable to the custodian rather than an
unremarkable blank. A commitment with no assertion that something is being
committed to is indistinguishable from a commitment to nothing. Booked as PR-53,
and marked not-a-reversal-candidate.

Recorded the meta-rule Gate House named, now in its third setting here: unknown
versus absent in the stance map, directory-asserted versus registration-supplied
in the tenant claim, erased versus never held in the evidence path. Wherever a
system reaches one appearance by two routes, the record must say which route.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3W1dQG7GFFM9d94jFx7iR

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1565372@bnt-lap001
Assistant-Session: 16bb2f25-b34c-49ef-8e94-5fec3567a568
2026-09-10 07:57:57 +02:00

25 KiB

Product Requirements Document — Stage 1

Repo: informed-decision Stage: 1 — the L3 approval approver surface Workplan task: INFD-WP-0001-T03 Status: draft. Revised 2026-09-09 against approval-engine/docs/approver-surface-requirements.md (commits 31da1af, 5203f46) and two corrections approval-engine sent with it. Revised requirements are marked [rev-1] Companion specs: UseCaseCatalog.md, ArchitectureBlueprint.md (gated on INFD-WP-0001-T02), EvidenceModel.md


1. Scope

Stage 1 delivers one thing: a browser-facing surface on which a human approver is shown an approval-engine approval request, understands it, acts on it with the full disposition vocabulary, and leaves a reconstructable record of what they were shown when they bound themselves.

Everything in this document is L3 on the depth spectrum. L0, L1, L2, L4 and L5 appear only in UseCaseCatalog.md, and only to constrain the schema so it cannot fork later.

Traceability. Every requirement below carries a trace: line pointing at either a GOAL.md definition-of-done item (DoD-n), an INTENT.md principle (P-n) or wrongness condition (W-n), or a named external contract. A requirement that traces to nothing is out of scope by construction.

Testability. Every requirement states an observable pass condition. "The UI should be clear" is not a requirement; "a bind attempt with an unacknowledged required highlight returns an error and creates no disposition" is.


2. Personas

2.1 Approver (primary)

Holds a mandate. Receives a memo because the routing said they are the person who may bind this act — not because they were available. Is usually busy, is frequently not a specialist in the subject matter, and is accountable for the outcome afterwards.

What they need: to know why this is on their desk, what happens if they say yes, what happens if they say nothing, and what they are actually binding. What they must be able to do without leaving the object: ask a question, send it back, and push it upward.

2.2 Requester (secondary)

Assembled the memo, or an agent assembled it on their behalf. Needs to know where it is, who is holding it, and what was asked for when it comes back. Stage 1 does not give the requester an authoring UI — memos arrive from approval-engine. Authoring is Stage 2+.

2.3 Observer / auditor (tertiary)

Arrives months later with a question of the form "who decided this, having seen what?" Never uses the live surface. Consumes the evidence bundle export and audit-core. Their requirement is that the answer exists and verifies offline.

2.4 Non-persona: the agent

An agent may assemble and enrich a memo. It has no disposition capability. This is stated as a persona exclusion because the temptation to add "auto-approve low-risk items" arrives in every approval product, and it is the single change that would void the identity half of the promise. trace: P-10, W-"An agent binds", guard G_NOAGENT


3. Functional requirements

3.1 Identity and session

PR-01 — Identity is imported, never issued. Every principal is authenticated by key-cape via authorization-code + PKCE. The surface holds no password, no local credential store, and issues no assurance level of its own. Pass: no code path creates a session not derived from a key-cape token; authentication failure yields no session. trace: DoD-2, P-9, GOAL invariant "Identity is imported"

PR-02 [rev-1] — Token shape is fixed by the resource server. Tokens presented to approval-engine carry aud=approval-engine (the resource server, never the OAuth client id), principal_type: human, tenant: tenant:platform compared by exact string equality (no alias, no prefix handling — platform and tenant:coulomb are refused), non-empty sub and scope, assurance as a non-null JSON object, and RS256 verification against the issuer's /jwks. The access token is the evidence; id_token is never presented here.

Scopes required: approval:read to fetch the approval and its claim, and approval:approve to record an entry. The surface never holds approval:consume.

Pass: a token request including approval:consume fails a build-time check; approval-engine's verifier accepts the issued token on both routes. trace: approval-engine docs/approver-surface-requirements.md §1-2; DoD-2

Correction from the first draft: this requirement previously listed only approval:approve. That was wrong and would have shipped a surface able to submit an entry it was never able to display — key-cape specified the human client as [openid, approval:approve]. Raised as approval-engine open question A; see PR-05.

PR-04 [rev-1] — assurance carries authentication facts in a documented shape. approval-engine persists the token's assurance claim verbatim into the approver entry and interprets nothing. It is therefore the only place where "this approval was bound under MFA" survives into the approval record. An empty object {} is accepted by the engine and evidences nothing.

The shape must carry, at minimum: authentication method, acr/amr, and auth_time. informed-decision and key-cape own it between them.

Pass: a documented assurance shape exists and is agreed with key-cape; an entry produced by this surface allows MFA to be established from the approval record alone. trace: approval-engine §4 open question B and its 2026-09-09 correction

PR-05 [rev-1] — Entitlement to view is checked before rendering. approval-engine returning 200 is not a statement that this human is entitled to see the approval — it never answers "may this actor do X". The surface owes access-engine a check before rendering an approval to a person.

Consuming a decision is not rendering one: this requirement does not make the surface a PDP, and satisfying it must not be done by local judgment. Pass: no approval is rendered without a decision obtained from access-engine; no local rule substitutes for it when access-engine is unavailable (fail closed — see PR-06). trace: approval-engine §3; DoD-8; W-"It starts deciding"

PR-06 [rev-1] — Engine responses map to the vocabulary, and are not swallowed.

Status Reason Required surface behaviour
401 unauthenticated re-authenticate; never retry blindly
403 forbidden wrong scope/tenant; surface as a boundary error, not a decline
404 not_found no such approval in this tenant
409 duplicate_approver render as SUCCESS. This subject already has an entry; the first stands
409 conflict approval is terminal (revoked/superseded/consumed/expired); the act is no longer available
422 unprocessable malformed request — a defect in this surface
503 store_unavailable fail closed. Never present success

A browser double-submit producing duplicate_approver is routine; rendering it as a failure is a lie to the approver. Pass: each row has a test; duplicate_approver produces a success state and 503 produces no success state under any timing. trace: approval-engine §7

PR-07 [rev-1] — approved is never rendered as permission to act. approved is a state of an object. approval-engine actively refuses to serialize a decision (_assert_not_decision). A surface that renders "approved" as "you may now do the thing" has re-implemented a PDP in the browser. Approval status is read from the response body, never inferred from a 200. Pass: no copy or affordance in the surface presents approval status as authorization; status is read from the response object. trace: approval-engine §7; W-"It starts deciding"

PR-03 — The surface displays which identity and which scope are being used. Before any binding act, the approver sees the identity they are acting as and the tenant/scope they are acting in, as committed values — not as inferred context. Pass: both values appear in the rendered binding document and therefore in view_hash. trace: P-11, EvidenceModel binding slice

3.2 Presentation

PR-08 [rev-2] — The act-scope is committed; the membership claim is not borrowed to stand in for it. binding.target — the tenant, environment and system being entered — is the authoritative act-scope and is inside view_hash. The token's tenant claim is a membership fact about the principal and is never used as the act-scope. Pass: no code path derives binding.target from the token's tenant claim; changing binding.target changes view_hash (existing isolation vector 3). trace: GH-DEC-2026-013 §5; EvidenceModel §8c

PR-09 [rev-2] — The tenant claim is stored with its provenance. key-cape emits tenant as a bare string, so a consumer cannot distinguish a tenant the directory asserted about the person from one a registration supplied about the client they came through. Until the claim carries its own provenance, this surface records which route the value arrived by.

This surface must not declare a tenant the directory record does not carry without saying so in the record — doing so asserts a fact about a person on this registration's authority, which GH-DEC-2026-013 permits only as a bounded gap. Pass: every stored tenant value carries a provenance marker; a value with an undifferentiated or absent provenance is a validation failure, not a default. trace: GH-DEC-2026-013 §5; key-cape 329e48f

PR-10 — A memo renders question, requested act, binding level, brief and consequences before any action control is reachable. Pass: the disposition controls are not operable until the brief region has been rendered; a memo missing a question fails validation and does not render. trace: INTENT "One question per memo"; GOAL invariant

PR-11 — The packet is always reachable in full. Highlights are an aid to attention, never a filter. Every document in the packet is openable from the memo, and the count of packet items is visible without interaction. Pass: for a memo with n packet items, all n are reachable; no item is hidden behind a highlight-only view. trace: P-8, W-"Highlights shrink the legal object"

PR-12 — The UI states unmistakably that the whole instrument is bound. Acknowledging highlights must never read as binding only the acknowledged sections. This is a legal exposure, not a copy preference. Pass: the bind control carries scope language naming the whole instrument; reviewed and signed off explicitly as a requirement, not left to implementation. trace: P-8, W-"Highlights shrink the legal object"; exploration open question 5

PR-13 [rev-1] — Every render creates a presentation record. Carrying memo id, memo version, locale, UI release, timestamp, principal, and the computed view_hash and awareness_hash. Pass: rendering a memo twice creates two presentation records; a disposition references exactly one. trace: DoD-3, DoD-5, guard G_PRES

Constraint discovered 2026-09-09: POST /entries reads the request body and discards it. Approver identity, assurance and evidence_ref come only from the verified token, deliberately — a caller-supplied approver field is provenance, not evidence. So view_hash cannot ride into the approval entry. Stage 1 therefore holds the presentation record here, emits it to audit-core, and correlates to the entry by (approval_id, subject, approved_at), all three returned by the engine after a successful entry. Putting the hash inside the approval object is a change request against approval-engine and is doctrine first — it goes to gate-house. GOAL.md DoD-3 is satisfied by the correlation triple, not by a hash stored on the entry.

PR-14 — A disposition binds to a presentation of the same memo version. A bind attempt against a stale version is refused, not silently upgraded. Pass: incrementing the memo version invalidates outstanding presentations; a bind against the old presentation returns a version conflict. trace: guard G_PRES; INTENT invariant "Binding acts reference a presentation of the same version"

3.3 The acknowledgment gate

PR-20 — Required highlights must be acknowledged before a binding verb. Where highlights[].required_ack is true, accept, decline on a bind step, and acknowledge are unavailable until every required highlight id is in the presentation's acked set. Pass: a bind attempt with an unacknowledged required highlight fails closed, creates no disposition, and returns an error naming the outstanding ids. trace: DoD-4, guard G_ACK

PR-21 — Acknowledgment is an explicit act, never inferred. Scroll position, time on page, focus events and viewport intersection MUST NOT acknowledge a highlight. Pass: no acknowledgment is recorded by any event other than a deliberate control activation. trace: P-12, INTENT "Informedness without surveillance"

3.4 Disposition vocabulary

PR-30 — The surface implements the full verb set for the step kind, not approve/reject. Overlay verbs comment and discuss are available whenever the memo status allows. return, forward and escalate are available to the assignee on every step kind. accept and decline follow the legality table. Pass: every verb in state-transitions.md §B2 legal for the step kind is reachable in the UI for that step. trace: DoD-6, INTENT §"The Disposition Vocabulary"

PR-31 — accept is not offered on a weak step. On inform, comment, review and acknowledge steps, accept is absent — not present-and-disabled. Kenntnisnahme is not approval. Pass: the control does not exist in the rendered UI for weak steps; an API call attempting it is refused. trace: state-transitions §B2 ("accept: —" on weak); INTENT design principle 6

PR-32 — return carries at least one coded reason. Free text alone is not a return. Return is a first-class success path and must be as easy to reach as accept. Pass: a return with zero coded reasons is refused (guard G_REASONS); the return control has equal visual weight to accept. trace: DoD-6, P-4, guard G_REASONS

PR-33 — return and decline are distinguishable in the record. A memo sent back for improvement and a memo refused are different outcomes and must not collapse to one status. Pass: returned and completed/declined are distinct in the stored record and in the export. trace: DoD-6, GOAL definition of done 6

PR-34 — discuss keeps the dialogue on the object. A question to the requester creates a thread entry on the memo. The surface provides no "email the requester" affordance. Pass: no outbound side-channel exists; the thread is part of the exported evidence. trace: P-5

PR-35 — escalate requires a strictly higher mandate target. Where the target's mandate is not strictly higher for this act, the verb is forward, not escalate. Pass: guard G_ESC is evaluated; a same-level target is refused as an escalation. trace: guard G_ESC Known Stage 1 limitation: without the mandate graph, G_ESC cannot be fully evaluated. Stage 1 MUST record the distinction the approver asserted rather than silently treating escalate as forward. See §6.

3.5 The binding / awareness split

PR-40 — Awareness fields never enter view_hash. Proposed role, other available roles, other tenants, and last-session summary are rendered on the same surface, hashed into awareness_hash, and excluded from the signed binding document. Pass: isolation vector 2 — editing an awareness field does not change view_hash — is green in CI. trace: DoD-4, P-11, W-"Awareness leaks into the signature", EvidenceModel

PR-41 — Awareness is visually distinguished from binding. The approver can tell, without reading documentation, which part of the surface they are committing to and which part is orientation. Pass: the two regions are separately labelled; the labelling is part of the reviewed design, not incidental styling. trace: P-11

PR-42 — Promotion is explicit and versions the memo. Moving a field from awareness into binding creates a new memo version and invalidates outstanding presentations. Pass: guard G_PROMOTE; a presentation taken before promotion cannot be bound against after it. trace: guard G_PROMOTE, INTENT invariant on awareness_promoted``

3.6 Evidence

PR-50 — An evidence bundle exports and verifies offline. Containing the memo, its versions, the presentations with both hashes, the dispositions, the thread, and the hash chain. Pass: an exported bundle verifies with no network access; verification detects a mutated field. trace: DoD-7, P-7

PR-51 — The export is the evidence, not a screenshot. No requirement is satisfied by a rendered image of the surface. Pass: the bundle is structured data; the canonicalizer reproduces view_hash from it. trace: P-7

PR-52 — The surface states the bound of its own guarantee. Documentation and the export state that attestation covers accident and later tampering, not a compromised surface presenting X and attesting Y. Pass: the residual appears in EvidenceModel.md and in the bundle metadata. trace: gate-house decision request §3; approval-engine's equivalent residual

PR-53 [rev-2] — Emitted evidence asserts that content exists and where custody sits. Commitment-only emission carries, alongside the hashes, an assertion that the committed content exists and the custodian holding it, so that failure to produce at retrieval is a conformance failure attributable to the custodian rather than an unremarkable blank. Without it, a reviewer cannot distinguish erased, withheld, lost and never held. Pass: every emitted commitment carries content_exists and custody; a retrieval failure produces a finding naming the custodian. trace: GH-DEC-2026-014 §4 — a granting condition, not a reversal candidate

PR-54 [rev-2] — Commitment-only evidence is never described as reconstructability. It satisfies non-alteration. It does not carry what it commits to, and establishes what was committed to only conditionally — if a document is later produced, whether it is the one. Pass: no document, export field or conformance claim in this repository describes the independent evidence path as reconstructing content. trace: GH-DEC-2026-014

3.7 Locale and accessibility

PR-60 — German and English, with German as a first-class source language. The Umlaufmappe vocabulary — Kenntnisnahme, Mitzeichnung, Schlusszeichnung, Rücksprache, Rückgabe — is the domain vocabulary, not a translation of English product terms. Pass: both locales render every verb; locale is part of the presentation record and therefore of view_hash. trace: DoD-6, INTENT §"The Disposition Vocabulary"; exploration open question 7

PR-61 — Locale changes the memo version's presentation, not its meaning. Because locale enters view_hash, a memo read in German and one read in English produce different presentations of the same binding. This is intended and must be documented, not smoothed over. Pass: the two presentations differ in view_hash and reference the same memo version. trace: canonicalize.md allow-list

PR-62 — Keyboard-operable and screen-reader-navigable throughout. An approver using assistive technology can read the brief, open packet items, acknowledge highlights and bind, without a mouse. Pass: every disposition control and every acknowledgment control is reachable and operable by keyboard; the acknowledgment state is programmatically determinable. trace: P-8; PR-21 (acknowledgment must remain explicit under AT too)


4. Anti-requirements

These are requirements not to build things a reasonable implementer would otherwise add. Each is testable as an absence.

PR-70 — No dwell timers. Minimum reading time, "you must spend 30 seconds on this page", and countdown-gated buttons are forbidden.

PR-71 — No keystroke, mouse-movement or attention analytics. Informedness is evidenced by the hash of the presented view plus explicit acknowledgment, never by behavioural telemetry.

PR-72 — No dark patterns. Accept is not visually privileged over return. Decline is not hidden behind a secondary menu. No pre-checked acknowledgments. No confirm-shaming copy.

PR-73 [rev-1] — No auto-approval, no delegation-to-agent, no "approve all". Bulk selection may exist for reading; it must not exist for binding.

Correction: the first draft assumed approval-engine would refuse a non-human approver entry. It does not — only /consume is restricted by principal type, and the approval-engine-operator service client holds approval:approve. So a non-human principal can supply approver evidence today. Whether it should is gate-house doctrine. Enforcement of "humans bind, agents draft" is therefore this surface's own obligation, and is auditable via schema v4's entries[].principal_type, which records the verified value from the token. Read that field; never infer it from the shape of subject_id, which is a naming convention, not a verified claim. Entries written before v4 are null and must not be read as human.

PR-74 — No approval-state caching. The surface never stores or serves whether an approval is valid. It asks approval-engine. trace: W-"It reimplements approval-engine", GOAL invariant

PR-75 — No authorization endpoint. Nothing in this repository answers "may this actor do X". trace: DoD-8, W-"It starts deciding"

Pass for §4: each is a review checklist item and, where mechanically checkable, a test asserting the absence.


5. Non-requirements for Stage 1

Not deficiencies — deliberate exclusions carried from GOAL.md:

  • Memo authoring. Memos arrive from approval-engine.
  • The mandate graph.
  • L4/L5, QES, QTSP, qualified archival retention.
  • Multi-party parallel Mitzeichnung.
  • EUDI Wallet / eID identity import.
  • Notification transport. A memo may be announced elsewhere; the dialogue stays on the object.
  • Mobile-native clients. Responsive web only.
  • Offline operation.

6. Known limitations, stated up front

L-01 — escalate without a mandate graph is forwarding. Guard G_ESC requires knowing that the target's mandate is strictly higher. Stage 1 has no such map. The surface records the approver's asserted intent to escalate and marks it unverified rather than either blocking the verb or pretending the guard passed. GOAL.md records this as accepted; EvidenceModel.md must state that an escalation in a Stage 1 bundle is an assertion, not a verified claim.

L-02 — view_hash is computed by the renderer. A compromised surface can present one thing and attest another. Not closed, not closable at this layer, and not claimed to be closed.

L-03 — Binding level is organizational only. Stage 1 produces no AES or QES signature. binding_level is carried in the object and in view_hash so that higher levels are expressible without a schema change, but nothing above organizational is implemented or claimed.

L-04 — view_hash versus approval-engine's binding digest is unresolved. Filed as INFD-IN-0001 R3. Until ruled, the surface computes view_hash and does not assert any relationship to the approval claim's digest.

L-05 — view_hash is not inside the approval entry. By the engine's deliberate design (PR-13). Correlation is by (approval_id, subject, approved_at). An auditor holding only the approval object cannot reach the presentation without also holding this surface's record or audit-core.

L-06 — A memo return is invisible to approval-engine. accept is the only disposition that becomes a POST …/entries. return, discuss, escalate and the rest are dispositions of a memo and are not represented in that engine at all. Correct separation, but it means the approval object alone does not show that a human sent the question back.


7. Open questions for review

  1. Does the approver need to see other pending memos? Answered 2026-09-09 by construction. approval-engine exposes get-by-id only: no list, no search, no "awaiting me" query, and there will not be one. Approval ids reach this surface from whatever requested the approval, never by polling. A listing surface would itself be an authorization question and would need scoping by something other than that engine's judgment. The approvals-inbox shape is foreclosed upstream, which is the outcome this repository wanted anyway.
  2. What does the surface do when approval-engine is reachable but audit-core is not? Fail closed on binding, or bind and queue? The ArchitectureBlueprint must answer this per-dependency (T05).
  3. Who is the requester for an approval-engine-originated memo, given the engine does not model one? Likely the approval's principal, but this needs confirming against the approval object.
  4. Is abstain in Stage 1? It is in the state tables and it is real in Mitzeichnung practice, but it has no obvious approval-engine counterpart.