Establish INTENT, Stage 1 GOAL, and founding workplan

Claim ownership of the browser-facing approver UI that approval-engine
deliberately does not contain. approval-engine's INTENT names an approvals
inbox under Non-Goals, and docs/keycape-service-registrations.md records that
the human approver client's client_id and callback URI "must come from its
owner once it exists" — leaving key-cape's KEY-WP-0013-T02 blocked on an
unassigned component.

- INTENT.md: Decision Memo concept, the binding/awareness split and the two
  hashes, ownership and non-ownership against the named estate repositories,
  and a provisional PEP-shaped layer placement flagged for a gate-house ruling
  rather than asserted.
- GOAL.md: Stage 1 is the L3 approval approver surface — the narrowest real
  consumer with a live blocking dependency — plus the written answer to who
  owns the approver UI.
- workplans/INFD-WP-0001: founding documents, the gate-house layer/ownership
  ruling, the four specs (PRD, UseCaseCatalog, ArchitectureBlueprint,
  EvidenceModel), schema and canonicalizer promotion out of history/ with the
  isolation vectors under test, the key-cape client registration, and a
  walking skeleton that includes return and discuss.

history/ is preserved unmodified as provenance.

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
This commit is contained in:
tegwick 2026-09-09 10:47:36 +02:00
parent 771ddb9ced
commit ee2cca579c
16 changed files with 3218 additions and 1 deletions

130
GOAL.md Normal file
View file

@ -0,0 +1,130 @@
---
repo: informed-decision
repo_flavor: project
project_status: active
stage: 1
started: "2026-09-09"
---
# Goal — informed-decision, Stage 1
## Outcome
**The approver UI has an owner, and it is this repository.**
Stage 1 is complete when a human approver can be shown a `approval-engine`
approval request on a surface this repository owns, understand it, act on it
with a vocabulary richer than approve/reject, and leave behind a
reconstructable record of *what they were shown when they bound themselves*
and when `key-cape` holds the browser-client registration it has been waiting
for since 2026-09-08.
Two things are being settled at once, and they are the same thing:
1. **The ownership question.** `approval-engine` disowns the inbox by design
and named the gap in `docs/keycape-service-registrations.md`. Nobody picked
it up. Stage 1 picks it up, in writing, with a catalog placement agreed by
`gate-house` rather than asserted.
2. **The capability.** A browser-facing approver surface that presents
`approval:approve` tokens to `approval-engine` and records the presentation.
## Why this is the first stage
The founding exploration reaches L5 — multi-party international instruments.
Stage 1 deliberately does not. It takes the **narrowest real consumer with a
live blocking dependency**, because a decision surface with no consumer is a
design document, and this estate already has enough of those.
The approval approver flow is L3 on the depth spectrum. It is the right first
cut because it is the only level currently costing another repository progress.
## What Stage 1 delivers
- `INTENT.md`, `GOAL.md`, `SCOPE.md`, and a `layer.yaml` derived from a
`gate-house` answer.
- The four specs: `docs/specs/ProductRequirementsDocument.md`,
`docs/specs/UseCaseCatalog.md`, `docs/specs/ArchitectureBlueprint.md`,
`docs/specs/EvidenceModel.md`.
- The Decision Memo schema promoted out of `history/` into `schemas/`, with the
canonicalizer and the isolation vectors under test in CI.
- A published OIDC browser-client contract — `client_id` and callback URI —
handed to `key-cape`, closing `KEY-WP-0013-T02`.
- A working L3 approver surface against a deployed `approval-engine`.
## What Stage 1 explicitly does not deliver
- L4/L5, QES, QTSP integration, or qualified archival retention.
- Multi-party parallel sign-off (`Mitzeichnung` across several parties at once).
- The mandate graph. Stage 1 routes to a named approver; it does not maintain a
living map of who may bind what. Without that map, `escalate` is only
forwarding — this is a **known and accepted** Stage 1 limitation, recorded
here so it is not discovered later as a defect.
- EUDI Wallet / eID identity import. Identity comes from `key-cape` only.
- A general workflow engine, a notification transport, or a ticketing system.
## Invariants
These hold for Stage 1 and for every stage after it.
- **`access-engine` remains the only policy decision point.** This repository
renders questions and records answers. No endpoint here answers "may this
actor do X". A disposition recorded on a memo is evidence of a human act, not
an authorization verdict.
- **`approval-engine` remains the sole owner of approval state.** This surface
calls its mutations and reads its introspection claim. It never caches
validity, never infers consumption, and never holds approval current-state.
`GH-DEC-2026-003` ordering is honoured: decision, then consume, then act.
- **Identity is imported.** Every principal on this surface is authenticated by
`key-cape`. No local credential, no self-issued assurance level. Human
principals carry `principal_type: human` and are refused `approval:consume` by
the engine — this surface must never attempt it.
- **The binding/awareness split survives contact with the first consumer.**
`view_hash` covers only what the person committed to. Proposed roles,
other-tenant orientation and last-session summaries are hashed separately and
are not signed unless explicitly promoted into `awareness_promoted`.
- **One question per memo.** An approval that bundles three unrelated acts is
three memos.
- **Humans bind, agents draft.** An agent may assemble a memo and may not
complete a disposition.
- **The schema does not fork.** Any Stage 1 field added for approvals must be
expressible for an L0 login banner, or it does not go in the shared object.
## The question Stage 1 must answer in writing
> **Who owns the approver UI?**
The answer this repository proposes, to be ratified rather than assumed:
- **informed-decision owns it** — the surface, the presentation record, the
browser client, and the evidence of informedness.
- It is **PEP-shaped**, not an Engine: it is browser-facing and causes a
protected side effect on the far side of a decision, so it owes companion §5
and statute §6.4.
- It supplies exactly one PIP-like fact — *what was presented* — as a claim
carrying `view_hash`, issuer and freshness, and it never evaluates that fact.
- The catalog row does not exist yet. `INFD-WP-0001-T02` asks `gate-house` and
writes `layer.yaml` from the ruling. If `gate-house` places this component
elsewhere, `INTENT.md` and this file change to match — the ruling wins.
## Definition of done
1. `gate-house` has ruled on the layer placement and `layer.yaml` reflects it.
2. `key-cape` has the `client_id` and callback URI, and `KEY-WP-0013-T02` is
unblocked with the answer traceable to this repository.
3. The four specs exist, are reviewed against the current `approval-engine`,
`access-engine`, `key-cape` and `audit-core` contracts, and name their
external gates.
4. `view_hash` / `awareness_hash` are computed by code in this repository, and
the four isolation vectors from the exploration are green in CI.
5. One approval is approved end to end by a human through this surface against
a deployed `approval-engine`, and the resulting approval entry is
reconstructable from a stored presentation.
6. `return` and `discuss` are demonstrated on a real approval and are
distinguishable from `decline` in the record.
## Stage 2 and beyond — direction only, not commitment
L0 informed login/elevation in front of `key-cape`, then L2 ADR accept against
the estate's own decision records. Those two prove scale invariance downward and
sideways. L4 contract paths and any QES work wait for a named buyer and a
retention answer, both open questions in the founding exploration.

245
INTENT.md Normal file
View file

@ -0,0 +1,245 @@
---
project: informed-decision
product: Informed Decision
status: draft
intent_version: 0.1.0
date: "2026-09-09"
domain: infotech
topic_slug: netkingdom
layer: surface
role: pep-shaped # PROVISIONAL — see "Layer Placement", INFD-WP-0001-T02
standard: net-kingdom/canon/standards/security-layer-model_v0.7.md
companion: net-kingdom/SECURITY-COMPANION.md
---
# INTENT
> This file captures why this repository exists and the kind of system it is
> meant to become. It is aspirational and stable, not a description of current
> implementation. Implemented boundary belongs in `SCOPE.md`; the current stage
> belongs in `GOAL.md`; current work belongs in `workplans/`.
## Purpose
> **informed-decision owns the surface on which a human is shown a decision and
> binds their identity to it — and owns the evidence that the presentation
> actually happened.**
It answers one question, totally and decidably:
> **What was this person shown, in what role, at the moment they bound
> themselves to this act — and can that be proven afterwards?**
Nothing else. It does not decide whether the act is permitted (`access-engine`),
does not own the approval object (`approval-engine`), does not author the
doctrine that requires an approval (`gate-house`), does not authenticate the
person (`key-cape`), and does not archive the trail (`audit-core`).
## Why This Exists
Every layer of the NetKingdom estate has an owner except the one a human
actually touches.
`approval-engine`'s `INTENT.md` names it under Non-Goals, twice and
deliberately: *"Not a workflow or ticketing system"*, *"Not an approvals inbox
or notification surface."* That exclusion is correct — an engine that renders
its own object is self-dealing — but it leaves a hole, and the hole is now
load-bearing:
- `approval-engine` is a bearer-token resource server with no Ingress, no
redirect endpoint and no browser client
(`deploy/approval-engine.yaml`: Namespace + ClusterIP + StatefulSet).
- `key-cape` (`KEY-WP-0013-T02`) asked approval-engine for the human approver
client's `client_id` and callback URI on 2026-09-08.
- approval-engine correctly refused to invent them, recording in
`docs/keycape-service-registrations.md`: *"The human approver flow therefore
belongs to whichever browser-facing approver UI presents `approval:approve`
tokens to this engine. That component is not in this repo."*
So the approver UI is not merely missing. It is a **named, unassigned
dependency** that currently blocks an OIDC client registration in another
repository. **This repository claims it.**
The claim is not opportunistic. The founding exploration
(`history/20260909-initial-exploration/`) arrived at the same object from the
opposite direction — the German *Umlaufmappe* / *Zeichnungsbuch*, a portable
decision object that carries a question, the context needed to answer it, the
requested act, and a cryptographic binding between *identity*, *what was shown*,
and *what was bound*. That is exactly the artifact the estate is missing, and it
generalizes past approvals without ever becoming a decision point.
## The Concept — the Decision Memo
A **Decision Memo** is a portable object presented to one human who holds a
mandate. It carries:
- a **question** that must be answered, and a **requested act**;
- the **brief and consequences** a reasonable decision-maker needs;
- a **packet** of hashed documents and **highlights** pointing into it;
- a **binding slice** — the identity and scope being committed, which is signed;
- an **awareness slice** — orientation (proposed role, other roles, other
tenants, last session), which is shown and hashed but **not** signed;
- a **route** (`Zeichnungsleiste`), a **thread**, and a record of every
**presentation** and **disposition**;
- an append-only **evidence** chain.
Two hashes are computed over every presentation:
| Hash | Over | Signed? |
| --- | --- | --- |
| `view_hash` | the binding document | yes, from organizational level up |
| `awareness_hash` | the awareness document | no, unless a field was promoted |
`view_hash` is the mechanism. It is what turns *"the file was signed"* into
*"this person, in this role, was shown this view, and bound this act."*
## Scale Invariance Is the Thesis
One object model, one evidence model, one UX grammar, from a ten-second login
to a multi-party instrument:
| Level | Act | Typical binding |
| --- | --- | --- |
| L0 | Informed login / session | acknowledgment / organizational |
| L1 | Privileged authorization (JIT elevation) | organizational |
| L2 | Formal ADR / decision-record accept | organizational / AES |
| L3 | Management approval | organizational / AES |
| L4 | Contract / instrument | AES / QES |
| L5 | Multi-party international instrument | QES per party (horizon, not v1) |
If L0 and L3 share the schema, the transitions, and the two hashes, the thesis
holds. If they fork, it does not — and the estate has bought a bespoke approvals
inbox instead of a decision surface.
**The approval approver UI is L3.** It is the first stage precisely because it
is a real consumer with a real blocking dependency, not a demo.
## The Disposition Vocabulary
The differentiator is not another workflow engine. It is that *approve/reject*
is the wrong vocabulary for judgment. The verbs are:
`comment` · `discuss` (Rücksprache) · `return` for improvement (Rückgabe) ·
`forward` · `escalate` · `acknowledge` (Kenntnisnahme) · `accept`
(Mitzeichnung / Schlusszeichnung) · `decline` · `withdraw` · `configure`
`accept` on a Kenntnisnahme step is **illegal on purpose**. **Return is success,
not failure** — a memo sent back for improvement is the surface working.
## What informed-decision Owns
- the **Decision Memo** object, its versions, and its lifecycle;
- the **presentation record** — what was rendered, to whom, when, in which
locale and UI release;
- **canonicalization** of the binding and awareness documents, and the
computation of `view_hash` / `awareness_hash`;
- **highlight acknowledgment** — required highlights acked before a bind is
permitted;
- the **disposition vocabulary** and its legality tables;
- the **evidence bundle** as an export — the reconstructable answer to *who
decided what, having seen what*;
- the **browser-facing OIDC client** for human principals, including the
`client_id` and callback URI that `key-cape` is waiting for;
- the **pre-sign / post-sign split**: identity and bind-scope are committed;
hats, last-used role, and other-tenant orientation are awareness unless
explicitly promoted.
## What informed-decision Does Not Own
- **the decision**`access-engine`, always and only (statute §6). This
repository renders a question and records an answer; it never evaluates
whether an actor may do a thing;
- **the approval object**`approval-engine`. This surface calls that engine's
mutations with an `approval:approve`-scoped token; it never holds approval
current-state and never answers "is this approval valid";
- **approval doctrine** — which acts need approval, how many approvers, which
separations of duty — `gate-house`;
- **identity and authentication**`key-cape`. Identity is *imported*
(OIDC, passkeys, eID, EUDI Wallet, QTSP), never invented here;
- **the evidence archive**`audit-core`. This repository emits and exports;
the trail is held there;
- **credentials materialized after a decision**`secrets-engine`;
- **notification transport** — a memo may be announced by other means; the
dialogue stays on the object.
## Design Principles
1. **One object, many depths.** Login and instrument share a schema.
2. **Presentation is part of the signed payload.** Dynamic linking, not a
signature over a file nobody read.
3. **Competence before convenience.** Routing follows mandate.
4. **Return is success.**
5. **Dialogue stays on the object.** Side-channel email is an anti-pattern.
6. **Binding level is explicit.** Kenntnisnahme ≠ Mitzeichnung ≠ QES.
7. **Evidence is an export, not a screenshot.**
8. **Least privilege of attention.** Highlights first, full packet available,
no dark patterns — and the UI must make unmistakable that the *whole*
instrument is bound, not only the highlights.
9. **Identity is imported.**
10. **Humans bind, agents draft.** An agent may assemble a memo. It may never
complete the binding act on the principal's surface.
11. **Awareness is not consent.** Defaulting a role to last-used is required
for situational awareness and forbidden from silently entering `view_hash`.
12. **Informedness without surveillance.** Evidence is the hash of the
presented view plus explicit highlight acknowledgment — never keystroke
analytics, dwell timers, or attention theater.
## Layer Placement — Provisional
Under security layer model v0.7 this repository is **not** an Engine. It is
browser-facing and it causes a protected side effect on the far side of a
decision, which is the shape statute §6.4 and companion §5 call **PEP-shaped**.
`approval-engine`'s own INTENT names this expectation: *"Callers that do are
PEP-shaped and owe companion §5 / statute §6.4."*
This is declared **provisional** because the catalog row does not exist yet and
inventing one is the drift §17 exists to prevent. `INFD-WP-0001-T02` takes the
question to `gate-house` and writes `layer.yaml` from the answer, not from this
paragraph. The framing this repository will argue: informed-decision is a
**presentation and binding surface** — a PEP for the acts it fronts, and a PIP
for exactly one fact, *what was presented*, which it supplies as a claim and
never evaluates.
## What Would Make This Repository Wrong
- **It starts deciding.** Any surface that answers "may this actor do X" rather
than "here is the question, and here is what you were shown" is the failure
statute §6 names.
- **It becomes an approvals inbox and nothing else.** If the object that ships
cannot also carry an L0 login banner and an L2 ADR accept, the scale-invariance
thesis is dead and the estate bought a queue.
- **It forks the schema per level.** Three levels, three object models, one
broken promise.
- **Awareness leaks into the signature.** If a last-used role or a tenant
orientation hint enters `view_hash`, the surface signs something the person
did not commit to.
- **Highlights shrink the legal object.** If acking three highlights is
presented as binding only those three sections, the UI has produced a lie.
- **An agent binds.** If any automation can complete a disposition on a
principal's behalf, the identity half is fiction.
- **It reimplements `approval-engine`.** Caching approval validity, inferring
consumption from a decision record, or holding approval current-state here
breaks `GH-DEC-2026-003` and the atomicity contract.
- **It invents identity.** A local password, a local session token that is not
derived from `key-cape`, or a self-issued assurance level.
- **Nobody uses it.** If `approval-engine` never receives an `approval:approve`
token from this component's client, the surface is decorative and the gap it
claimed to close is still open.
## Success Criteria
1. `key-cape` holds a registered browser client whose `client_id` and callback
URI this repository owns and published, and `KEY-WP-0013-T02` is unblocked.
2. A human approver completes an approval entry against `approval-engine`
entirely through this surface, with an access token carrying
`aud=approval-engine`, `principal_type: human`, and scope `approval:approve`.
3. The approval entry is reconstructable from a `view_hash` — the estate can
show what the approver was looking at.
4. Required highlights cannot be bypassed: a bind attempt without acks fails.
5. `return`, `discuss`, and `escalate` are exercisable on a real approval and
are not modelled as `decline`.
6. The same schema renders an L0 login banner and an L2 ADR accept, with the
binding/awareness split intact and the isolation vectors green.
7. An evidence bundle exports and verifies offline against
`audit-core`'s record of the same transitions.
8. No endpoint in this repository answers an authorization question.

View file

@ -1,3 +1,37 @@
# informed-decision
Userinterface for executive decisions modeled as a sign and return book.
User interface for executive decisions, modelled as a sign-and-return book —
the German *Umlaufmappe* / *Zeichnungsbuch*, made cryptographic.
A **Decision Memo** carries a question, the context needed to answer it, the
requested act, and a binding between *identity*, *what was shown*, and *what was
bound*. The promise is not "the file was signed" but **"this person, in this
role, was shown this view, and bound this act."**
One object model from a ten-second login (L0) to a multi-party instrument (L5).
## Where to start
| File | What it is |
| --- | --- |
| `INTENT.md` | Why this repository exists and what it must never become |
| `GOAL.md` | The current stage, its invariants, and its definition of done |
| `workplans/` | Current work |
| `history/20260909-initial-exploration/` | Founding exploration — schema, state transitions, canonicalization, vectors |
## Stage 1
Own the browser-facing **approver UI** that `approval-engine` deliberately does
not contain, and answer in writing who owns it. `approval-engine` is a
bearer-token resource server with no browser client; `key-cape`
(`KEY-WP-0013-T02`) is waiting on a `client_id` and callback URI that no
component has claimed. This repository claims them.
See `GOAL.md`.
## Boundaries
This repository renders questions and records answers. It does **not** decide
(`access-engine`), does not own the approval object (`approval-engine`), does
not author approval doctrine (`gate-house`), does not authenticate anyone
(`key-cape`), and does not archive the trail (`audit-core`).

View file

@ -0,0 +1,325 @@
# informed-decision — Initial exploration
Working notes from the founding conversation (9 September 2026).
Use this file plus the sibling artefacts as the project starting point.
Companion files:
| File | What it is |
|---|---|
| `decision-memo.schema.json` | JSON Schema draft 2020-12, object model v0.2 |
| `decision-memo-schema.md` | Human commentary on the schema and invariants |
| `decision-memo.example.json` | ADR accept example (L2) |
| `decision-memo.login.example.json` | Login banner example (L0) with binding vs awareness |
| `state-transitions.md` | Verb × status × step-kind tables |
| `canonicalize.md` | `view_hash` / `awareness_hash` algorithm |
| `canonicalize.py` | Reference canonicalizer |
| `vectors/` | Binding and awareness fixtures plus expected hashes |
---
## 1. Intent
Build **informed-decision**: a user interface for executive (and everyday) decisions modelled as a *sign-and-return book*.
Use cases named in the brief:
- logging into a system
- authorizing an action that needs elevated privileges
- formally accepting an ADR
- approving a management decision you are responsible for
- signing a business contract
- at greatest depth: multi-party sign-off of complex international instruments
Required verbs on the book:
- present necessary context
- add comments
- contact the requester
- return the memo for improvement
- forward or escalate
- accept electronically on the users identity
- document what happened with auditability
- establish cryptographic proof (sign the decision, stamp relevant files)
Spectrum:
- **Simplest:** commentary on a login mask — which system, which identity, which privileges.
- **Deepest:** complex decision memos with multiple documents, highlight pointers to critical sections, multi-party sign-off.
The German administrative metaphor is the right one: *Umlaufmappe*, *Zeichnungsleiste*, *Mitzeichnung*, *Schlusszeichnung*, *Kenntnisnahme*, *Rücksprache*, *Bitte um Berücksichtigung*.
---
## 2. Unifying insight
A sign-and-return book is not a document folder. It is a **portable decision object** that travels with:
- a question that must be answered
- the context a reasonable decision-maker needs
- a requested act (login, elevate, approve, accept, sign, escalate)
- a record of what happened
- a cryptographic binding between *identity*, *what was shown*, and *what was bound*
Existing product categories split that object:
| Category | Strength | Gap |
|---|---|---|
| PAM / JIT access | time-bound elevation, justification | almost no work-object context |
| E-sign / CLM | ceremony, sealed PDFs, routing | weak “why this is on your desk” |
| ADR tools | context / decision / consequences | “accepted” is a Git status, not an identity act |
| Board portals | packs, votes, elite audience | too heavy for daily operational acts |
| Digital *Umlaufmappe* (SAP, enaio, M-Files) | circulation + protocol | weak proof of *informedness*; not login-scale |
| Decision intelligence | recommendation and policy | not the binding act |
White space: one object model, one evidence model, one UX grammar, from a 10-second login to a treaty.
The differentiator is not another workflow engine. It is:
1. **Informedness as a measurable property** (what was presented, highlighted, opened, attested).
2. **A disposition vocabulary richer than approve/reject.**
3. **Cryptographic binding of presentation to act** (dynamic linking), not only of file to identity.
4. **Scale invariance.**
Research notes that belong in the backlog:
- eIDAS / eIDAS 2.0 / EUDI Wallet: QES becomes a mass wallet capability around 202627; identity should be imported, not invented.
- Dynamic linking: the human-readable content actually presented must be bound into the signature.
- Accountability UI (identifiability, evaluation, social presence) reduces abuse of access — relevant to the login end of the spectrum.
- Agents may draft memos; they must not be able to complete the binding act on the principals surface.
---
## 3. Vision and mission drafts
**Working vision**
People should never have to bind an identity to an action they could not reasonably understand. Informed-decision is the book in which that understanding is presented, contested, returned, escalated, and sealed.
Alternates kept:
- Every binding act is an informed act: the person who decides sees what matters, can send the question back, and leaves cryptographic proof of what they bound themselves to.
- Replace the circulating folder, the login banner, and the signature page with one governed surface.
- Make European identity useful at the moment of judgment: wallet-grade identity, qualified signatures, and an evidence pack that still answers *who decided what, having seen what*.
**Working mission**
Give every organization a reusable Decision Memo: assemble the minimum necessary context, route it to the person who actually holds the mandate, let them comment, discuss, return or escalate, and — when they accept — bind identity, presentation, and outcome into a verifiable evidence bundle.
**Promise**
Not “the file was signed.”
“This person, in this role, was shown this view, and bound this act.”
---
## 4. Design principles
1. One object, many depths. Login and treaty share a schema.
2. Presentation is part of the signed payload.
3. Competence before convenience. Routing follows mandate.
4. Return is success, not failure.
5. Dialogue stays on the object. Side-channel email is an anti-pattern.
6. Binding level is explicit. Kenntnisnahme ≠ Mitzeichnung ≠ QES.
7. Evidence is an export, not a screenshot.
8. Least privilege of attention. Highlights first; full packet available; no dark patterns.
9. Identity is imported (SSO, passkeys, eID, EUDI Wallet, QTSP).
10. Humans bind. Agents draft.
Later addition that is now first-class:
11. **Pre-sign and post-sign information are different objects.** Identity and bind-scope are committed. Hats, last-used role, and other-tenant orientation are awareness unless promoted.
---
## 5. Depth spectrum
| Level | Act | Typical binding |
|---|---|---|
| L0 | Informed login / session | acknowledgment / organizational |
| L1 | Privileged authorization (JIT) | organizational |
| L2 | Formal ADR / decision-record accept | organizational / AES |
| L3 | Management approval | organizational / AES |
| L4 | Contract / instrument | AES / QES |
| L5 | Multi-party international instrument | QES per party (horizon, not v1) |
v1 should prove the thesis on A+B+C sharing one object: informed login/elevation, formal ADR, one real contract type.
---
## 6. Decision Memo object (v0.2)
```
DecisionMemo
├── requester / subject / parties[] + mandate
├── question + requested_act + binding_level
├── brief + consequences
├── identity_context.binding PRE-SIGN identity + scope → view_hash
├── identity_context.awareness hats, last-used, tenants → awareness_hash
├── session POST-SIGN live hat
├── packet[] hashed files
├── highlights[] pointers into packet
├── route.steps[] Laufweg / Zeichnungsleiste
├── thread[]
├── presentations[]
├── dispositions[]
├── relations[]
└── evidence append-only hash chain
```
Lifecycle:
```
draft → submitted → circulating | awaiting_actor | in_discussion
↘ returned —new version→ submitted
↘ completed (outcome set)
↘ withdrawn | expired
completed —new memo→ superseded
L0/L1 extra: completed login/elevate → session.active → configure hats → session.ended
```
Core invariants (I1I13) live in `decision-memo-schema.md`. The ones that must not be bargained away:
- One question per memo.
- Binding acts reference a presentation of the same version.
- AES/QES dynamically links `view_hash`.
- Required highlights must be acked before bind.
- Agents may draft, not bind.
- Return always carries structured reasons.
- Highlights never shrink the legal object.
- `binding` is signed; `awareness` is not, unless `promote` copied named fields into `awareness_promoted`.
- Hats are not scopes. `requires_new_bind` or `hat.elevates` means a new memo (or promote + new version).
---
## 7. Pre-sign vs post-sign (login / multi-tenant)
The login case forced a split that applies everywhere.
**Pre-sign / binding slice** (editable, signed)
- which identity you log in as (ordinary vs break-glass)
- which scope/tenant/system you enter
- privileges granted *at the gate*
- monitoring and consent terms
**Awareness slice** (shown on the same surface, hashed separately, not signed)
- proposed hat after login (`last_used` | `system_default` | `policy` | `explicit`)
- other hats available inside that bound scope
- other tenants you support (orientation)
- last session summary
**Post-sign / session**
- `configure(hat)` if `elevates=false`
- switching to a tenant with `requires_new_bind=true` opens a child memo
- an elevating hat (`Payroll Admin`) cannot be configured; it must be promoted into binding or issued as its own elevation memo
Banner sketch:
```
PRE-SIGN (bound)
Identity [ Bernd Worsch ▾ ]
Scope [ ACME Corp / Payroll-Prod ▾ ]
AWARENESS (not signed)
After login you will wear Finance Controller
source: last used, Tue 18:12 CEST
Other hats: Auditor · Payroll Admin (needs new bind)
Other tenants: Beta GmbH (needs new bind)
```
Defaulting the hat to last-used is required for situational awareness and forbidden from silently entering `view_hash`.
---
## 8. State transitions (summary)
Actors: requester, current-step assignee, any party, session subject, system, agent.
Overlay verbs always available in circulation: `comment`, `discuss`. Requester may `withdraw` before terminal. System `stamp`s evidence.
| Verb | Weak step (`inform`/`review`) | Co-sign | Bind step |
|---|---|---|---|
| return / forward / escalate | yes | yes | yes |
| acknowledge | completes step | completes step | overlay |
| accept | **illegal** | completes step | may complete memo |
| decline | illegal | completes step/memo | completes memo `declined` |
| configure | illegal until `session.active` | same | same |
`accept` on Kenntnisnahme is illegal on purpose.
After login/elevate bind:
- `configure(hat)` allowed only for non-elevating hats in `available_hats`
- other-tenant with `requires_new_bind``scope.rebind_required` + child memo
- binding verbs on a sealed version are illegal
Full tables, guards (`G_PRES`, `G_ACK`, `G_MAND`, `G_HAT`, …), and illegal combinations: `state-transitions.md`.
---
## 9. Canonicalization (summary)
Two hashes on every presentation.
| Hash | Input | Signed? |
|---|---|---|
| `view_hash` | binding document | yes, from organizational level up |
| `awareness_hash` | awareness document | no, unless promote ran |
Profile: JCS-inspired restricted JSON — UTF-8, sorted keys, no insignificant whitespace, integers only, unknown keys stripped, nulls dropped.
`view_hash` allow-list: `memo_id`, `memo_version`, `question`, `requested_act`, `binding_level`, `brief`, `locale`, `ui_release`, normalized `packet`, normalized `highlights`, `binding`, optional `awareness_promoted`.
`awareness_hash` allow-list: proposed hat + source, available hats/scopes, last session, situation note.
```
canonical = dumps(document)
hash = SHA-256(UTF-8(canonical))
```
Reference: `canonicalize.py`. Spec: `canonicalize.md`.
Computed vectors (`vectors/expected.json`):
```
login view_hash 492d9d311bf44ec9de0d0abef28abac7d31df2781527e8fe276a3186ee1b06b8
login awareness_hash 2be7742970a01e7a879ae5040660659fb8c9a5c024e6c7fb338f3944a4fe05d1
ADR-0042 view_hash 1c89ec07c3cc9d16f85a1ba1be5169456b3c55d21161f64037787779ae91f202
```
Isolation that must stay green:
1. Shuffling object keys does not change hashes.
2. Editing awareness fields does not change `view_hash`.
3. Changing `binding.target` ACME → Beta does change `view_hash`.
4. Selecting a hat after login emits `session.hat_selected` and does not rewrite `view_hash`.
AES/QES signed attributes carry `memo_id`, `memo_version`, `disposition.verb`, `presentation.view_hash`. They do not carry `awareness_hash` unless promote ran.
---
## 10. Open questions (still open)
1. Beachhead buyer: German public-sector *Schriftgut*, platform-eng ADR+PAM, or mid-market contract?
2. Product vs protocol? Long game is both (Decision Memo + Evidence Bundle + Disposition verbs in front of IdP, Git, and e-sign).
3. How to evidence “informed” without surveillance theater — hash of presented view + explicit highlight ack, not keystroke analytics.
4. Mandate graph: without a living map of who may bind what, escalate is just forwarding.
5. Liability of highlights: legally the whole instrument is bound; the UI must make that unmistakable.
6. Retention / qualified archive for L4+.
7. Name: keep `informed-decision`; use *Umlaufmappe* / *Zeichnungsbuch* in DACH language.
---
## 11. Suggested next build slice
Do not start at L5. Ship a thin Decision Memo runtime that already has the split hashes:
1. Embeddable L0 login/elevation component in front of an IdP or PAM.
2. L2 ADR accept with comment / return / escalate / accept and AES-optional seal.
3. One single-jurisdiction contract path.
If those three share this schema, the transitions, and the two hashes, the vision is real. If they fork, it is not.

View file

@ -0,0 +1,184 @@
# Canonicalization algorithm v0.2
Two hashes exist on every presentation.
| Hash | Input | Enters the signature? |
|---|---|---|
| `view_hash` | binding document | yes, for every binding verb at `organizational` and above |
| `awareness_hash` | awareness document | no, unless a `promote` disposition copied named fields into `awareness_promoted` |
That split is the whole point of pre-sign vs post-sign.
Reference implementation: `canonicalize.py`.
## 1. Profile
Restricted JCS (RFC 8785 subset):
1. UTF-8.
2. No insignificant whitespace.
3. Object keys sorted by raw UTF-8 bytes (all keys in this spec are ASCII).
4. Arrays keep author order except where this spec sorts (`packet` by `item_id`, `highlights` by `id`, hats/scopes/identities by `id`, permission lists lexicographically).
5. Numbers are integers only. Timestamps stay strings (`YYYY-MM-DDTHH:MM:SSZ`).
6. Strings use RFC 8259 escaping; U+0000U+001F as `\u00xx`.
7. Drop keys whose value is `null`. Keep empty arrays.
8. Unknown keys are stripped. Adding a field to the live memo does not change a hash until it is added to the allow-list below.
## 2. Binding document (`view_hash`)
Allow-list, then normalize:
```
memo_id
memo_version
question
requested_act
binding_level
brief
locale
ui_release
packet[] → {item_id, hash} sorted by item_id
highlights[] → {id, item_id, locator, required_ack, severity} sorted by id
binding → BindingSlice (pre-sign identity + scope)
awareness_promoted → only fields explicitly promoted into the bind
```
`binding` allow-list:
```
principal
available_identities[] sorted by id
target the scope this act enters
available_bind_scopes[] scopes choosable as *this* bind, sorted by id
granted_at_bind roles/permissions sorted
justification
blast_radius
terms
```
Hats, last-used role, other-tenant orientation, situation notes **do not appear here**.
`hash` form is always `alg:hex` (`sha256:…`).
Then:
```
canonical = dumps(binding_document)
view_hash = SHA-256(UTF-8(canonical))
```
`dumps` is the JCS-subset serializer in `canonicalize.py`.
## 3. Awareness document (`awareness_hash`)
Allow-list:
```
memo_id
memo_version
locale
ui_release
proposed_hat
proposed_hat_source system_default | last_used | policy | explicit | inferred
available_hats[] sorted by id
available_scopes[] sorted by id (orientation only)
last_session
situation_note
```
Same `dumps` + SHA-256.
## 4. Dynamic linking
A binding disposition stores both hashes on the presentation and signs only `view_hash`.
Signed attributes (AES/QES) MUST contain at least:
```
memo_id
memo_version
disposition.verb
presentation.view_hash
```
They MUST NOT contain `awareness_hash` unless `promote` ran.
## 5. Promote
`promote` copies named awareness fields into `binding.awareness_promoted` on a **new memo version**. Typical case: an elevating hat (`hat.elevates=true`) must become part of the bind, or a tenant that looked like a hat is actually a bind-scope.
After promote:
- new `view_hash`
- previous presentations invalid
- route restarts or current step is re-presented
## 6. Test vectors
Computed with `canonicalize.py`.
### V1 — login binding (identity + tenant)
Input: `vectors/login-binding.json`
`view_hash.hex` =
```
492d9d311bf44ec9de0d0abef28abac7d31df2781527e8fe276a3186ee1b06b8
```
What is inside: chosen principal, break-glass identity as an *available bind identity*, target tenant ACME, sibling tenant Beta as an *available bind-scope*, gate privileges `authenticated` + `session.create`, monitoring terms.
What is not inside: Finance Controller hat, last session, Payroll Admin.
### V2 — login awareness (hats + last session)
Input: `vectors/login-awareness.json`
`awareness_hash.hex` =
```
2be7742970a01e7a879ae5040660659fb8c9a5c024e6c7fb338f3944a4fe05d1
```
Proposed hat `hat:finance-controller` source `last_used`. Payroll Admin is listed with `elevates=true` so the UI can show it and the state machine can refuse `configure` on it.
### V3 — ADR accept
Input: `vectors/adr-binding.json`
`view_hash.hex` =
```
1c89ec07c3cc9d16f85a1ba1be5169456b3c55d21161f64037787779ae91f202
```
### Isolation checks the implementation must keep green
1. Shuffling object keys in the input JSON does not change either hash.
2. Adding or changing awareness fields on a combined object does not change `view_hash`.
3. Changing `binding.target.id` from `tenant:acme` to `tenant:beta` **does** change `view_hash` (that is a different login).
4. Changing only `proposed_hat` changes `awareness_hash` only.
5. Selecting `hat:finance-controller` after login emits `session.hat_selected` and does not rewrite `view_hash`.
## 7. UI mapping (login)
```
┌─────────────────────────────────────────────────────────┐
│ PRE-SIGN (editable, bound) │
│ Identity [ Bernd Worsch ▾ ] │
│ Scope [ ACME Corp / Payroll-Prod ▾ ] │
│ At gate authenticated · session.create │
│ Terms sessions recorded │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ AWARENESS (shown, not signed) │
│ After login you will wear │
│ Finance Controller (last used, Tue 18:12) │
│ other hats: Auditor · Payroll Admin (needs new bind) │
│ Other tenants you support: Beta GmbH (needs new bind) │
└─────────────────────────────────────────────────────────┘
[ Switch identity ] [ Change tenant ] [ Proceed ]
```
Proceed signs V1. After session start, hat dropdown is `configure`. Tenant dropdown to Beta opens a child memo.

View file

@ -0,0 +1,292 @@
#!/usr/bin/env python3
"""Canonicalization for informed-decision view_hash and awareness_hash.
Profile: JCS-inspired restricted JSON (RFC 8785 subset).
- UTF-8
- objects with keys sorted by UTF-8 code units (equivalent to UTF-16 for our ASCII keys)
- no insignificant whitespace
- integers only (no floats)
- strings escaped as in RFC 8259 / JCS
"""
from __future__ import annotations
import hashlib
import json
from typing import Any
ALLOWED_BINDING_KEYS = (
"awareness_promoted",
"binding",
"binding_level",
"brief",
"highlights",
"locale",
"memo_id",
"memo_version",
"packet",
"question",
"requested_act",
"ui_release",
)
ALLOWED_AWARENESS_KEYS = (
"available_hats",
"available_scopes",
"last_session",
"locale",
"memo_id",
"memo_version",
"proposed_hat",
"proposed_hat_source",
"situation_note",
"ui_release",
)
BINDING_SLICE_KEYS = (
"available_bind_scopes",
"available_identities",
"blast_radius",
"granted_at_bind",
"justification",
"principal",
"target",
"terms",
)
def _is_int(n: Any) -> bool:
return isinstance(n, int) and not isinstance(n, bool)
def escape_string(s: str) -> str:
out = ['"']
for ch in s:
o = ord(ch)
if ch == '"':
out.append('\\"')
elif ch == "\\":
out.append("\\\\")
elif ch == "\b":
out.append("\\b")
elif ch == "\f":
out.append("\\f")
elif ch == "\n":
out.append("\\n")
elif ch == "\r":
out.append("\\r")
elif ch == "\t":
out.append("\\t")
elif o < 0x20:
out.append(f"\\u{o:04x}")
else:
out.append(ch)
out.append('"')
return "".join(out)
def dumps(value: Any) -> str:
if value is None:
return "null"
if value is True:
return "true"
if value is False:
return "false"
if _is_int(value):
return str(value)
if isinstance(value, str):
return escape_string(value)
if isinstance(value, list):
return "[" + ",".join(dumps(v) for v in value) + "]"
if isinstance(value, dict):
items = []
for key in sorted(value.keys()):
if not isinstance(key, str):
raise TypeError("object keys must be strings")
items.append(escape_string(key) + ":" + dumps(value[key]))
return "{" + ",".join(items) + "}"
raise TypeError(f"unsupported type for canonicalization: {type(value)!r}")
def pick(obj: dict, allowed: tuple[str, ...]) -> dict:
return {k: obj[k] for k in allowed if k in obj}
def strip_nulls(value: Any) -> Any:
if isinstance(value, dict):
return {k: strip_nulls(v) for k, v in value.items() if v is not None}
if isinstance(value, list):
return [strip_nulls(v) for v in value]
return value
def normalize_hash(h: Any) -> str:
if isinstance(h, str):
return h if ":" in h else f"sha256:{h}"
if isinstance(h, dict):
return f"{h['alg']}:{h['hex']}"
raise TypeError("hash must be string or {alg,hex}")
def normalize_packet(packet: list) -> list:
items = []
for item in packet:
items.append(
{
"hash": normalize_hash(item["hash"]),
"item_id": item["item_id"],
}
)
items.sort(key=lambda i: i["item_id"])
return items
def normalize_highlights(highlights: list) -> list:
items = []
for hl in highlights:
loc = hl.get("locator") or {}
items.append(
{
"id": hl["id"],
"item_id": hl["item_id"],
"locator": strip_nulls(loc),
"required_ack": bool(hl.get("required_ack", False)),
"severity": hl.get("severity", "informational"),
}
)
items.sort(key=lambda i: i["id"])
return items
def normalize_party(party: dict) -> dict:
out = {
"display_name": party["display_name"],
"id": party["id"],
"kind": party["kind"],
}
if party.get("identifiers"):
ids = []
for ident in party["identifiers"]:
ids.append({"scheme": ident["scheme"], "value": ident["value"]})
ids.sort(key=lambda i: (i["scheme"], i["value"]))
out["identifiers"] = ids
if party.get("role"):
out["role"] = party["role"]
return out
def normalize_scope(scope: dict) -> dict:
out = {
"id": scope["id"],
"kind": scope["kind"],
"label": scope["label"],
}
if "environment" in scope:
out["environment"] = scope["environment"]
if "requires_new_bind" in scope:
out["requires_new_bind"] = bool(scope["requires_new_bind"])
return out
def normalize_hat(hat: dict) -> dict:
out = {"id": hat["id"], "label": hat["label"]}
if hat.get("kind"):
out["kind"] = hat["kind"]
if "elevates" in hat:
out["elevates"] = bool(hat["elevates"])
if hat.get("permissions_preview"):
out["permissions_preview"] = sorted(hat["permissions_preview"])
if hat.get("scope_id"):
out["scope_id"] = hat["scope_id"]
return out
def normalize_binding(binding: dict) -> dict:
raw = pick(binding, BINDING_SLICE_KEYS)
out: dict[str, Any] = {}
if "principal" in raw:
out["principal"] = normalize_party(raw["principal"])
if raw.get("available_identities"):
ids = [normalize_party(p) for p in raw["available_identities"]]
ids.sort(key=lambda p: p["id"])
out["available_identities"] = ids
if "target" in raw:
out["target"] = normalize_scope(raw["target"])
if raw.get("available_bind_scopes"):
scopes = [normalize_scope(s) for s in raw["available_bind_scopes"]]
scopes.sort(key=lambda s: s["id"])
out["available_bind_scopes"] = scopes
if raw.get("granted_at_bind"):
g = dict(raw["granted_at_bind"])
if g.get("roles"):
g["roles"] = sorted(g["roles"])
if g.get("permissions"):
g["permissions"] = sorted(g["permissions"])
out["granted_at_bind"] = strip_nulls(g)
for k in ("justification", "blast_radius", "terms"):
if k in raw:
out[k] = strip_nulls(raw[k])
return out
def binding_document(src: dict) -> dict:
doc = pick(src, ALLOWED_BINDING_KEYS)
if "packet" in doc:
doc["packet"] = normalize_packet(doc["packet"])
if "highlights" in doc:
doc["highlights"] = normalize_highlights(doc["highlights"])
if "binding" in doc:
doc["binding"] = normalize_binding(doc["binding"])
if "awareness_promoted" in doc:
doc["awareness_promoted"] = strip_nulls(doc["awareness_promoted"])
return strip_nulls(doc)
def awareness_document(src: dict) -> dict:
doc = pick(src, ALLOWED_AWARENESS_KEYS)
if doc.get("proposed_hat"):
doc["proposed_hat"] = normalize_hat(doc["proposed_hat"])
if doc.get("available_hats"):
hats = [normalize_hat(h) for h in doc["available_hats"]]
hats.sort(key=lambda h: h["id"])
doc["available_hats"] = hats
if doc.get("available_scopes"):
scopes = [normalize_scope(s) for s in doc["available_scopes"]]
scopes.sort(key=lambda s: s["id"])
doc["available_scopes"] = scopes
return strip_nulls(doc)
def sha256_hex(canonical: str) -> str:
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
def view_hash(src: dict) -> dict:
canonical = dumps(binding_document(src))
return {
"alg": "sha256",
"hex": sha256_hex(canonical),
"canonical": canonical,
}
def awareness_hash(src: dict) -> dict:
canonical = dumps(awareness_document(src))
return {
"alg": "sha256",
"hex": sha256_hex(canonical),
"canonical": canonical,
}
if __name__ == "__main__":
import pathlib
import sys
path = pathlib.Path(sys.argv[1])
data = json.loads(path.read_text())
kind = sys.argv[2] if len(sys.argv) > 2 else "view"
result = view_hash(data) if kind == "view" else awareness_hash(data)
print(result["canonical"])
print(result["hex"])

View file

@ -0,0 +1,149 @@
# Decision Memo object schema v0.2
Machine-readable schema: `decision-memo.schema.json`
Worked example: `decision-memo.example.json`
The Decision Memo is one object at every depth (L0 login banner through L5 multi-party instrument). Depth changes which fields are populated, not which types exist.
## 1. Object graph
```
DecisionMemo
├── requester / subject / parties[] identity + mandate
├── question + requested_act + binding_level what is being asked
├── brief + consequences
├── identity_context.binding PRE-SIGN identity + scope (in view_hash)
├── identity_context.awareness hats, last-used, other tenants (awareness_hash)
├── session POST-SIGN live hat inside the bound scope
├── packet[] files, hashed
├── highlights[] pointers into packet
├── route.steps[] Laufweg / Zeichnungsleiste
├── thread[] discussion stays on the object
├── presentations[] what each actor was shown
├── dispositions[] comment | return | forward | accept…
├── relations[] supersedes / relates_to
└── evidence hash-chained event log + optional seal
```
## 2. Lifecycle
```
draft
→ submitted
→ circulating / awaiting_actor / in_discussion
→ returned ──(new version)──→ submitted
→ completed (outcome set)
→ withdrawn | expired
completed ─(new memo)→ superseded
```
Rules:
- Edits in `draft` bump `version` without a new `id`.
- After `submitted`, content edits that change `brief`, `packet`, or `highlights` **must** bump `version` and invalidate unused presentations.
- `return` sets status `returned`, does not mutate the sealed packet in place. Requester opens version N+1, which `supersedes` the circulation of N.
- `forward` replaces the current step assignee. `escalate` adds or replaces a step of higher competence.
- Terminal statuses: `completed`, `withdrawn`, `expired`, `superseded`.
- `outcome` is only set when `status=completed` (or timed-out/cancelled mappings).
## 3. Invariants
I1. **One question.** `question` is a single present-tense sentence. If you need two questions, you need two memos.
I2. **Presentation binds the act.** Every `disposition` with verb in `{acknowledge, accept, decline, abstain, recuse, sign}` MUST reference a `presentation_id` whose `memo_version` equals the memo version at act time.
I3. **Dynamic linking.** Binding acts at `aes` or higher MUST set `signature.presentation_bound=true` and include `presentation.view_hash` in the signed attributes.
I4. **Required highlights.** If any highlight has `required_ack=true`, a binding act is invalid unless those ids are in `presentation.acked_highlight_ids`.
I5. **Mandate fit.** The actor on a `schlusszeichnung` / `sign` step MUST carry a `mandate` whose scope covers `requested_act` and any `legal` / value constraints.
I6. **Packet integrity.** `packet[].hash` is content-addressed. Replacing bytes is a new `item.id` (or a new item version + `replaces`).
I7. **Evidence is append-only.** `evidence.events[].event_hash = H(seq || type || at || payload_hash || prev_hash)`. `head_hash` is the last `event_hash`.
I8. **Agents may draft, not bind.** `party.kind=agent` may create highlights and brief text. Agents MUST NOT emit binding verbs.
I9. **Return is data.** `verb=return` requires `return.reasons[]`. Empty “please fix” is invalid.
I10. **Whole instrument vs excerpt.** Highlights never narrow the legal object. `legal` and the primary packet item define what is bound; highlights are attention, not scope.
I11. **Pre-sign vs post-sign.** `identity_context.binding` is in `view_hash`. `identity_context.awareness` is only in `awareness_hash`. Binding verbs sign the former. `configure` mutates `session` after bind and never rewrites `binding`.
I12. **Hats are not scopes.** A hat (`elevates=false`) is a perspective inside an already bound scope. A scope with `requires_new_bind=true` (other tenant, other identity, elevating hat) is a new memo.
I13. **Defaults are awareness.** `proposed_hat_source` of `last_used` or `system_default` must be shown and must not be silently copied into `view_hash`.
## 4. Disposition verbs
| Verb | Changes route? | Binding? | Required payload |
|---|---|---|---|
| comment | no | no | body |
| discuss | may set `in_discussion` | no | body + optional transfer.to for requester |
| return | yes → requester | no | return.reasons |
| forward | yes → peer assignee | no | transfer.to |
| escalate | yes → higher mandate | no | transfer.to + reason |
| acknowledge | completes inform/review step | weak | presentation_id |
| accept | may complete memo | yes | presentation_id + optional signature |
| decline | may complete memo | yes | presentation_id + body |
| abstain / recuse | completes step for actor | yes | presentation_id + reason |
| withdraw | terminal | requester only | body |
| stamp | no | system/org seal | signature on listed items |
## 5. Canonical `view_hash`
Serialize, UTF-8, JCS (RFC 8785), then hash:
```json
{
"memo_id": "...",
"memo_version": 2,
"question": "...",
"requested_act": "accept",
"binding_level": "aes",
"brief": "...",
"packet": [{"item_id":"...","hash":"sha256:..."}],
"highlights": [{"id":"...","item_id":"...","locator":{...},"required_ack":true}],
"ui_release": "informed-decision@0.3.1",
"locale": "en"
}
```
That document is what “informed” means in the evidence bundle.
## 6. Evidence event minimum set
For a completed accept:
1. `memo.created`
2. `memo.submitted`
3. `packet.attached` (per item)
4. `highlight.added` (per required highlight)
5. `route.started`
6. `identity.authenticated`
7. `presentation.opened`
8. `presentation.highlight_acked`
9. `disposition.recorded` (accept)
10. `signature.created` (if aes/qes)
11. `timestamp.qualified` (if qes / long retention)
12. `file.stamped` (per `is_to_be_stamped`)
13. `bundle.sealed`
## 7. Field usage by depth
| Field | L0 login | L1 elevate | L2 ADR | L3 approval | L4 contract | L5 treaty |
|---|---|---|---|---|---|---|
| identity_context | required | required | optional | optional | rare | rare |
| packet | optional | ticket + change | ADR + links | memo + annexes | contract + annexes | instruments + translations |
| highlights | optional | blast-radius | rollback / cost | risk spans | clauses | reservations |
| binding_level | acknowledgment / organizational | organizational | organizational / aes | organizational / aes | aes / qes | qes per party |
| route | one step | requester → approver | review → accept | matrix | sequential/parallel signers | hybrid + deposit |
| legal.form_requirement | none | none | none | none / textform | textform / schriftform | varies by party |
## 8. What is deliberately not in v0.1
- Full mandate-graph resolution (referenced by `mandate.policy_ref`, not modelled here)
- Payment / consideration objects
- Translation alignment and reservation semantics for L5
- Raw PII in evidence (use `ip_hash`, identifier schemes)
- UI widget tree — only `ui_release` + canonical view

View file

@ -0,0 +1,150 @@
{
"id": "01K4Q8Z3R7V2N6K9M1P5T8W4XC",
"version": 2,
"schema_version": "0.2.0",
"status": "awaiting_actor",
"depth": 2,
"title": "Accept ADR-0042: event-sourced billing ledger",
"question": "Do you formally accept ADR-0042 as the billing architecture for prod?",
"requested_act": "accept",
"binding_level": "aes",
"locale": "en",
"created_at": "2026-09-08T14:02:11Z",
"updated_at": "2026-09-09T07:18:44Z",
"submitted_at": "2026-09-08T15:10:00Z",
"due_at": "2026-09-12T16:00:00Z",
"requester": {
"id": "01K4Q8A1B2C3D4E5F6G7H8J9K0",
"kind": "person",
"display_name": "Lea Hoffmann",
"role": "staff engineer",
"identifiers": [
{ "scheme": "email", "value": "lea.hoffmann@example.com" },
{ "scheme": "idp:oidc-sub", "value": "auth.example.com|a8f21c" }
]
},
"parties": [
{
"id": "01K4Q8MANDATE000000000001",
"kind": "person",
"display_name": "Bernd Worsch",
"role": "principal engineer / ADR acceptor",
"mandate": {
"id": "01K4Q8MANDATE00000000000A",
"scope": "Accept architecture decision records for billing and payments",
"policy_ref": "https://policy.example.com/adr-acceptance"
}
}
],
"brief": "Replace nightly batch invoicing with an append-only event ledger. Replay is the source of truth for invoices. Dual-run for 60 days. Rollback: freeze the ledger and revert the read model to the last nightly snapshot.",
"consequences": "On-call owns replay tooling. Finance reporting moves from SQL warehouse tables to a projection job. No in-place updates of posted invoices.",
"packet": [
{
"id": "01K4Q8DOC0000000000000001",
"role": "primary",
"filename": "ADR-0042.md",
"media_type": "text/markdown",
"language": "en",
"bytes": 12880,
"hash": { "alg": "sha256", "hex": "6b1c0f8a9d2e4c7b8a1f0e3d5c6b7a8f9e0d1c2b3a4f5e6d7c8b9a0f1e2d3c4b" },
"storage_uri": "s3://decisions/adr/0042.md",
"version": 2,
"is_to_be_signed": true,
"label": "ADR-0042"
}
],
"highlights": [
{
"id": "01K4Q8HL00000000000000001",
"item_id": "01K4Q8DOC0000000000000001",
"severity": "critical",
"locator": {
"kind": "markdown_heading",
"heading": "Consequences / rollback"
},
"title": "Rollback is operationally expensive",
"note": "Accepting this ADR commits on-call to ledger freeze drills before go-live.",
"required_ack": true,
"author_id": "01K4Q8A1B2C3D4E5F6G7H8J9K0",
"created_at": "2026-09-08T14:20:00Z",
"source": "requester"
}
],
"route": {
"mode": "sequential",
"steps": [
{
"id": "01K4Q8STEP000000000000001",
"ordinal": 1,
"kind": "review",
"assignee": {
"id": "01K4Q8MANDATE000000000002",
"kind": "role",
"display_name": "Billing domain reviewer"
},
"required_verbs": ["acknowledge"],
"status": "completed",
"completed_at": "2026-09-08T17:40:00Z"
},
{
"id": "01K4Q8STEP000000000000002",
"ordinal": 2,
"kind": "schlusszeichnung",
"assignee": {
"id": "01K4Q8MANDATE000000000001",
"kind": "person",
"display_name": "Bernd Worsch"
},
"required_verbs": ["accept", "return", "escalate"],
"min_binding_level": "aes",
"status": "active",
"due_at": "2026-09-12T16:00:00Z"
}
],
"current_step_ids": ["01K4Q8STEP000000000000002"]
},
"thread": [
{
"id": "01K4Q8MSG0000000000000001",
"author_id": "01K4Q8MANDATE000000000002",
"body": "Rollback drill date is missing. Acceptable if added before go-live.",
"created_at": "2026-09-08T17:39:12Z"
}
],
"presentations": [
{
"id": "01K4Q8PRES000000000000001",
"actor_id": "01K4Q8MANDATE000000000001",
"memo_version": 2,
"opened_at": "2026-09-09T07:10:00Z",
"ui_release": "informed-decision@0.3.1",
"locale": "en",
"viewport": "brief",
"packet_hashes": [
{
"item_id": "01K4Q8DOC0000000000000001",
"hash": { "alg": "sha256", "hex": "6b1c0f8a9d2e4c7b8a1f0e3d5c6b7a8f9e0d1c2b3a4f5e6d7c8b9a0f1e2d3c4b" }
}
],
"highlight_ids": ["01K4Q8HL00000000000000001"],
"highlight_set_hash": { "alg": "sha256", "hex": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" },
"brief_hash": { "alg": "sha256", "hex": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" },
"view_hash": { "alg": "sha256", "hex": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" },
"acked_highlight_ids": ["01K4Q8HL00000000000000001"],
"auth_context": {
"method": "passkey",
"assurance": "substantial",
"idp": "https://auth.example.com"
}
}
],
"dispositions": [],
"relations": [
{ "kind": "supersedes", "to": "01K4Q8OLDADR0000000000001", "note": "Supersedes ADR-0011 batch invoicing" }
],
"legal": {
"form_requirement": "none",
"retention_years": 10,
"classification": "internal"
}
}

View file

@ -0,0 +1,153 @@
{
"id": "01K4LOGIN00000000000000001",
"version": 1,
"schema_version": "0.2.0",
"status": "awaiting_actor",
"depth": 0,
"title": "Login to Payroll-Prod / ACME",
"question": "Log into Payroll-Prod as Bernd Worsch in tenant ACME?",
"requested_act": "login",
"binding_level": "organizational",
"locale": "en",
"created_at": "2026-09-09T08:01:00Z",
"updated_at": "2026-09-09T08:01:00Z",
"requester": {
"id": "01K4SYSTEM00000000000000001",
"kind": "system",
"display_name": "Payroll-Prod IdP"
},
"subject": {
"id": "01K4PERSONBERND00000000001",
"kind": "person",
"display_name": "Bernd Worsch"
},
"brief": "You are entering Payroll-Prod. Sessions are recorded. Privileges at the gate are identity-scoped, not hat-scoped.",
"identity_context": {
"system": "Payroll-Prod",
"environment": "prod",
"binding": {
"principal": {
"id": "01K4PERSONBERND00000000001",
"kind": "person",
"display_name": "Bernd Worsch",
"role": "employee",
"identifiers": [
{ "scheme": "email", "value": "bernd.worsch@example.com" },
{ "scheme": "idp:oidc-sub", "value": "auth.example.com|bernd" }
]
},
"available_identities": [
{
"id": "01K4PERSONBERND00000000001",
"kind": "person",
"display_name": "Bernd Worsch"
},
{
"id": "01K4PERSONBERNDADMIN000001",
"kind": "person",
"display_name": "Bernd Worsch (break-glass)"
}
],
"target": {
"kind": "tenant",
"id": "tenant:acme",
"label": "ACME Corp",
"environment": "prod",
"requires_new_bind": true
},
"available_bind_scopes": [
{
"kind": "tenant",
"id": "tenant:acme",
"label": "ACME Corp",
"environment": "prod",
"requires_new_bind": true
},
{
"kind": "tenant",
"id": "tenant:beta",
"label": "Beta GmbH",
"environment": "prod",
"requires_new_bind": true
}
],
"granted_at_bind": {
"roles": ["authenticated"],
"permissions": ["session.create"]
},
"terms": {
"monitoring": true,
"consent_code": "LOGIN-PROD-2026"
}
},
"awareness": {
"proposed_hat": {
"id": "hat:finance-controller",
"label": "Finance Controller",
"kind": "access_profile",
"elevates": false,
"scope_id": "tenant:acme"
},
"proposed_hat_source": "last_used",
"available_hats": [
{
"id": "hat:finance-controller",
"label": "Finance Controller",
"kind": "access_profile",
"elevates": false
},
{
"id": "hat:auditor-readonly",
"label": "Auditor (read-only)",
"kind": "perspective",
"elevates": false
},
{
"id": "hat:payroll-admin",
"label": "Payroll Admin",
"kind": "role",
"elevates": true
}
],
"available_scopes": [
{
"kind": "tenant",
"id": "tenant:beta",
"label": "Beta GmbH",
"environment": "prod",
"requires_new_bind": true
}
],
"last_session": {
"ended_at": "2026-09-08T16:12:00Z",
"hat_id": "hat:finance-controller",
"scope_id": "tenant:acme"
},
"situation_note": "Last session Tuesday 18:12 CEST as Finance Controller in ACME."
}
},
"packet": [],
"highlights": [],
"route": {
"mode": "sequential",
"steps": [
{
"id": "01K4LOGINSTEP0000000000001",
"ordinal": 1,
"kind": "acknowledge",
"assignee": {
"id": "01K4PERSONBERND00000000001",
"kind": "person",
"display_name": "Bernd Worsch"
},
"required_verbs": ["accept", "decline"],
"min_binding_level": "organizational",
"status": "active"
}
],
"current_step_ids": ["01K4LOGINSTEP0000000000001"]
},
"session": {
"status": "pending"
}
}

View file

@ -0,0 +1,910 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://informed-decision.dev/schema/decision-memo/v0.2",
"title": "Informed-Decision Decision Memo",
"description": "Portable decision object: context, circulation, disposition, and cryptographic evidence of an informed identity-bound act.",
"$defs": {
"uri": { "type": "string", "format": "uri" },
"datetime": { "type": "string", "format": "date-time" },
"ulid": {
"type": "string",
"pattern": "^[0-9A-HJKMNP-TV-Z]{26}$",
"description": "ULID. UUID v7 is also acceptable at the API boundary."
},
"sha256": {
"type": "string",
"pattern": "^sha256:[a-f0-9]{64}$"
},
"contentHash": {
"type": "object",
"additionalProperties": false,
"required": ["alg", "hex"],
"properties": {
"alg": { "type": "string", "enum": ["sha256", "sha384", "sha3-256"] },
"hex": { "type": "string", "pattern": "^[a-f0-9]{64,96}$" }
}
},
"locale": { "type": "string", "pattern": "^[a-z]{2}(-[A-Z]{2})?$" },
"markdown": { "type": "string", "maxLength": 100000 },
"shortText": { "type": "string", "minLength": 1, "maxLength": 280 },
"mediumText": { "type": "string", "maxLength": 4000 },
"bindingLevel": {
"type": "string",
"description": "How strongly the resulting act is meant to bind.",
"enum": [
"none",
"acknowledgment",
"organizational",
"ses",
"aes",
"qes",
"qeseal"
]
},
"assuranceLevel": {
"type": "string",
"enum": ["low", "substantial", "high"]
},
"depthLevel": {
"type": "integer",
"minimum": 0,
"maximum": 5,
"description": "L0 login … L5 multi-party instrument. Advisory; schema is shared."
},
"requestedAct": {
"type": "string",
"enum": [
"login",
"elevate",
"acknowledge",
"approve",
"accept",
"decline",
"sign",
"seal",
"countersign",
"ratify"
]
},
"memoStatus": {
"type": "string",
"enum": [
"draft",
"submitted",
"circulating",
"awaiting_actor",
"in_discussion",
"returned",
"completed",
"withdrawn",
"expired",
"superseded"
]
},
"outcome": {
"type": "string",
"enum": [
"accepted",
"declined",
"acknowledged",
"abstained",
"recused",
"cancelled",
"timed_out"
]
},
"dispositionVerb": {
"type": "string",
"enum": [
"comment",
"discuss",
"return",
"forward",
"escalate",
"acknowledge",
"accept",
"decline",
"abstain",
"recuse",
"withdraw",
"stamp",
"configure",
"promote"
]
},
"sessionStatus": {
"type": "string",
"enum": ["none", "pending", "active", "ended"]
},
"awarenessSource": {
"type": "string",
"enum": ["system_default", "last_used", "policy", "explicit", "inferred"]
},
"scopeKind": {
"type": "string",
"enum": ["system", "tenant", "workspace", "resource", "privilege_set", "environment"]
},
"partyKind": {
"type": "string",
"enum": ["person", "org", "role", "group", "system", "agent"]
},
"routeMode": {
"type": "string",
"enum": ["sequential", "parallel", "hybrid"]
},
"stepKind": {
"type": "string",
"enum": [
"inform",
"comment",
"review",
"mitzeichnung",
"schlusszeichnung",
"acknowledge",
"approve",
"sign",
"seal",
"deposit"
]
},
"documentRole": {
"type": "string",
"enum": [
"brief",
"primary",
"annex",
"supporting",
"prior_decision",
"diff",
"translation",
"evidence_export"
]
},
"highlightSeverity": {
"type": "string",
"enum": ["critical", "material", "informational"]
},
"evidenceEventType": {
"type": "string",
"enum": [
"memo.created",
"memo.updated",
"memo.submitted",
"memo.withdrawn",
"memo.superseded",
"memo.expired",
"packet.attached",
"packet.detached",
"highlight.added",
"highlight.updated",
"route.started",
"route.step_assigned",
"route.step_completed",
"route.returned",
"route.forwarded",
"route.escalated",
"presentation.opened",
"presentation.viewed",
"presentation.highlight_acked",
"identity.authenticated",
"identity.step_up",
"disposition.recorded",
"signature.created",
"seal.created",
"timestamp.qualified",
"file.stamped",
"bundle.sealed",
"awareness.shown",
"session.started",
"session.configured",
"session.hat_selected",
"session.ended",
"scope.rebind_required"
]
},
"identifier": {
"type": "object",
"additionalProperties": false,
"required": ["scheme", "value"],
"properties": {
"scheme": {
"type": "string",
"description": "idp:oidc-sub | eudi:pid | eidas:person | email | employee_id | lei | orga_id | did"
},
"value": { "type": "string", "minLength": 1, "maxLength": 1024 },
"issuing_authority": { "type": "string" }
}
},
"mandate": {
"type": "object",
"additionalProperties": false,
"required": ["id", "scope"],
"properties": {
"id": { "$ref": "#/$defs/ulid" },
"scope": {
"type": "string",
"description": "Human-readable competence: 'CFO contracts ≤ 250k EUR', 'prod-admin JIT', 'ADR acceptor'."
},
"policy_ref": { "$ref": "#/$defs/uri" },
"max_value": {
"type": "object",
"additionalProperties": false,
"properties": {
"amount": { "type": "number" },
"currency": { "type": "string", "minLength": 3, "maxLength": 3 }
}
},
"valid_from": { "$ref": "#/$defs/datetime" },
"valid_until": { "$ref": "#/$defs/datetime" },
"delegation_of": { "$ref": "#/$defs/ulid" }
}
},
"partyRef": {
"type": "object",
"additionalProperties": false,
"required": ["id", "kind", "display_name"],
"properties": {
"id": { "$ref": "#/$defs/ulid" },
"kind": { "$ref": "#/$defs/partyKind" },
"display_name": { "type": "string", "minLength": 1, "maxLength": 200 },
"identifiers": {
"type": "array",
"items": { "$ref": "#/$defs/identifier" }
},
"org_id": { "$ref": "#/$defs/ulid" },
"role": { "type": "string" },
"mandate": { "$ref": "#/$defs/mandate" },
"contact": {
"type": "object",
"additionalProperties": false,
"properties": {
"email": { "type": "string", "format": "email" },
"channel": { "type": "string", "enum": ["in_app", "email", "matrix", "teams"] }
}
}
}
},
"locator": {
"type": "object",
"additionalProperties": false,
"description": "Stable pointer into a packet item. Prefer quote+context over pixel boxes.",
"required": ["kind"],
"properties": {
"kind": {
"type": "string",
"enum": [
"text_quote",
"pdf_page_region",
"markdown_heading",
"json_pointer",
"xpath",
"sheet_range",
"whole_document"
]
},
"quote": { "type": "string", "maxLength": 2000 },
"prefix": { "type": "string", "maxLength": 200 },
"suffix": { "type": "string", "maxLength": 200 },
"page": { "type": "integer", "minimum": 1 },
"bbox": {
"type": "array",
"items": { "type": "number" },
"minItems": 4,
"maxItems": 4
},
"heading": { "type": "string" },
"pointer": { "type": "string" },
"sheet": { "type": "string" },
"range": { "type": "string" }
}
},
"packetItem": {
"type": "object",
"additionalProperties": false,
"required": ["id", "role", "filename", "media_type", "hash", "bytes"],
"properties": {
"id": { "$ref": "#/$defs/ulid" },
"role": { "$ref": "#/$defs/documentRole" },
"filename": { "type": "string" },
"media_type": { "type": "string" },
"language": { "$ref": "#/$defs/locale" },
"bytes": { "type": "integer", "minimum": 0 },
"hash": { "$ref": "#/$defs/contentHash" },
"storage_uri": { "$ref": "#/$defs/uri" },
"pages": { "type": "integer", "minimum": 1 },
"version": { "type": "integer", "minimum": 1 },
"replaces": { "$ref": "#/$defs/ulid" },
"is_to_be_signed": { "type": "boolean", "default": false },
"is_to_be_stamped": { "type": "boolean", "default": false },
"label": { "type": "string" }
}
},
"highlight": {
"type": "object",
"additionalProperties": false,
"required": ["id", "item_id", "severity", "locator", "author_id", "created_at"],
"properties": {
"id": { "$ref": "#/$defs/ulid" },
"item_id": { "$ref": "#/$defs/ulid" },
"severity": { "$ref": "#/$defs/highlightSeverity" },
"locator": { "$ref": "#/$defs/locator" },
"title": { "$ref": "#/$defs/shortText" },
"note": { "$ref": "#/$defs/mediumText" },
"required_ack": { "type": "boolean", "default": false },
"author_id": { "$ref": "#/$defs/ulid" },
"created_at": { "$ref": "#/$defs/datetime" },
"source": { "type": "string", "enum": ["requester", "reviewer", "system", "agent"] }
}
},
"routeStep": {
"type": "object",
"additionalProperties": false,
"required": ["id", "ordinal", "kind", "assignee", "required_verbs"],
"properties": {
"id": { "$ref": "#/$defs/ulid" },
"ordinal": { "type": "integer", "minimum": 1 },
"parallel_group": {
"type": "string",
"description": "Steps sharing a group may complete in any order. Next ordinal waits for the group."
},
"kind": { "$ref": "#/$defs/stepKind" },
"assignee": { "$ref": "#/$defs/partyRef" },
"required_verbs": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/dispositionVerb" }
},
"min_binding_level": { "$ref": "#/$defs/bindingLevel" },
"due_at": { "$ref": "#/$defs/datetime" },
"started_at": { "$ref": "#/$defs/datetime" },
"completed_at": { "$ref": "#/$defs/datetime" },
"status": {
"type": "string",
"enum": ["pending", "ready", "active", "blocked", "completed", "skipped", "returned"]
},
"completion_disposition_id": { "$ref": "#/$defs/ulid" },
"instructions": { "$ref": "#/$defs/mediumText" }
}
},
"route": {
"type": "object",
"additionalProperties": false,
"required": ["mode", "steps"],
"properties": {
"mode": { "$ref": "#/$defs/routeMode" },
"policy_ref": { "$ref": "#/$defs/uri" },
"steps": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/routeStep" }
},
"current_step_ids": {
"type": "array",
"items": { "$ref": "#/$defs/ulid" }
}
}
},
"commentAnchor": {
"type": "object",
"additionalProperties": false,
"properties": {
"item_id": { "$ref": "#/$defs/ulid" },
"highlight_id": { "$ref": "#/$defs/ulid" },
"locator": { "$ref": "#/$defs/locator" }
}
},
"threadMessage": {
"type": "object",
"additionalProperties": false,
"required": ["id", "author_id", "body", "created_at"],
"properties": {
"id": { "$ref": "#/$defs/ulid" },
"author_id": { "$ref": "#/$defs/ulid" },
"body": { "$ref": "#/$defs/markdown" },
"anchor": { "$ref": "#/$defs/commentAnchor" },
"created_at": { "$ref": "#/$defs/datetime" },
"visibility": {
"type": "string",
"enum": ["all_parties", "internal", "step_only"],
"default": "all_parties"
}
}
},
"presentationSnapshot": {
"type": "object",
"additionalProperties": false,
"description": "Canonical record of what an actor was shown. The disposition MUST reference a snapshot.",
"required": [
"id",
"actor_id",
"memo_version",
"opened_at",
"ui_release",
"locale",
"packet_hashes",
"highlight_set_hash",
"view_hash"
],
"properties": {
"id": { "$ref": "#/$defs/ulid" },
"actor_id": { "$ref": "#/$defs/ulid" },
"memo_version": { "type": "integer", "minimum": 1 },
"opened_at": { "$ref": "#/$defs/datetime" },
"closed_at": { "$ref": "#/$defs/datetime" },
"ui_release": { "type": "string" },
"locale": { "$ref": "#/$defs/locale" },
"viewport": { "type": "string", "enum": ["banner", "brief", "packet", "sign", "awareness", "configure"] },
"phase": { "type": "string", "enum": ["pre_bind", "bind", "post_bind"] },
"awareness_hash": {
"$ref": "#/$defs/contentHash",
"description": "Hash of the awareness slice as shown. Not part of view_hash unless a promote disposition copied fields into binding."
},
"packet_hashes": {
"type": "array",
"items": {
"type": "object",
"required": ["item_id", "hash"],
"additionalProperties": false,
"properties": {
"item_id": { "$ref": "#/$defs/ulid" },
"hash": { "$ref": "#/$defs/contentHash" }
}
}
},
"highlight_ids": {
"type": "array",
"items": { "$ref": "#/$defs/ulid" }
},
"highlight_set_hash": { "$ref": "#/$defs/contentHash" },
"brief_hash": { "$ref": "#/$defs/contentHash" },
"view_hash": {
"$ref": "#/$defs/contentHash",
"description": "Hash of the canonical presentation document (brief + ordered highlights + visible packet manifests)."
},
"acked_highlight_ids": {
"type": "array",
"items": { "$ref": "#/$defs/ulid" }
},
"auth_context": {
"type": "object",
"additionalProperties": false,
"properties": {
"method": {
"type": "string",
"enum": [
"password",
"mfa",
"passkey",
"oidc",
"saml",
"eudi_wallet",
"national_eid",
"qscd"
]
},
"assurance": { "$ref": "#/$defs/assuranceLevel" },
"idp": { "type": "string" },
"session_id": { "type": "string" }
}
}
}
},
"returnPayload": {
"type": "object",
"additionalProperties": false,
"required": ["reasons"],
"properties": {
"reasons": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["code", "text"],
"additionalProperties": false,
"properties": {
"code": {
"type": "string",
"enum": [
"insufficient_context",
"wrong_scope",
"wrong_assignee",
"highlights_incomplete",
"document_stale",
"legal_issue",
"commercial_issue",
"risk_unaddressed",
"other"
]
},
"text": { "$ref": "#/$defs/mediumText" },
"anchor": { "$ref": "#/$defs/commentAnchor" }
}
}
},
"required_changes": { "$ref": "#/$defs/markdown" }
}
},
"transferPayload": {
"type": "object",
"additionalProperties": false,
"required": ["to"],
"properties": {
"to": { "$ref": "#/$defs/partyRef" },
"reason": { "$ref": "#/$defs/mediumText" },
"keep_self_informed": { "type": "boolean", "default": true }
}
},
"signaturePayload": {
"type": "object",
"additionalProperties": false,
"required": ["level", "signed_hash"],
"properties": {
"level": { "$ref": "#/$defs/bindingLevel" },
"profile": {
"type": "string",
"enum": ["PAdES-B-LT", "PAdES-B-LTA", "XAdES-B-LT", "CAdES-B-LT", "JAdES", "raw"]
},
"signed_hash": { "$ref": "#/$defs/contentHash" },
"signed_item_ids": {
"type": "array",
"items": { "$ref": "#/$defs/ulid" }
},
"presentation_bound": {
"type": "boolean",
"description": "True when view_hash is included in the signed attributes (dynamic linking)."
},
"certificate_ref": { "type": "string" },
"tsp": { "type": "string" },
"qts_token_ref": { "type": "string" },
"cms_or_jose": { "type": "string", "description": "Detached signature reference or storage URI." }
}
},
"disposition": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"verb",
"actor",
"step_id",
"presentation_id",
"created_at",
"payload_hash"
],
"properties": {
"id": { "$ref": "#/$defs/ulid" },
"verb": { "$ref": "#/$defs/dispositionVerb" },
"actor": { "$ref": "#/$defs/partyRef" },
"step_id": { "$ref": "#/$defs/ulid" },
"presentation_id": {
"$ref": "#/$defs/ulid",
"description": "Required binding to a presentation snapshot. No snapshot, no binding act."
},
"body": { "$ref": "#/$defs/markdown" },
"return": { "$ref": "#/$defs/returnPayload" },
"transfer": { "$ref": "#/$defs/transferPayload" },
"signature": { "$ref": "#/$defs/signaturePayload" },
"configure": { "$ref": "#/$defs/configurePayload" },
"created_at": { "$ref": "#/$defs/datetime" },
"payload_hash": { "$ref": "#/$defs/contentHash" },
"actor_signature": {
"description": "Optional signature over payload_hash + presentation.view_hash + memo.id@version.",
"$ref": "#/$defs/signaturePayload"
}
}
},
"evidenceEvent": {
"type": "object",
"additionalProperties": false,
"required": ["id", "seq", "type", "at", "payload_hash", "prev_hash", "event_hash"],
"properties": {
"id": { "$ref": "#/$defs/ulid" },
"seq": { "type": "integer", "minimum": 0 },
"type": { "$ref": "#/$defs/evidenceEventType" },
"at": { "$ref": "#/$defs/datetime" },
"actor_id": { "$ref": "#/$defs/ulid" },
"payload": { "type": "object" },
"payload_hash": { "$ref": "#/$defs/contentHash" },
"prev_hash": { "$ref": "#/$defs/contentHash" },
"event_hash": { "$ref": "#/$defs/contentHash" },
"ip_hash": {
"type": "string",
"description": "Optional HMAC of network address; store raw IP only where lawful."
},
"device_attestation": { "type": "string" }
}
},
"evidenceBundle": {
"type": "object",
"additionalProperties": false,
"required": ["id", "memo_id", "memo_version", "events", "head_hash"],
"properties": {
"id": { "$ref": "#/$defs/ulid" },
"memo_id": { "$ref": "#/$defs/ulid" },
"memo_version": { "type": "integer" },
"events": {
"type": "array",
"items": { "$ref": "#/$defs/evidenceEvent" }
},
"head_hash": { "$ref": "#/$defs/contentHash" },
"sealed_at": { "$ref": "#/$defs/datetime" },
"seal": { "$ref": "#/$defs/signaturePayload" },
"qualified_timestamp_ref": { "type": "string" },
"export_uri": { "$ref": "#/$defs/uri" }
}
},
"relation": {
"type": "object",
"additionalProperties": false,
"required": ["kind", "to"],
"properties": {
"kind": {
"type": "string",
"enum": [
"supersedes",
"superseded_by",
"relates_to",
"implements",
"triggered_by",
"attachment_of",
"translation_of"
]
},
"to": { "$ref": "#/$defs/ulid" },
"note": { "type": "string" }
}
},
"scopeRef": {
"type": "object",
"additionalProperties": false,
"required": ["kind", "id", "label"],
"properties": {
"kind": { "$ref": "#/$defs/scopeKind" },
"id": { "type": "string", "minLength": 1, "maxLength": 200 },
"label": { "type": "string" },
"environment": { "type": "string", "enum": ["dev", "test", "stage", "prod", "other"] },
"requires_new_bind": {
"type": "boolean",
"default": false,
"description": "If true, entering this scope is a new binding act, not a post-sign configure."
}
}
},
"hat": {
"type": "object",
"additionalProperties": false,
"description": "Post-bind perspective: role, access profile, or UI/work mode inside an already bound scope.",
"required": ["id", "label"],
"properties": {
"id": { "type": "string", "minLength": 1, "maxLength": 200 },
"label": { "type": "string" },
"kind": {
"type": "string",
"enum": ["role", "access_profile", "perspective", "duty"]
},
"permissions_preview": { "type": "array", "items": { "type": "string" } },
"scope_id": { "type": "string" },
"elevates": {
"type": "boolean",
"default": false,
"description": "If true, selecting this hat is a privilege act and MUST be promoted into binding or a child memo."
}
}
},
"bindingSlice": {
"type": "object",
"additionalProperties": false,
"description": "PRE-SIGN. Identity and scope being bound. Included in view_hash. Choosing among identities or bind-scopes happens here.",
"required": ["principal", "target"],
"properties": {
"principal": { "$ref": "#/$defs/partyRef" },
"available_identities": {
"type": "array",
"items": { "$ref": "#/$defs/partyRef" }
},
"target": { "$ref": "#/$defs/scopeRef" },
"available_bind_scopes": {
"type": "array",
"description": "Scopes the actor may enter as part of THIS bind (e.g. pick tenant at login).",
"items": { "$ref": "#/$defs/scopeRef" }
},
"granted_at_bind": {
"type": "object",
"additionalProperties": false,
"properties": {
"roles": { "type": "array", "items": { "type": "string" } },
"permissions": { "type": "array", "items": { "type": "string" } },
"duration_seconds": { "type": "integer", "minimum": 1 }
}
},
"justification": { "$ref": "#/$defs/mediumText" },
"blast_radius": { "$ref": "#/$defs/mediumText" },
"terms": {
"type": "object",
"additionalProperties": false,
"properties": {
"monitoring": { "type": "boolean" },
"consent_code": { "type": "string" },
"consent_text_hash": { "$ref": "#/$defs/contentHash" }
}
}
}
},
"awarenessSlice": {
"type": "object",
"additionalProperties": false,
"description": "Shown before and after bind for situational awareness. Hashed separately as awareness_hash. NOT in view_hash unless promoted.",
"properties": {
"proposed_hat": { "$ref": "#/$defs/hat" },
"proposed_hat_source": { "$ref": "#/$defs/awarenessSource" },
"available_hats": { "type": "array", "items": { "$ref": "#/$defs/hat" } },
"available_scopes": {
"type": "array",
"description": "Other scopes visible for orientation. Those with requires_new_bind=true cannot be configured post-sign.",
"items": { "$ref": "#/$defs/scopeRef" }
},
"last_session": {
"type": "object",
"additionalProperties": false,
"properties": {
"ended_at": { "$ref": "#/$defs/datetime" },
"hat_id": { "type": "string" },
"scope_id": { "type": "string" },
"principal_id": { "$ref": "#/$defs/ulid" }
}
},
"situation_note": { "$ref": "#/$defs/mediumText" }
}
},
"sessionState": {
"type": "object",
"additionalProperties": false,
"description": "POST-SIGN live configuration inside a completed bind (login, elevation). Configure acts mutate this, not the sealed binding slice.",
"required": ["status"],
"properties": {
"status": { "$ref": "#/$defs/sessionStatus" },
"started_at": { "$ref": "#/$defs/datetime" },
"ended_at": { "$ref": "#/$defs/datetime" },
"bound_memo_version": { "type": "integer", "minimum": 1 },
"bound_presentation_id": { "$ref": "#/$defs/ulid" },
"active_hat": { "$ref": "#/$defs/hat" },
"active_hat_source": { "$ref": "#/$defs/awarenessSource" }
}
},
"configurePayload": {
"type": "object",
"additionalProperties": false,
"properties": {
"hat": { "$ref": "#/$defs/hat" },
"source": { "$ref": "#/$defs/awarenessSource" },
"note": { "$ref": "#/$defs/mediumText" }
}
},
"identityContext": {
"type": "object",
"additionalProperties": false,
"description": "L0/L1 split: binding_slice is pre-sign and signed; awareness_slice is orientation and optional post-sign configure.",
"properties": {
"system": { "type": "string" },
"environment": { "type": "string", "enum": ["dev", "test", "stage", "prod", "other"] },
"principal": { "$ref": "#/$defs/partyRef" },
"requested_roles": { "type": "array", "items": { "type": "string" } },
"requested_permissions": { "type": "array", "items": { "type": "string" } },
"duration_seconds": { "type": "integer", "minimum": 1 },
"justification": { "$ref": "#/$defs/mediumText" },
"blast_radius": { "$ref": "#/$defs/mediumText" },
"binding": { "$ref": "#/$defs/bindingSlice" },
"awareness": { "$ref": "#/$defs/awarenessSlice" }
}
}
},
"type": "object",
"additionalProperties": false,
"required": [
"id",
"version",
"schema_version",
"status",
"created_at",
"updated_at",
"requester",
"question",
"requested_act",
"binding_level",
"brief",
"packet",
"route"
],
"properties": {
"id": { "$ref": "#/$defs/ulid" },
"version": { "type": "integer", "minimum": 1 },
"schema_version": { "type": "string", "const": "0.2.0" },
"status": { "$ref": "#/$defs/memoStatus" },
"outcome": { "$ref": "#/$defs/outcome" },
"depth": { "$ref": "#/$defs/depthLevel" },
"title": { "$ref": "#/$defs/shortText" },
"question": {
"$ref": "#/$defs/shortText",
"description": "Single present-tense decision question."
},
"requested_act": { "$ref": "#/$defs/requestedAct" },
"binding_level": { "$ref": "#/$defs/bindingLevel" },
"locale": { "$ref": "#/$defs/locale" },
"created_at": { "$ref": "#/$defs/datetime" },
"updated_at": { "$ref": "#/$defs/datetime" },
"submitted_at": { "$ref": "#/$defs/datetime" },
"due_at": { "$ref": "#/$defs/datetime" },
"completed_at": { "$ref": "#/$defs/datetime" },
"expires_at": { "$ref": "#/$defs/datetime" },
"requester": { "$ref": "#/$defs/partyRef" },
"subject": {
"$ref": "#/$defs/partyRef",
"description": "Who the decision is about, if different from requester (e.g. the person requesting elevation)."
},
"parties": {
"type": "array",
"items": { "$ref": "#/$defs/partyRef" }
},
"brief": {
"$ref": "#/$defs/markdown",
"description": "Minimum necessary context. Hash of brief is part of presentation.view_hash."
},
"consequences": { "$ref": "#/$defs/markdown" },
"identity_context": { "$ref": "#/$defs/identityContext" },
"session": { "$ref": "#/$defs/sessionState" },
"packet": {
"type": "array",
"items": { "$ref": "#/$defs/packetItem" }
},
"highlights": {
"type": "array",
"items": { "$ref": "#/$defs/highlight" }
},
"route": { "$ref": "#/$defs/route" },
"thread": {
"type": "array",
"items": { "$ref": "#/$defs/threadMessage" }
},
"presentations": {
"type": "array",
"items": { "$ref": "#/$defs/presentationSnapshot" }
},
"dispositions": {
"type": "array",
"items": { "$ref": "#/$defs/disposition" }
},
"relations": {
"type": "array",
"items": { "$ref": "#/$defs/relation" }
},
"evidence": { "$ref": "#/$defs/evidenceBundle" },
"legal": {
"type": "object",
"additionalProperties": false,
"properties": {
"governing_law": { "type": "string" },
"form_requirement": {
"type": "string",
"enum": ["none", "textform", "schriftform", "notarial", "other"]
},
"retention_years": { "type": "integer", "minimum": 1 },
"classification": {
"type": "string",
"enum": ["public", "internal", "confidential", "restricted"]
}
}
},
"extensions": {
"type": "object",
"description": "Namespaced vendor or domain extensions. Must not alter binding semantics.",
"additionalProperties": true
}
}
}

View file

@ -0,0 +1,184 @@
# Decision Memo state transitions v0.2
Actors: `requester`, `assignee` (current ready step), `party` (any listed party), `subject` (session principal after bind), `system`, `agent`.
Guards:
| Id | Meaning |
|---|---|
| G_STEP | Verb is in `route.steps[current].required_verbs` **or** is a always-on overlay verb for that step kind (table B) |
| G_PRES | `presentation_id` exists, `memo_version` matches, `phase` is `pre_bind` or `bind` |
| G_ACK | All `highlights[required_ack=true]` are in `presentation.acked_highlight_ids` |
| G_MAND | Actor mandate covers `requested_act` + legal/value constraints |
| G_LEVEL | Actor signature meets `step.min_binding_level` and memo `binding_level` |
| G_REASONS | `return.reasons` has ≥1 coded reason |
| G_TARGET | `transfer.to` is a resolvable party with compatible mandate |
| G_ESC | Target mandate is *strictly higher* than current assignee for this act |
| G_NOAGENT | `actor.kind ≠ agent` |
| G_SESSION | `session.status = active` and actor is `session` subject |
| G_HAT | `configure.hat.id``awareness.available_hats` and `hat.elevates = false` |
| G_PROMOTE | Field moved from awareness → binding; new memo version; presentations invalidated |
Overlay verbs (always available when status allows, even if not in `required_verbs`): `comment`, `discuss`.
Requester-only overlay: `withdraw` while not terminal.
System-only: `stamp`, expiry.
## A. Memo-level status machine
```
submit
draft ──────────────► submitted
▲ │
│ │ route.started
│ edit (ver++) ▼
│ circulating
│ │
│ ├── first ready step → awaiting_actor
│ ├── discuss → in_discussion
│ │ │
│ │ └── assignee resumes → awaiting_actor
│ │
│ ├── return → returned ──requester revises (ver++)──► submitted
│ ├── last binding verb → completed (+ outcome)
│ ├── withdraw → withdrawn
│ └── due passed → expired (outcome timed_out)
└── completed ── new memo relation.supersedes ──► superseded
L0/L1 extra:
completed + requested_act ∈ {login, elevate}
→ session.status = active (bound_configuring window)
→ configure / session.ended
→ session.status = ended
```
`in_discussion` is a substate of circulation. It does not reset the step.
## B. Verb × status × step kind
Legend: `✓` allowed · `—` forbidden · `C` allowed and may complete the step · `M` allowed and may complete the *memo* · `S` session-only after bind.
Step kinds are grouped:
- **Weak:** `inform`, `comment`, `review`, `acknowledge`
- **Co-sign:** `mitzeichnung`, `approve`
- **Bind:** `schlusszeichnung`, `sign`, `seal`, `deposit`
### B1. Status `draft`
| Verb | requester | assignee | agent | Effect |
|---|---|---|---|---|
| comment | ✓ | — | ✓ | thread only |
| discuss | ✓ | — | — | stays draft |
| withdraw | ✓ | — | — | → withdrawn |
| accept / decline / acknowledge / return / forward / escalate / configure | — | — | — | not circulating |
| submit (system/requester) | ✓ | — | — | → submitted |
### B2. Status `submitted` / `circulating` / `awaiting_actor` / `in_discussion`
| Verb | Weak step | Co-sign | Bind step | Guards | Status after |
|---|---|---|---|---|---|
| comment | ✓ any party | ✓ | ✓ | — | unchanged (`in_discussion` if was circulating) |
| discuss | ✓ any party | ✓ | ✓ | — | `in_discussion` |
| return | ✓ assignee | ✓ | ✓ | G_NOAGENT G_REASONS | `returned` |
| forward | ✓ assignee | ✓ | ✓ | G_NOAGENT G_TARGET | stays circulating; step assignee replaced |
| escalate | ✓ assignee | ✓ | ✓ | G_NOAGENT G_TARGET G_ESC | circulating; higher step inserted/activated |
| acknowledge | C assignee | C | ✓ overlay | G_NOAGENT G_PRES G_ACK | step complete; memo completes only if this was the last step *and* `requested_act=acknowledge` |
| accept | — | C assignee | **M** assignee | G_NOAGENT G_PRES G_ACK G_MAND G_LEVEL G_STEP | last bind/co-sign step → `completed` outcome=`accepted` |
| decline | — | C assignee | **M** assignee | G_NOAGENT G_PRES G_STEP | → `completed` outcome=`declined` (memo ends; later steps skipped) |
| abstain | — | C assignee | C assignee | G_NOAGENT G_PRES | step complete for this actor; parallel group may continue; if actor was sole bind step → `completed` outcome=`abstained` |
| recuse | ✓ | C | C | G_NOAGENT G_PRES | step unassigned; must forward/escalate or return |
| withdraw | requester only | same | same | not terminal | → `withdrawn` |
| stamp | system | system | system | after a bind disposition or on completion | unchanged |
| configure | — | — | — | not yet bound | — |
| promote | requester | requester | requester | G_PROMOTE | version++; back toward submitted |
`accept` on a **weak** step is illegal. Weak steps take `acknowledge`. That is how Kenntnisnahme stays distinct from Schlusszeichnung.
### B3. Status `returned`
| Verb | Who | Effect |
|---|---|---|
| comment / discuss | any party | thread |
| revise + submit | requester | version++; → submitted; new route from policy |
| withdraw | requester | → withdrawn |
| binding verbs | — | illegal on a returned version |
### B4. Status `completed`
| Verb | Who | Effect |
|---|---|---|
| comment | parties | thread; does not unseal |
| stamp | system | file.stamped / bundle.sealed |
| configure | subject | **S** session hat; see §C |
| promote | requester | not on this version; open child memo |
| accept / return / … | — | sealed |
If `requested_act ∈ {login, elevate}` then `session.status` becomes `active` at the completing accept/acknowledge.
### B5. Terminal `withdrawn` | `expired` | `superseded`
No actor verbs except system `stamp` of the evidence bundle already accumulated.
## C. Post-bind session transitions
Applies when `session.status = active`.
```
pre_bind presentation
│ actor chooses identity + bind-scope
│ sees proposed hat / other tenants as AWARENESS only
bind disposition (accept | acknowledge)
│ view_hash covers binding_slice only
│ awareness_hash recorded alongside, not signed
session.active
├── configure(hat) if hat.elevates=false ∧ hat in available_hats
├── attempt other scope
│ requires_new_bind=false → still configure (same bind)
│ requires_new_bind=true → emit scope.rebind_required, open child memo
└── session.ended logout / expiry / revoke
```
| Event | In `view_hash`? | In `awareness_hash`? | Evidence event |
|---|---|---|---|
| Chosen identity | yes | copy ok | identity.authenticated |
| Chosen bind-scope / tenant of *this* login | yes | copy ok | disposition.recorded |
| Privileges granted *at the gate* | yes | preview ok | disposition.recorded |
| Proposed / last-used hat shown | no | yes | awareness.shown |
| Hat selected after login | no | yes | session.hat_selected |
| Hat with `elevates=true` | must promote | — | promote + new bind |
| Other tenant with `requires_new_bind` | no | listed | scope.rebind_required |
Defaulting the hat to `last_used` or `system_default` is an awareness act. Showing it on the login banner is required for situational awareness and forbidden from silently entering `view_hash`.
## D. Completing a step vs completing the memo
A step completes when the assignee records a verb in that steps completion set:
| Step kind | Completion set |
|---|---|
| inform, review, acknowledge | acknowledge, return, recuse |
| comment | comment (if so configured) or acknowledge |
| mitzeichnung, approve | accept, decline, abstain, recuse, return |
| schlusszeichnung, sign, seal, deposit | accept, decline, abstain, recuse, return |
The memo completes when:
1. A bind-step `decline` is recorded, or
2. Every required step is `completed` or `skipped`, and at least one completing verb in the memos `requested_act` family was recorded, or
3. Requester `withdraw`, or system expiry.
Parallel groups: all members must reach a terminal step status before the next ordinal becomes `ready`.
## E. Illegal combinations (explicit)
- Agent + any binding verb
- `accept` on `inform`/`review`
- `configure` before `session.active`
- `configure` of a hat with `elevates=true`
- `configure` of a scope with `requires_new_bind=true`
- Binding verb whose presentation `phase=post_bind`
- Binding verb whose presentation `awareness_hash` was substituted for `view_hash`
- Mutating `identity_context.binding` after the first bind disposition of that version

View file

@ -0,0 +1,31 @@
{
"memo_id": "01K4Q8Z3R7V2N6K9M1P5T8W4XC",
"memo_version": 2,
"question": "Do you formally accept ADR-0042 as the billing architecture for prod?",
"requested_act": "accept",
"binding_level": "aes",
"brief": "Replace nightly batch invoicing with an append-only event ledger.",
"locale": "en",
"ui_release": "informed-decision@0.3.1",
"packet": [
{
"item_id": "01K4Q8DOC0000000000000001",
"hash": {
"alg": "sha256",
"hex": "6b1c0f8a9d2e4c7b8a1f0e3d5c6b7a8f9e0d1c2b3a4f5e6d7c8b9a0f1e2d3c4b"
}
}
],
"highlights": [
{
"id": "01K4Q8HL00000000000000001",
"item_id": "01K4Q8DOC0000000000000001",
"severity": "critical",
"required_ack": true,
"locator": {
"kind": "markdown_heading",
"heading": "Consequences / rollback"
}
}
]
}

View file

@ -0,0 +1,5 @@
{
"login_view_hash": "492d9d311bf44ec9de0d0abef28abac7d31df2781527e8fe276a3186ee1b06b8",
"login_awareness_hash": "2be7742970a01e7a879ae5040660659fb8c9a5c024e6c7fb338f3944a4fe05d1",
"adr_view_hash": "1c89ec07c3cc9d16f85a1ba1be5169456b3c55d21161f64037787779ae91f202"
}

View file

@ -0,0 +1,63 @@
{
"memo_id": "01K4LOGIN00000000000000001",
"memo_version": 1,
"locale": "en",
"ui_release": "informed-decision@0.4.0",
"proposed_hat": {
"id": "hat:finance-controller",
"label": "Finance Controller",
"kind": "access_profile",
"elevates": false,
"permissions_preview": ["invoice.read", "invoice.export"],
"scope_id": "tenant:acme"
},
"proposed_hat_source": "last_used",
"available_hats": [
{
"id": "hat:finance-controller",
"label": "Finance Controller",
"kind": "access_profile",
"elevates": false,
"permissions_preview": ["invoice.read", "invoice.export"],
"scope_id": "tenant:acme"
},
{
"id": "hat:auditor-readonly",
"label": "Auditor (read-only)",
"kind": "perspective",
"elevates": false,
"permissions_preview": ["invoice.read"],
"scope_id": "tenant:acme"
},
{
"id": "hat:payroll-admin",
"label": "Payroll Admin",
"kind": "role",
"elevates": true,
"permissions_preview": ["payroll.run", "employee.export-all"],
"scope_id": "tenant:acme"
}
],
"available_scopes": [
{
"kind": "tenant",
"id": "tenant:acme",
"label": "ACME Corp",
"environment": "prod",
"requires_new_bind": true
},
{
"kind": "tenant",
"id": "tenant:beta",
"label": "Beta GmbH",
"environment": "prod",
"requires_new_bind": true
}
],
"last_session": {
"ended_at": "2026-09-08T16:12:00Z",
"hat_id": "hat:finance-controller",
"scope_id": "tenant:acme"
},
"situation_note": "Last session Tuesday 18:12 CEST as Finance Controller in ACME. Payroll Admin is an elevating hat and needs its own bind."
}

View file

@ -0,0 +1,73 @@
{
"memo_id": "01K4LOGIN00000000000000001",
"memo_version": 1,
"question": "Log into Payroll-Prod as Bernd Worsch in tenant ACME?",
"requested_act": "login",
"binding_level": "organizational",
"brief": "You are entering Payroll-Prod. Sessions are recorded. Privileges at the gate are identity-scoped, not hat-scoped.",
"locale": "en",
"ui_release": "informed-decision@0.4.0",
"packet": [],
"highlights": [],
"binding": {
"principal": {
"id": "01K4PERSONBERND00000000001",
"kind": "person",
"display_name": "Bernd Worsch",
"role": "employee",
"identifiers": [
{ "scheme": "email", "value": "bernd.worsch@example.com" },
{ "scheme": "idp:oidc-sub", "value": "auth.example.com|bernd" }
]
},
"available_identities": [
{
"id": "01K4PERSONBERND00000000001",
"kind": "person",
"display_name": "Bernd Worsch",
"identifiers": [
{ "scheme": "idp:oidc-sub", "value": "auth.example.com|bernd" }
]
},
{
"id": "01K4PERSONBERNDADMIN000001",
"kind": "person",
"display_name": "Bernd Worsch (break-glass)",
"identifiers": [
{ "scheme": "idp:oidc-sub", "value": "auth.example.com|bernd-bg" }
]
}
],
"target": {
"kind": "tenant",
"id": "tenant:acme",
"label": "ACME Corp",
"environment": "prod",
"requires_new_bind": true
},
"available_bind_scopes": [
{
"kind": "tenant",
"id": "tenant:acme",
"label": "ACME Corp",
"environment": "prod",
"requires_new_bind": true
},
{
"kind": "tenant",
"id": "tenant:beta",
"label": "Beta GmbH",
"environment": "prod",
"requires_new_bind": true
}
],
"granted_at_bind": {
"roles": ["authenticated"],
"permissions": ["session.create"]
},
"terms": {
"monitoring": true,
"consent_code": "LOGIN-PROD-2026"
}
}
}

View file

@ -0,0 +1,289 @@
---
id: INFD-WP-0001
type: workplan
title: "Founding specs and approver-UI ownership"
domain: infotech
repo: informed-decision
status: proposed
owner: claude
topic_slug: netkingdom
created: "2026-09-09"
updated: "2026-09-09"
origin: founding
origin_ref: history/20260909-initial-exploration/InitialExploration.md
---
# INFD-WP-0001 — Founding specs and approver-UI ownership
Establish `informed-decision` as a governed repository in the NetKingdom estate,
settle who owns the browser-facing approver UI that `approval-engine`
deliberately does not contain, and produce the specification set that Stage 1
implementation will be built against.
The trigger is concrete and dated. On 2026-09-08 `key-cape` (`KEY-WP-0013-T02`)
asked `approval-engine` for the human approver client's `client_id` and callback
URI. `approval-engine` correctly declined to invent them and recorded in
`docs/keycape-service-registrations.md` that the human approver flow *"belongs to
whichever browser-facing approver UI presents `approval:approve` tokens to this
engine. That component is not in this repo."* `APPROVAL-WP-0002-T01` remains
`progress` partly because of it. This workplan closes that gap by naming the
owner and shipping the contract.
**Status is `proposed`, pending review** against the current `approval-engine`,
`access-engine`, `key-cape`, `gate-house` and `audit-core` contracts, and
against the deployment estate. T02 is the gate: if `gate-house` places this
component differently, T03T05 are rewritten before they are written.
Scope boundary for this workplan: **specification and contract, plus one
walking skeleton**. Full L3 product build is residual and belongs to
`INFD-WP-0002`.
## Establish the founding documents
```task
id: INFD-WP-0001-T01
status: done
priority: high
```
Write the repository's stable statements of purpose and current stage, derived
from the founding exploration rather than reinvented.
Acceptance: `INTENT.md` states purpose, the Decision Memo concept, ownership and
non-ownership against the named estate repositories, design principles, wrongness
conditions and success criteria; `GOAL.md` states the Stage 1 outcome, exclusions,
invariants and definition of done; `README.md` orients a new reader in under a
minute; `history/20260909-initial-exploration/` is preserved unmodified as the
provenance record.
Completed 2026-09-09: `INTENT.md`, `GOAL.md` and `README.md` written. `SCOPE.md`
is deliberately deferred to T06 — a scope file written before the layer ruling
and the specs would describe an imagined boundary, which is the drift `SCOPE.md`
exists to prevent.
## Settle layer placement and approver-UI ownership with gate-house
```task
id: INFD-WP-0001-T02
status: todo
priority: high
```
Take the ownership question to `gate-house` as doctrine rather than asserting a
catalog row. The proposal to argue: `informed-decision` owns the browser-facing
approver surface, the presentation record and the evidence of informedness; it
is **PEP-shaped** under statute §6.4 and companion §5 because it is
browser-facing and causes a protected side effect on the far side of a decision;
it supplies exactly one PIP-like fact — *what was presented* — as a claim, and
never evaluates it.
Raise explicitly, and do not paper over:
- §17 has no request-claim schema owner assigned. A `view_hash`-bearing
presentation claim must yield to that schema when it exists rather than
inventing a permanent local shape.
- `approval-engine`'s claim already carries *"a digest over the same canonical
binding the decision point already computes"*. Whether `view_hash` is that
digest, a sibling of it, or a distinct presentation attestation is a real
question and the wrong answer creates two competing canonicalizations of the
same act. This is the single highest-risk unknown in the workplan.
- A surface that renders both the question and the answer is adjacent to the
self-dealing objection that kept the object out of `access-engine`. State why
it is not the same failure: this repository holds no state a decision reads
as authority.
Acceptance: an intake is filed with `gate-house`; a ruling or a recorded decision
exists; `layer.yaml` is written from the ruling and matches the catalog row, not
this workplan's prose; `INTENT.md` and `GOAL.md` are amended if the ruling
differs from the proposal; the `view_hash`-versus-binding-digest relationship is
recorded as a decision, not left implicit. Blocking for T05 and T07.
## Product Requirements Document
```task
id: INFD-WP-0001-T03
status: todo
priority: high
```
Write `docs/specs/ProductRequirementsDocument.md` for Stage 1: the L3 approval
approver surface, scoped to one real consumer.
Must cover: the personas (approver holding a mandate, requester, observer/auditor);
the disposition vocabulary and which verbs are legal on which step kinds; the
required-highlight acknowledgment gate; the pre-sign/awareness split as it
appears in the UI; the evidence bundle as an export; accessibility and locale
(DE/EN, given the *Umlaufmappe* framing); and the explicit non-requirements from
`GOAL.md` — no mandate graph, no QES, no notification transport.
State the anti-requirements as first-class: no dark patterns, no dwell timers,
no keystroke analytics, and a UI that makes unmistakable that the whole
instrument is bound rather than only the acknowledged highlights.
Acceptance: every requirement traces to either a `GOAL.md` definition-of-done
item or a named external contract; each requirement is testable; the document
names what it is deliberately not requiring and why.
## Use Case Catalog
```task
id: INFD-WP-0001-T04
status: todo
priority: medium
```
Write `docs/specs/UseCaseCatalog.md` covering the full depth spectrum L0L5, with
Stage 1 scope marked, so that scale invariance is testable as a design property
rather than a claim in a vision statement.
For each level: actor, trigger, requested act, binding level, the verbs that must
be available, the evidence produced, and the estate repository that is the
counterparty. Include the L0 login banner and the L2 ADR accept in full even
though they are out of Stage 1 build scope — their purpose here is to constrain
the schema so the object cannot fork later.
Include the negative cases: `accept` on a Kenntnisnahme step (illegal by design),
a bind attempt with unacknowledged required highlights, an agent attempting a
disposition, and a tenant switch requiring a new bind.
Acceptance: every use case maps onto the single Decision Memo schema with no
level-specific object; each negative case names the invariant it protects; the
catalog states for each level which estate repository would consume it.
## Architecture Blueprint
```task
id: INFD-WP-0001-T05
status: todo
priority: high
```
Write `docs/specs/ArchitectureBlueprint.md`. Depends on T02 — the layer ruling
determines what this component is permitted to be.
Must cover: the component boundary and its position in the security layer model;
the call graph to `key-cape` (OIDC authorization-code + PKCE for humans),
`approval-engine` (bearer `approval:approve`, never `approval:consume`),
`access-engine` (decision, never rendered here) and `audit-core` (evidence
emission); the storage posture for memos, presentations and dispositions;
the deployment shape including the Ingress and external origin that
`approval-engine` explicitly does not have; and the failure modes — what the
surface does when `approval-engine`, `key-cape` or `audit-core` is unavailable.
Fail-closed is the default and must be stated per dependency. A surface that
degrades into showing a memo it cannot bind is acceptable; a surface that
degrades into binding without evidence is not.
Acceptance: no component in the diagram renders an authorization decision; the
token audiences, scopes and principal types match `approval-engine`'s
`docs/keycape-service-registrations.md` exactly; every external dependency has a
stated unavailable-stance; the blueprint names which parts are Stage 1 and which
are placeholders.
## Evidence model, schema promotion and canonicalization under test
```task
id: INFD-WP-0001-T06
status: todo
priority: high
```
Promote the exploration artifacts from `history/` into governed, tested
repository assets, and write `docs/specs/EvidenceModel.md`.
Move `decision-memo.schema.json` to `schemas/`, `canonicalize.py` into the
package, and the fixtures in `vectors/` into the test suite. `history/` stays
untouched as provenance; the governed copies are the ones that change.
The four isolation properties from the exploration become tests that must stay
green:
1. shuffling object keys does not change either hash;
2. editing an awareness field does not change `view_hash`;
3. changing `binding.target` does change `view_hash`;
4. selecting a role after login emits `session.hat_selected` and does not
rewrite `view_hash`.
`EvidenceModel.md` covers what an evidence bundle contains, how it verifies
offline, its relationship to `audit-core`'s archive, and — carried over from
`approval-engine`'s reasoning rather than rediscovered — the honest statement
that a hash chain proves records were not altered after arrival and cannot prove
a record was never sent.
Write `SCOPE.md` as the last step of this task, once the layer ruling and the
specs have fixed the real boundary.
Acceptance: schema, canonicalizer and vectors live outside `history/` and are
exercised in CI; the three published expected hashes reproduce byte-for-byte;
`EvidenceModel.md` states the residual it does not close; `SCOPE.md` exists and
describes the implemented-and-first-cut boundary, not the aspiration.
## Publish the OIDC browser-client contract to key-cape
```task
id: INFD-WP-0001-T07
status: todo
priority: high
```
Own and publish the two strings `approval-engine` could not supply: the human
approver client's `client_id` and its full callback URI. Depends on T02 for the
ownership ruling and on T05 for the deployment origin.
The registration is an authorization-code + PKCE public or confidential browser
client — not `client_credentials` — and the resulting access token must carry
`aud=approval-engine`, `principal_type: human`, `tenant: tenant:platform` and
scope `approval:approve`. Redirect URIs match exactly at `/authorize`, so the
origin must be a real deployed origin, decided in T05, not a placeholder.
Do not request `approval:consume`: `approval-engine` refuses it for human
principals, and consumption belongs to the PEP that causes the side effect.
Acceptance: `docs/keycape-client-registration.md` publishes both strings and the
expected token shape; the contract is sent to `key-cape` referencing
`KEY-WP-0013-T02`, and to `approval-engine` referencing its
`docs/keycape-service-registrations.md` follow-up; a token issued against the
registration is accepted by `approval-engine`'s verifier; `KEY-WP-0013-T02` is
unblocked. **This is the task that discharges the gap that created this
repository.**
## Walking skeleton — one approval, end to end
```task
id: INFD-WP-0001-T08
status: todo
priority: medium
```
Prove the specs against reality with the thinnest possible L3 path: sign in via
`key-cape`, list approvals awaiting this principal from `approval-engine`,
render one as a Decision Memo with brief, packet and highlights, acknowledge the
required highlights, and submit an approval entry with a stored presentation
record carrying `view_hash`.
`return` and `discuss` are in this skeleton, not deferred. They are the
differentiator; a skeleton with only approve/reject proves the wrong product.
Acceptance: one approval is approved by a real human through this surface
against a deployed `approval-engine`; the approval entry is reconstructable from
a stored presentation; a bind attempt with unacknowledged required highlights
fails closed; `return` produces a structured reason and is distinguishable from
`decline` in the record; no code path in this repository evaluates whether the
act is permitted.
Gated externally on `approval-engine` `APPROVAL-WP-0002-T01` reaching `done` and
on the service being deployed with an origin this surface can reach.
## Known risks
- **T02 is a hard gate.** Writing the blueprint before the layer ruling risks
building a component the statute does not permit in that shape.
- **Two canonicalizations.** If `view_hash` and `approval-engine`'s binding
digest are not reconciled in T02, the estate ends up with two hashes over the
same act and no rule for which one is authoritative.
- **Deployment origin is on someone else's critical path.** T07 cannot complete
without a real external origin, and this repository does not yet own an
Ingress. This is the most likely cause of slip.
- **Scope pressure toward an approvals inbox.** The fastest way to close
`KEY-WP-0013-T02` is to build a queue with two buttons. That would satisfy the
dependency and abandon the thesis. T04 exists to make the cost of that visible.