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:
parent
771ddb9ced
commit
ee2cca579c
16 changed files with 3218 additions and 1 deletions
130
GOAL.md
Normal file
130
GOAL.md
Normal 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
245
INTENT.md
Normal 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.
|
||||
36
README.md
36
README.md
|
|
@ -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`).
|
||||
|
|
|
|||
325
history/20260909-initial-exploration/InitialExploration.md
Normal file
325
history/20260909-initial-exploration/InitialExploration.md
Normal 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 user’s 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 2026–27; 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 principal’s 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 (I1–I13) 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.
|
||||
184
history/20260909-initial-exploration/canonicalize.md
Normal file
184
history/20260909-initial-exploration/canonicalize.md
Normal 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+0000–U+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.
|
||||
292
history/20260909-initial-exploration/canonicalize.py
Normal file
292
history/20260909-initial-exploration/canonicalize.py
Normal 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"])
|
||||
149
history/20260909-initial-exploration/decision-memo-schema.md
Normal file
149
history/20260909-initial-exploration/decision-memo-schema.md
Normal 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
|
||||
150
history/20260909-initial-exploration/decision-memo.example.json
Normal file
150
history/20260909-initial-exploration/decision-memo.example.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
910
history/20260909-initial-exploration/decision-memo.schema.json
Normal file
910
history/20260909-initial-exploration/decision-memo.schema.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
184
history/20260909-initial-exploration/state-transitions.md
Normal file
184
history/20260909-initial-exploration/state-transitions.md
Normal 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 step’s 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 memo’s `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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"login_view_hash": "492d9d311bf44ec9de0d0abef28abac7d31df2781527e8fe276a3186ee1b06b8",
|
||||
"login_awareness_hash": "2be7742970a01e7a879ae5040660659fb8c9a5c024e6c7fb338f3944a4fe05d1",
|
||||
"adr_view_hash": "1c89ec07c3cc9d16f85a1ba1be5169456b3c55d21161f64037787779ae91f202"
|
||||
}
|
||||
|
|
@ -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."
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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, T03–T05 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 L0–L5, 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue