taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

commit ae57cd26f1b70a9c912f34430045ed00651fb67e
parent 24783f56cad281e43f28ab1f545c18938781941d
Author: sebasjm+llm <sebasjm@numis.ar>
Date:   Tue,  6 Oct 2026 11:12:00 -0300

better docs

Diffstat:
Acore/endpoint-documentation-requirements.rst | 1277+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adesign-documents/105-machine-readable-endpoint-documentation.rst | 1336+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 2613 insertions(+), 0 deletions(-)

diff --git a/core/endpoint-documentation-requirements.rst b/core/endpoint-documentation-requirements.rst @@ -0,0 +1,1277 @@ +.. _endpoint-documentation-requirements: + +============================================== +Endpoint and Method Documentation Requirements +============================================== + +.. contents:: Table of Contents + :local: + :depth: 2 + +Status: proposed convention. Applies to: every per-endpoint ``.rst`` file +under ``taler-docs/core/`` (HTTP APIs: exchange, merchant, bank-\*, +corebank, challenger, taldir, mailbox, donau, auditor, sync, terminal, +...) and every per-operation ``.rst`` file under +``taler-docs/core/wallet-core/`` (wallet-core methods). + +This document is the normative format specification. Design intent and +rationale, the diagnostic of the current corpus, the migration plan, and +worked examples live in :doc:`DD 105 +<../design-documents/105-machine-readable-endpoint-documentation>`. + +The format is language-agnostic. The shape layer describes wire-level +types (JSON objects exchanged between client and server) via the +historically named ``ts:def`` / ``ts:op`` directives; nothing in this +specification ties the documented semantics to any implementation +language, and generated artifacts may target any language. + + +1. Purpose +========== + +Each endpoint/method is documented in **exactly one** ``.rst`` file that is +**completely machine-readable**: every semantic claim a client, a doc lint, +or a use-case generator could need is expressed in a directive option, a +field, or a typed block — never only in prose. Prose remains for human +readers, but it is *rendering*, not *source of truth*. + +The goal is that the following can be **derived** from the doc corpus +without reading prose: + +* per-entity state tables (states, transitions, timers, failure exits), + with every transition naming its trigger and guard, +* per-actor observation projections (who can see which state, through + which endpoint field or notification channel), +* cross-component flows (which call creates an identifier, which calls + carry or consume it, which watched source moves an entity), +* sequence diagrams (happy-path messages from transition edges, + ``alt`` blocks from guards, ``loop`` blocks from timers). + +The docs are **authoritative**: implementations conform to them. To make +that enforceable, the machine-readable shape layer (``ts:def`` types, +request/response schemas) should be used to generate as much source code +as possible (types, validators, stubs), with drift caught by CI in the +implementation repositories. + +One design philosophy runs through everything below: **derive edges, +declare anchors.** Anything that is an *edge* (a transition, an arming, +an observation, a carriage of a value) is written once, on the edge +itself — the transition, the method, the effect. Anything that acts as +a *resolution anchor* (an entity, a timer name, an identifier, a +notification channel) is declared once, minimally, so that the edges can +be checked against it. If a fact can be derived from edges, it is never +also declared; if a name must be resolvable, it is never left implicit. + + +2. File organization +==================== + +R2.1 One file per method. No file documents two unrelated methods; no + method is documented in two files. Two declared exceptions exist: + + * *Variant groups*: one logical method may be documented as + several directive instances in one file when the variants differ + only in a path parameter's value domain (per-record-type delete + endpoints, per-provider webhook entry points) or in long-poll + parameter dialect. The file is linted as one method with + variants. + * *Proxy stanzas*: wildcard delegation declarations + (``.. http:any::`` with wildcard paths, e.g. reverse-proxy + pass-throughs) are not methods; they carry no request/response + semantics and follow only the naming rules of R2.2. + +R2.2 Naming (required): + + * HTTP endpoints: ``<method>-<path-segments>.rst``, lowercase, + path variables in upper case without ``$`` + (``get-withdrawal-operation-WITHDRAWAL_ID.rst``), + placed in the per-API directory (``core/exchange/``, + ``core/merchant/``, ``core/bank-integration/``, ...). + * wallet-core operations: ``<op-name>.rst`` in kebab-case inside + the category directory (``core/wallet-core/balances/``). + +R2.3 Chapter files (``api-*.rst``) keep version history, shared + conventions, and ``.. include::`` of per-method files. They carry + **no** per-method semantics that the method file itself lacks. + +R2.4 Cross-method information lives in exactly two places outside the + method files: the **entity registry** (§4: entities, lifecycles, + timers, identifiers) and the **per-component notification pages** + (§5: real-time channels). Method files *reference* these; they + never restate them. + + +3. The method file format +========================= + +A compliant file contains the sections below **in this order**. +Requirement levels: **REQ** (must be present), **COND** (present when +applicable), **OPT**. + +3.1 Method header — REQ +----------------------- + +The header is the method's machine-readable identity card. Every option +exists because a specific consumer needs it: the lint (vocab checks), +the use-case generator (persona closures, observation bindings), the +code generator (shape extraction), or the exclusion filter (drafts). + +HTTP endpoints keep the existing identity directive, extended with new +options: + +.. code-block:: rst + + .. http:get:: /withdrawal-operation/$WITHDRAWAL_ID + :since: v1 + :maturity: stable + :idempotency: readonly + :observes-state: withdrawal-operation.status + :visible-to: wallet-user, bank-customer (capability withdrawal-id) + :wait: long-poll(timeout_ms, old_state) + + One-line summary usable standalone (no markup that breaks extraction). + + Longer prose description... + +wallet-core operations keep ``ts:op``, extended the same way: + +.. code-block:: rst + + .. ts:op:: getBalances + :read-only: + :since: v4 + :maturity: stable + :idempotency: readonly + :visible-to: wallet-user + :wait: none + +Options: + +``:since:`` (REQ) + Protocol/API version introducing the method, exactly ``v<N>``. + Needed so derived artifacts can be sliced by protocol version (an old + wallet talking to a new exchange sees a genuinely different machine). + +``:deprecated:`` (COND) + Version of deprecation, exactly ``v<N>``; presence implies + ``:maturity: deprecated`` unless ``:maturity:`` says otherwise. + +``:maturity:`` (REQ, default ``stable``) + One of: ``stable``, ``experimental``, ``draft-no-consumers``, + ``proposed-unimplemented``, ``deprecated``. + Methods marked ``draft-no-consumers`` or ``proposed-unimplemented`` + are excluded from use-case derivation (their files exist to document + intent, not behavior). Examples in the current corpus: wads and + reserve-control endpoints in the exchange API, ``/seed`` in donau, + the apns/account-directory/ebisync APIs. + (The option is named ``:maturity:`` and not ``:status:`` because the + HTTP domain already uses ``:status:`` as a field-name alias for + status codes.) + +``:idempotency:`` (REQ) + One of: ``readonly``, ``replay-safe``, ``keyed:<field>``, ``unsafe``. + See §3.5 and §7.1. + +``:observes-state: <entity>.<field>`` (COND) + The response field (dotted path into the response ``ts:def``) that + carries an entity's machine-readable state value + (e.g. ``withdrawal-operation.status``, ``order.order_status``, + ``transaction.state``). The field's type must be a string union or + an object with an enumerated discriminator — the lint resolves every + union member against the entity's lifecycle (§4). This is the + **observation binding**: it states that this method's response *is* a + view onto that entity's machine. Without it, the per-actor state + projections ("who can see which state, where") cannot be computed — + the generator would have to guess the binding from type names or + enum-member spelling, which collides across APIs (``pending`` + appears in half a dozen unions). + +``:visible-to:`` (REQ) + Comma-separated actor surfaces that may legitimately call or observe + through this method (§7.3). This is the single authorization fact of + the file: the actor value implies the credential mechanism (§7.3 + table), so no separate ``:auth:`` field exists. Where access is + gated by an unguessable identifier rather than credentials, the + capability is named in parentheses: + ``merchant-staff, tax-auditor (capability wtid)``. + Persona projections are computed as closures over this field. + +``:timer-ref: <entity>.<timer>`` (COND, repeatable) + References a timer **declared at the entity** (§4.3), with a ``:role:`` + describing this method's relationship to it: + + * ``arms`` — a successful call starts the clock, typically via a + request field named in ``:field:`` (``POST /private/orders`` arms + ``order.refund-deadline`` via ``refund_delay``). + * ``exposes`` — the response carries the timer's current value, in + the field named by ``:field:`` + (``ValidationChallengeInfo.solve_expiration``). + * ``affected-by`` — the method's behavior changes once the timer + fires (``.../challenge/$ID/confirm`` fails with + ``TAN_CHALLENGE_EXPIRED``). + + The timer's semantics are declared once at the entity; the firing + edges live in the lifecycle's transitions. Method files only declare + their *relationship* to the clock. + +``:wait:`` (REQ) + ``none``, or ``long-poll(<param1>, <param2>, ...)`` listing this + method's long-poll request parameters. This acknowledges that the + endpoint can rendezvous with a state change. It stays at the method + because the parameter dialects are per-endpoint and differ + (``timeout_ms`` alone, ``timeout_ms`` + ``old_state``, the merchant's + ``lp_status``/``lp_not_etag`` algebra). Real-time *push* channels + are **not** declared here — they live in the component's notification + page (§5). + +3.2 Request — REQ +----------------- + +**Why this section is strict:** the request block is what the code +generator turns into client stubs and validators, and what the lint +cross-checks ``:wait:`` parameters against. A parameter that exists +only in prose is invisible to both. + +.. code-block:: rst + + **Request:** + + :query timeout_ms: + *Optional.* Timeout in milliseconds for :ref:`long-polling + <long-polling>`. Since protocol **v3**. + :header Taler-Purse-Signature: + *Conditionally required.* Purse signature over the request. + :body: + The request body must be a `WithdrawRequest` object. + + .. ts:def:: WithdrawRequest + ... + +Rules: + +R3.2.1 Every parameter appears exactly once as ``:query:``, + ``:header:``, ``:path:``, or inside the ``ts:def`` body type. + Optionality (``*Optional.*``, ``*Conditionally required.*``) + starts the description; version annotations use ``@since`` / + ``@deprecated`` in the existing style. + +R3.2.2 Every type referenced is defined in a ``ts:def`` block in this + file or in a reachable common file. No anonymous prose types. + +3.3 Response — REQ +------------------ + +**Why the discipline:** HTTP status codes in this ecosystem do not +follow the naive "2xx = success, else failure" rule (see §3.4). The +response section declares the *shape* of each outcome; the **Errors** +section declares its *semantics*. Keeping the two apart is what lets +the generator distinguish a failure edge from a state report. + +.. code-block:: rst + + **Response:** + + :http:statuscode:`200 OK`: + :schema: BankWithdrawalOperationStatus + The withdrawal operation is known to the bank... + :http:statuscode:`404 Not found`: + :error: + The operation was not found. + +Rules: + +R3.3.1 Every documented status code carries ``:schema: <TypeName>`` + (a success-shaped body defined in ``ts:def``), ``:error:`` + (standard `ErrorDetail` body), or both. "Both" happens for + non-2xx codes whose body reports state rather than failure — + those additionally get an **Errors** entry with + ``:temporality: progress`` (§3.4). + +3.4 Errors — REQ (explicitly empty if none) +------------------------------------------- + +**Why this section exists:** error codes are overloaded across flows, +and the overloads are not inferable. A 404 on an unfunded reserve +means "wait, the wire hasn't arrived" (transient); a 404 on an expired +order means "gone forever" (terminal). A 402 on the order status page +is the *normal unpaid state* (progress); a 402 on purse merge is a +fixable rejection. Same codes, opposite meanings. The two fields per +entry — ``:temporality:`` (what kind of outcome this is) and +``:recovery:`` (what moves the flow forward) — are what make the +failure and timer columns of derived state tables computable instead +of guessed. + +Every non-2xx outcome gets a structured entry — including codes that +are **not failures** (progress signals such as 402-unpaid, 202-pending): + +.. code-block:: rst + + **Errors:** + + :error 404 TALER_EC_EXCHANGE_GENERIC_RESERVE_UNKNOWN: + :temporality: transient-state + :recovery: long-poll + The reserve is unknown; the wire transfer may not have arrived. + The wallet should long-poll the reserve status and repeat the + exact same request. + :error 409 TALER_EC_EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS: + :temporality: fixable + :recovery: retry-modified + The reserve balance is too low; retry withdrawing less. + :error 451 TALER_EC_EXCHANGE_KYC_REQUIRED: + :temporality: gate + :recovery: start-kyc + The account must pass KYC before this call can succeed. + :error 402 none: + :temporality: progress + :recovery: none + The order is unpaid; the body carries the ``taler_pay_uri`` the + wallet uses to proceed. This is the normal waiting state of the + flow, not a failure. + +Rules: + +R3.4.1 ``:temporality:`` (REQ per error) — one of ``transient-state``, + ``terminal``, ``fixable``, ``rate-limited``, ``gate``, + ``progress`` (§7.4). ``progress`` entries are how the + generator knows *not* to derive a failure edge: the code is the + state signal. + +R3.4.2 ``:recovery:`` (REQ per error) — the client action that can + move the flow forward, from §7.5. For ``progress`` entries this + is usually ``none`` or ``poll-later``. + +R3.4.3 The human-readable condition follows the fields. Multiple + error codes for one status get one entry each. Where the error + is caused by a timer firing, the condition references the + entity timer (``:timer-ref:`` in the header makes the method's + relationship explicit). + +3.5 Idempotency — COND (mutating methods) +----------------------------------------- + +**Why this is a first-class field:** retries are the universal response +to network failure, and a request that arrived-but-lost-its-response is +indistinguishable from one that never arrived. Whether a verbatim +replay is *safe* therefore decides whether the client's failure branch +exists at all — and whether the derived sequence diagram draws a retry +loop or an abort. This is not guessable: ``POST /withdraw`` is +replay-safe by careful design (repeat the exact request, get the same +response, coins are never lost to a network cut), while +``POST /private/orders`` without an explicit ``order_id`` creates a +duplicate. The label also flags design smells: an ``unsafe`` mutating +method in a system built on at-least-once networking deserves review. + +Covered by the ``:idempotency:`` header option (§3.1, vocabulary in +§7.1). For ``keyed:<field>`` the file additionally documents the key +field's reuse semantics in prose (existing good examples: bank-wire +``/transfer`` ``request_uid``, cashout ``request_uid``). The lint +requires: ``POST``/``PATCH``/``DELETE`` without +``:idempotency: unsafe | replay-safe | keyed:*`` is an error. + +3.6 Lifecycle linkage — COND +---------------------------- + +**Why two bindings and not one:** a method relates to entity machines +(§4) in two fundamentally different ways, and conflating them breaks +different consumers: + +* **Mutators** declare their transitions in **Effects** (§3.7). The + lint diffs these against the entity's lifecycle: an effect naming an + undeclared transition is a doc bug in one of the two places. This is + the two-sided check — the lifecycle says "call X drives A→B", the + method file says "I drive A→B on entity E", and the disagreement + surfaces. One-sided declarations silently rot; that is the lesson + of every doc-versus-code audit on this corpus. +* **Readers** declare the observation binding via ``:observes-state:`` + (§3.1). This is what makes the per-persona "user sees" column of a + state table *generated* rather than authored. + +R3.6.1 Every entity named in either place is registered in the entity + registry with: owner, transitions, terminal states, timers, + identifiers. + +R3.6.2 State values named anywhere in the file resolve to the + entity's lifecycle states (the set inferred from its + transitions). + +3.7 Effects — COND (mutating methods) +------------------------------------- + +**Why this section exists:** request/response documentation describes +what the *caller* gets back — it says nothing about what changed in the +*world*. ``POST /private/orders`` returns an order id; the state that +matters — "a wallet can now claim this order" — is a side effect +visible through a *different* endpoint by a *different actor*. A +mutation is not documented until you have said who else can now see +what. The Effects block is also the generator's source for the +*messages* of a sequence diagram: each effect edge is a signal to +another lifeline. + +.. code-block:: rst + + **Effects:** + + :effect: + :entity: order + :transition: none -> unpaid + :observable-by: :http:post:`/orders/$ORDER_ID/claim` + Creates the order; a wallet can now claim it. + +Field by field: + +``:entity:`` + The lifecycle entity whose machine moves (resolves to §4). + +``:transition:`` + The exact edge performed, in the lifecycle's own notation. Must + agree with the entity's declared transitions (the two-sided check + of §3.6). + +``:observable-by:`` + The endpoint through which another actor observes the change. This + is the join that lets a generator draw the *next* message in the + sequence diagram. + +R3.7.1 One ``:effect:`` entry per world-change beyond the response. + +R3.7.2 Real-time signals caused by the change (wallet notifications, + merchant webhooks) are **not** listed here; they follow from the + transition and are documented in the component's notification + page (§5). + +3.8 Identifiers — COND +---------------------- + +**Why this section exists:** methods do not exist in isolation — a +value created by one call shows up in another component's API three +hops later. This block declares, per *identifier*, the method's role +in that value's life. Identifiers are the handles and join keys of the +system (``reserve_pub``, ``wtid``, ``order-id``, claim nonces); +``order-id`` *names* an order entity, but the identifier itself is a +value, not a machine. + +From the union of all method-side tags, the generator derives the +identifier's full life story (created-by / carried-by / read-by / +consumed-by), draws the cross-component value-flow graph, and the lint +gets its two-sided join checks. The registry (§4.4) holds only the +anchor: the name, the capability property, and prose. + +.. code-block:: rst + + **Identifiers:** + + :identifier reserve_pub: + :role: carries + :registry: :ref:`identifier-reserve-pub` + Wallet-generated reserve key; delivered to the bank here, becomes + the wire subject of the funding transfer. + +The four roles, from the method's perspective: + +* ``creates`` — the value begins to exist here. ``POST + /private/orders`` creates ``order-id``; the wallet's withdrawal + preparation creates ``reserve_pub``. +* ``carries`` — transports a value created elsewhere: in the URL, body, + response, or wire subject. The bank-integration selection POST + carries ``reserve_pub`` from wallet to bank; ``POST + /private/transfers`` carries ``wtid`` from the bank statement into + the merchant backend. +* ``reads`` — uses the value as a lookup key. ``GET + /reserves/$RESERVE_PUB`` reads ``reserve_pub``; ``GET + /transfers/$WTID`` reads ``wtid``. +* ``consumes`` — validity ends here (single-use values): the + challenger's ``/token`` consumes the authorization grant; a + successful ``/orders/$ID/pay`` consumes the claim nonce. + +One method can play several roles on several identifiers: ``POST +/orders/$ID/pay`` *reads* ``order-id``, *consumes* the claim nonce, and +*carries* ``h_contract_terms`` onward to the exchange. + +R3.8.1 ``:role:`` vocabulary is §7.8. The ``:registry:`` ref must + resolve to the identifier's registry entry (§4.4). + +3.9 Details/Notes — OPT +----------------------- + +Free prose for anything genuinely not expressible above (rationale, +privacy notes, examples). Must not introduce semantics that contradict +or extend the structured sections; the lint greps for state names and +error codes appearing *only* here. + + +4. Entities and lifecycles +========================== + +**Why a registry:** state lives *between* methods. A method file can +honestly describe its own request and response, but the machine the +methods collectively implement — the states, the transitions, the +clocks — is a property of the *entity*, not of any endpoint. The +registry (``core/entities.rst``, with per-API chapters linking in) is +where that machine is declared once. Per the design philosophy, the +registry declares anchors (names, owners, terminal states, timer names, +identifier properties) and the edges (transitions) carry their own +facts. + +Each entity has exactly one declaration: + +.. code-block:: rst + + .. entity:: tan-challenge + :owner: corebank + + A TAN challenge issued for multi-factor confirmation of a + corebank operation. + + .. lifecycle:: + :terminal: solved, expired, exhausted + + :transition none -> unsolved: + :trigger: call <any MFA-gated call returning + 202 + ChallengeResponse> + The challenge is created as part of an MFA-gated operation. + :transition unsolved -> solved: + :trigger: call POST /accounts/$U/challenge/$ID/confirm + Correct TAN submitted. + :transition unsolved -> expired: + :trigger: timer tan-challenge.solve-expiration + The challenge was not solved in time. + :transition unsolved -> exhausted: + :trigger: call POST /accounts/$U/challenge/$ID/confirm + :if: the failed-confirmation counter reaches zero + Too many wrong TANs submitted. + + .. timer:: solve-expiration + The clock by which the challenge must be solved. (Armed by + challenge creation — derived from the ``arms`` timer-refs of + the creating methods; exposed in + ``ValidationChallengeInfo.solve_expiration`` — derived from + ``exposes`` timer-refs; fires the ``unsolved -> expired`` + transition — derived from the lifecycle.) + + .. timer:: retransmission-cooldown + Throttles TAN retransmission; calls while it is active fail + with 429 (derived from the ``affected-by`` timer-ref and the + error entry of the (re)send endpoint). + + .. identifier:: challenge-id + :capability: no (owner-authenticated) + Identifies the challenge on the challenge endpoints. + +4.1 Owner +--------- + +``:owner:`` (REQ) names the **component holding authoritative state and +executing the transitions** — not merely where the entity is documented +or displayed. Ownership determines which component's answer is truth; +every other component's view is a projection that may lag (the wallet's +balance is a projection of exchange-owned coin-spend state; the +wallet's order status display is a projection of merchant-owned order +state). An entity may be *exposed* through several APIs of the owner +(``withdrawal-operation``: bank-integration, corebank, terminal) — name +them in prose under the owner. Where state is genuinely shared across +components (KYC requirement and legitimization state is projected into +the exchange, the merchant, the bank, and the wallet), the owner is +the component whose answer is authoritative and the projections are +named as such; ownership is about who executes transitions, not about +which trigger kind dominates — a wallet transaction entity is owned by +wallet-core even though its transitions are driven by watches, timers, +and local user actions rather than incoming calls (§6). + +4.2 Lifecycle +------------- + +The lifecycle is the transition list. **States are inferred**: the +state set is exactly the names appearing as transition sources or +targets (plus the pseudo-state ``none`` for pre-existence, which is +never a real state). ``:terminal:`` (REQ) is **declared**, not +inferred — it is the absorbing set: + +* it anchors use-case success conditions ("done" = reaching a declared + terminal state); +* it makes the absorbing property checkable: a transition *leaving* a + declared terminal state is a lint error; +* it turns incompleteness into signal: a declared-non-terminal state + with no outgoing transitions is a lint warning (a silently truncated + machine becomes visible). + +``:final-reactivatable:`` (COND) declares states that anchor success the +same way but from which declared reactivation edges are legal (a paid +order later refunded; a ``done`` wallet transaction reactivated by a +refund query). Transitions leaving a ``final-reactivatable`` state are +permitted but must be listed explicitly. See §6 for the wallet +transaction entities that need this. + +Each ``:transition: A -> B:`` has: + +``:trigger:`` (REQ, repeatable) + What causes the transition (§7.7). This is the causality layer — + the part of the system that no per-endpoint doc can see locally: + + * ``call <method>`` — an external call to the owner's API + (resolvable to a method file). Example: the wallet's selection + POST drives ``pending -> selected``. + * ``timer <entity>.<timer>`` — a declared entity timer fires. + Example: ``solve-expiration`` drives ``unsolved -> expired``. + * ``watch <component>.<observable>`` — the owner watches an external + source (the exchange's wirewatch watching the bank's incoming + history; the merchant backend tracking exchange wire arrivals). + This is where **cross-component causality** lives: the transition + names the other component's observable, which is the join the old + prose never made. + * ``external <description>`` — a human or off-API action (bank-UI + 2FA confirmation, cash inserted at a terminal, an offline key + ceremony). Non-API actors enter the machine here, honestly marked + as outside every API. + +``:if:`` (COND) + The guard: the config/data condition under which this transition + exists at all, referencing observable fields + (``:if: /keys.kyc_enabled and the amount crosses the withdrawal + threshold``). Guards are how context forks (KYC on/off, thresholds, + feature flags) enter the machine without multiplying machines — one + canonical machine, guarded edges, instead of a combinatorial family + of near-identical diagrams. + +Prose (REQ) + One or two sentences of human explanation. + +4.3 Timers (slim anchors) +------------------------- + +A timer is declared as a **name plus semantics in prose** — nothing +more, because everything structural is derivable from edges: + +* *fires-to*: from the lifecycle transition whose ``:trigger:`` is this + timer; +* *armed-by*: from the method whose ``:timer-ref:`` has role ``arms``; +* *exposed-in*: from the method whose ``:timer-ref:`` has role + ``exposes`` (its ``:field:``); +* *gates*: from the ``429``/``rate-limited`` error entry of the method + whose ``:timer-ref:`` has role ``affected-by``. + +The block survives at all for three reasons: it is the **lint anchor** +every ``:trigger: timer`` / ``:timer-ref:`` resolves against (free +strings across hundreds of files would drift); it is the home of +**config-armed** timers no method arms (``:armed-by:`` is present only +in that case — e.g. ``purse_timeout`` comes from ``GET /keys`` global +fees, not from any call); and it is the **gap marker**: an entity whose +timers are not yet specified records the gap here once (see the +``withdrawal-operation`` example in DD 105), instead of silence +scattered across N method files. + +4.4 Identifiers (slim anchors) +------------------------------ + +An identifier is declared as **name + ``:capability:`` + prose**: + +.. code-block:: rst + + .. identifier:: reserve_pub + :capability: yes + Reserve public key; possession authorizes reserve-status reads. + Wallet-generated; becomes the wire subject of the funding + transfer. + +``:capability:`` (REQ: ``yes``/``no``) records whether possession +alone authorizes (unguessable: ``wtid``, ``withdrawal-id``, purse/claim +links) — the property that distinguishes "public by design" (``GET +/keys``) from "public but unguessable" (``/transfers/$WTID`` is usable +by tax auditors *because* of it). It is a property of the *value*, not +of any method, so it has no other home; it is also what the +``:visible-to:`` capability annotations resolve against (V3). + +Everything else is derived from the method files' **Identifiers:** +blocks (§3.8): *created-by*, *carried-by*, *read-by*, *consumed-by*. +One exception needs prose: identifiers created **internally** (``wtid`` +is minted by the exchange's aggregator process — no method tags +``creates``) get a created-by sentence here, mirroring config-armed +timers. The registry also serves as the canonical naming list — the +single place where ``reserve_pub`` is one concept, so that hundreds of +files of tags can be checked for spelling drift. + + +5. Per-component notification pages +=================================== + +**Why these pages exist:** how an actor *learns* that a state changed +is as much a part of the use case as the state change itself — and it +was historically documented in prose scattered across chapters. The +doctrine of this system is *signals are hints; reads are truth*: a +notification tells you something changed, an authoritative read tells +you what is. The notification page is where a component declares its +channels, their delivery semantics, and — critically — the read that +backs each signal. + +Every component that offers real-time signals documents them in a +single notification page: ``core/wallet-core/notifications.rst`` +(exists today as a flat list of notification types; to be restructured +into the channel form below), a merchant notification page (webhooks, +currently prose in ``api-merchant.rst``), an exchange notification page +(will honestly declare: none — pull + long-poll only), and so on. + +Each channel declaration: + +.. code-block:: rst + + .. notification-channel:: webhook + :component: merchant + :registration: POST /private/webhooks + :delivery: at-least-once + :events: order_created, pay, refund, order_settled, + category_created, category_update, category_delete, + inventory_update, inventory_product_created, ... + + Every event is a hint about an entity transition; the + authoritative read is named per event: + ``pay``/``refund``/``order_settled`` -> + ``GET /private/orders/$ORDER_ID``. + +Rules: + +R5.1 ``:delivery:`` is one of ``lossy-hint`` (no delivery guarantee; + re-read for truth — the wallet notification doctrine), + ``at-least-once`` (duplicates possible, recipient must + deduplicate), ``rendezvous`` (the channel itself blocks until the + state is available). If the implementation's delivery semantics + are not specified today, write ``unspecified`` — a visible gap, + not silence. + +R5.2 Every event names its **authoritative read** (the endpoint whose + response is the truth). The lint rejects a channel without a + read reference per event family. + +R5.3 Method files never declare push channels. The method whose + effect *causes* a signal is linked automatically: effect + transition (§3.7) + channel event mapping (here). + +R5.4 Long-poll is **not** a notification channel; it is a property of + the read endpoint and is declared via ``:wait:`` (§3.1). A + long-poll response is authoritative state delivered late, not a + hint. + + +6. wallet-core method variant +============================= + +``ts:op`` files follow the same requirements with this mapping: + +.. list-table:: + :header-rows: 1 + + * - HTTP concept + - wallet-core op equivalent + * - ``:visible-to:`` + - ``wallet-user`` (local message protocol, no credentials) + * - ``:http:statuscode:`` responses + - **Response** ``ts:def`` + **Expected errors** + * - ``:error`` entries + - **Expected errors** entries, same ``:temporality:`` / + ``:recovery:`` fields + * - ``:wait:`` + - ``long-poll(...)`` for blocking ops (``waitTransactionState``, + ``waitExchangeReady``, ...), ``none`` otherwise; the wallet's + push channel is documented in ``notifications.rst`` (§5) + * - effects + - transitions on transaction-group entities + (``transaction-withdrawal``, ...) with states from the DD37 + machine + +``TransactionState`` values used in ``:transition:`` fields must match +``major[:minor][/working]`` notation of DD37 and resolve against +``core/wallet-core/transactions/get-transaction-by-id.rst``. + +Wallet transaction entities extend the lifecycle model of §4 in four +ways (rationale in DD 105): + +* **Final-reactivatable states.** DD37's ``done`` is not absorbing + (refund queries, ``rebind-session``, and late KYC requirements can + reactivate a transaction). Transaction entities declare + ``:final-reactivatable:`` (§4.2) for such states and list the + reactivation edges explicitly. +* **Orthogonal modifiers instead of state explosion.** The DD37 + ``working`` flag and the ``suspended`` / ``suspended-aborting`` / + ``suspended-finalizing`` shadow states are declared as modifiers + (same mechanism as ``:modifiers:`` on other entities), not as + literal states. A ``:standard-edges: suspend-retry`` declaration on + the lifecycle implies the suspend/resume/retry edge family on every + non-final state, so the inferred state and edge sets include them + without authoring each one. DD37 state *expressions* + (``pending:*``, ``pending:-``, ``/idle``, comma alternation) are + accepted wherever a state set is matched. +* **Absence as signal.** The ``deleted`` state exists only in + notifications; its observation is the absence of the record (a + not-found error from ``getTransactionById``). ``:observes-state:`` + bindings may name this explicitly (``transaction.state, + absence-signal: deleted``). +* **Actions are not derived.** ``txActions`` are computed per state + *and* context; no "possible actions" column is generated from the + lifecycle. + +The registry entries for transaction entities are the canonical +machine-readable form of the DD37 machines; DD37's per-type state +tables are generated from the registry. + + +7. Controlled vocabularies +========================== + +Controlled vocabularies are where machine-readability lives or dies. +A field whose values come from a closed list can be checked, counted, +and closed over by tooling; a field with free text is prose wearing a +costume. Every value below is part of the contract: **adding a value +is a specification change** (tooling must learn its meaning), and using +a non-listed value is a lint error (M8.3). For each value we give the +definition, a real example from the corpus, and what the derivation +tooling does with it. + +7.1 ``:idempotency:`` values +---------------------------- + +The question this field answers is the most frequent operational +question a client faces: *"the network ate my request or its response — +may I send the exact same bytes again?"* The answer determines whether +the failure branch of a use case exists at all, and what the sequence +diagram draws at every network boundary. + +* ``readonly`` — the call mutates nothing: no entity transition, no + world-change. (Logging and caching on the server side do not count + as mutation.) Examples: ``GET /keys``, ``getBalances``. + *Consumers:* the generator expects no **Effects** section; the lint + warns if a ``readonly`` method declares one (V6's inverse). + +* ``replay-safe`` — the server has recorded the request and its + response; a byte-identical repeat returns the same response with no + additional effect. Examples: ``POST /withdraw`` (repeating the exact + request yields the same blinded signatures — coins are never lost to + a network cut), donau batch-issue (keyed on the recorded request + hash). *Failure mode prevented:* value loss when the response is + lost after the server committed. *Consumers:* the generator draws + retry self-loops for free. Note that replay means *byte-identical*; + a modified request is a new operation, and conflicts there belong to + **Errors** (e.g. ``TALER_EC_EXCHANGE_WITHDRAW_IDEMPOTENT_PLANCHET``). + +* ``keyed:<field>`` — the client supplies an idempotency key; reuse of + the key with the same body suppresses duplicate effects, reuse with a + *different* body is rejected (409). Examples: bank-wire + ``/transfer`` (``request_uid``), corebank cashouts (``request_uid``, + including the rule that a corrected quote needs a fresh nonce). + *Why it exists:* lets the client retry safely *and* lets the server + detect client bugs. §3.5 requires prose on the key's reuse + semantics. + +* ``unsafe`` — retries may duplicate effects. Example: + ``POST /private/orders`` without an explicit ``order_id`` creates a + second order. Must be justified in prose. *Consumers:* derivation + tooling surfaces ``unsafe`` mutating methods prominently — in a + system built on at-least-once networking, an unsafe mutation is a + design smell. *Guidance:* prefer having the client supply the + identifier (``order_id``), which turns ``unsafe`` into + ``keyed:order_id`` by construction. + +7.2 ``:maturity:`` values +------------------------- + +Why this field exists: derivation must know which methods describe +*behavior* and which describe *intent*. A documented-but-unimplemented +endpoint is not a fact about the system; deriving use cases from it +manufactures fiction. + +* ``stable`` — implemented and conformant; fully derivable. +* ``experimental`` — implemented, but may change without a version + bump. Derivable, but derived artifacts are marked experimental. + Example: nearly all auditor diagnostic endpoints ("still + experimental" throughout ``api-auditor.rst``). +* ``draft-no-consumers`` — a spec nothing implements or consumes. + Examples: the apns relay, ebisync, the bank account directory (all + self-declared "nothing depends on this API"); sync in practice. + Excluded from derivation; renders with a banner. +* ``proposed-unimplemented`` — a documented proposal, explicitly not + implemented. Examples: wads, and the reserve-control endpoints + (``/reserves/$PUB/open|close|attest``) in the exchange API. Excluded + from derivation; kept for design reference. +* ``deprecated`` — still functional, but new derived artifacts must not + use it; the replacement should be named in prose (and + ``:deprecated:`` carries the version). + +7.3 ``:visible-to:`` values (actor ⇒ implied credential) +-------------------------------------------------------- + +This field is the file's single authorization fact. The design +decision: the *actor* is declared and the *mechanism* is implied by the +table below, because declaring both would duplicate one fact in two +drift-prone places. Persona projections are computed as closures over +this field: a persona is a set of actors; its visible surface is the +set of methods whose ``:visible-to:`` intersects it. + +.. list-table:: + :header-rows: 1 + + * - Actor + - Implied mechanism + * - ``wallet-user`` + - wallet-core local message protocol (no credentials; "visibility" + is possession of the device running the wallet) + * - ``merchant-staff`` or ``merchant-staff:<scope>`` + - ``Authorization: Bearer secret-token:...`` with the named + permission scope (``orders-read``, ``transfers-write``, ...) + * - ``aml-officer`` / ``aml-officer:readonly`` + - ``Taler-AML-Officer-Signature`` header; the ``:readonly`` + variant maps to the officer's read-only access level (the + ``read_only`` flag in ``GET /aml/$OFFICER_PUB``) + * - ``exchange-operator`` + - master-key signature (management operations) + * - ``auditor`` + - auditor API authentication (see ``api-auditor.rst``) + * - ``tax-auditor`` + - none — capability identifiers only (``wtid``); this actor has + *no* credentials anywhere in the system + * - ``bank-customer`` + - external bank UI; on APIs, capability ``withdrawal-id`` + * - ``bank-admin`` + - corebank bearer token (admin scope) + * - ``kyc-provider`` + - provider webhook credentials + * - ``any-unauthenticated`` + - none — public by design (``GET /keys``). Do not confuse with + capability-gating: public-by-design means the *content* is + meant for everyone; capability-gated means the *identifier* is + the secret. + * - ``external`` + - outside any API (bank statements, PoS displays, offline + ceremonies) — the honest escape hatch for non-API actors + +Capability-gated access is annotated in the field: +``:visible-to: merchant-staff, tax-auditor (capability wtid)`` means +the listed actors hold the unguessable identifier; the lint resolves +the identifier against the registry (§4.4) and checks its +``:capability: yes`` (V3). + +7.4 ``:temporality:`` values +---------------------------- + +The classification of an outcome's *kind* — the input that decides +which column of the derived state table (happy / timer / failure) an +edge lands in, and whether a non-2xx is a failure at all. + +* ``transient-state`` — the condition resolves without the client + changing anything; the identical request may succeed later. + Example: 404 ``RESERVE_UNKNOWN`` on an unfunded reserve. + *Derived as:* a wait/retry edge, not a failure exit. +* ``terminal`` — will never succeed; abandon or restart the flow. + Examples: 410 expired purse; 410 already-solved challenge. + *Derived as:* a failure exit to a terminal documentation state. +* ``fixable`` — a *modified* request can succeed. Examples: 409 + insufficient funds (withdraw less); 400 parameter malformed. + *Derived as:* a failure edge with a return loop after modification. +* ``rate-limited`` — retry only after the documented delay. Examples: + mailbox 429 with the machine-readable ``retry_delay`` field; TAN + retransmission cooldown. *Derived as:* a retry edge annotated with + the delay's source field. +* ``gate`` — blocked on an action by the user or a third party, outside + this call. Examples: 451 KYC required; 202 MFA challenge; 402 + payment-required at sync. *Derived as:* a gate edge into the + relevant sub-flow (KYC legitimization, MFA solving). +* ``progress`` — **not an error**: the non-2xx code is the happy-path + state signal. Examples: 402 unpaid + ``taler_pay_uri`` on the order + status endpoint; 202 pending on deposit tracking; 304 unchanged + during long-poll. *Derived as:* **no failure edge** — the code *is* + the waiting state's observation. + +Classification guide (ask in order): Is it actually a failure? → +``progress``. Can the caller fix the request? → ``fixable``. Does +the condition resolve by itself? → ``transient-state``. Does it only +need a delay? → ``rate-limited``. Does it need an outside action? +→ ``gate``. Will it never succeed? → ``terminal``. + +7.5 ``:recovery:`` values +------------------------- + +The client's forward action — the label on the edge that *leaves* this +outcome. ``:temporality:`` says what kind of outcome it is; +``:recovery:`` says what to do about it. + +* ``retry-same`` — safe verbatim replay (pairs with + ``:idempotency: replay-safe`` / ``keyed:``; pairing an unsafe method + with ``retry-same`` is a contradiction the lint should flag). +* ``retry-modified`` — change the request per the error's guidance + (lower the amount; add the missing field; set ``max_age`` per the + ``maximum_allowed_age`` hint). +* ``retry-after`` — wait the documented delay, then ``retry-same`` + (429 + ``retry_delay``). +* ``long-poll`` — wait on the status endpoint's rendezvous parameters + (reserve status long-poll). +* ``poll-later`` — re-read after some time, without rendezvous (the + sync backup cadence; usual companion of ``progress``). +* ``refresh-config`` — re-fetch ``/keys`` or ``/config`` first + (denomination unknown ⇒ outdated keys). +* ``start-kyc`` — enter the legitimization sub-flow (451 responses + carry the requirement). +* ``user-action`` — the human must act outside this API (confirm in the + bank app; insert cash). +* ``abort`` — invoke the flow's own abort path (abort the withdrawal; + delete the purse). +* ``give-up`` — terminal; no forward action exists. +* ``none`` — informational; nothing to recover from. + +7.6 ``:timer-ref:`` roles +------------------------- + +A method's relationship to an entity clock comes in three kinds, and +each feeds a different derivation: + +* ``arms`` — a successful call starts the clock; ``:field:`` names the + arming request field (``POST /private/orders`` arms + ``order.refund-deadline`` via ``refund_delay``). *Feeds:* the + "armed when" column of timer tables. (Config-armed clocks are the + exception — declared ``:armed-by:`` at the anchor, §4.3.) +* ``exposes`` — the response carries the clock's current value; + ``:field:`` names the response field + (``ValidationChallengeInfo.solve_expiration``). *Feeds:* the + observation layer — where an actor reads the time remaining. +* ``affected-by`` — the method's behavior changes once the clock fires + (challenge confirm fails with ``TAN_CHALLENGE_EXPIRED``). + *Feeds:* the join between the timer's firing edge and this method's + error table. + +One method may reference several timers (one ``:timer-ref:`` each); one +reference carries one role. + +7.7 ``:trigger:`` kinds +----------------------- + +A transition without a cause is narrative; the trigger is what makes +the machine causal — and what makes the sequence diagram drawable, +since every trigger kind maps to a diagram element (message, timer +edge, external note). + +* ``call <method>`` — an external call to the owner's API. Resolvable + to a method file (V13). The most common kind. Example: the wallet's + selection POST drives ``pending -> selected``. +* ``timer <entity>.<timer>`` — a declared entity timer fires. + Resolvable to the §4.3 anchor. Example: ``solve-expiration`` drives + ``unsolved -> expired``. +* ``watch <component>.<observable>`` — the owner watches an external + source. This is where *cross-component* causality lives: the + exchange's wirewatch watching ``bank.incoming-history``; the merchant + backend tracking the exchange's outgoing wires. Naming form is + ``component.observable`` so the join stays checkable. +* ``external <description>`` — a human or off-API action: bank-UI 2FA + confirmation, cash inserted at a terminal, an offline key ceremony. + Non-API actors enter the machine here, honestly marked as outside + every API. Deliberately not lint-resolvable. + +Triggers are repeatable: one transition may have several (``selected -> +confirmed`` via the confirm endpoint *or* the bank UI). + +7.8 Identifier roles +-------------------- + +The method↔identifier relationship declared in **Identifiers:** +(§3.8): ``creates`` (the value begins to exist here), ``carries`` +(transports it onward in URL/body/response/wire subject), ``reads`` +(uses it as a lookup key), ``consumes`` (its validity ends here). +Definitions and examples are in §3.8. The roles are always from the +*method's* perspective: the same ``reserve_pub`` is created by the +wallet, carried by the bank-integration selection, read by the exchange +— and the union of those tags is what V21 checks joins with (exactly +one creator per carried/read value, or an internal-creation note at the +anchor). + + +8. Machine-readability rules +============================= + +These rules are the contract between authors and tooling. Each exists +because a specific derivation or lint check depends on it; breaking one +does not merely make the docs sloppy — it makes a specific derived +artifact silently wrong. Each rule below states the requirement, why +it exists, a typical violation, and what checks it. + +M8.1 One method per file, one identity directive per file (declared + variant groups excepted, §2). + + *Why:* the file is the unit of indexing — filename conventions + (§2.2), lint scoping, and generated cross-references all assume + the path↔method mapping is one-to-one. + *Violation:* two endpoints documented in one file "because they + are similar" — their semantics can no longer be addressed + separately, and backfill tools cannot target them. + *Checked by:* V1 (count identity directives per file). + +M8.2 Every element in §3 marked REQ is present; COND is present or + the absence is obvious from method kind (e.g. GET ⇒ no Effects). + + *Why:* the REQ fields are the minimum derivation set — a missing + REQ field means the generator must guess (the exact failure this + specification exists to eliminate). The COND absence rule exists + so the lint doesn't cry wolf on legitimately absent sections. + *Violation:* a POST without ``:idempotency:``. + *Checked by:* the REQ lint rules (V2, V3, V5, V11, ...). + +M8.3 All field values come from the §7 vocabularies — free text is a + lint error. + + *Why:* a misspelled value (``rate_limited`` for + ``rate-limited``) is invisible to a human reviewer but silently + removes the entry from every derived table. + *Violation:* ``:temporality: transient`` (missing ``-state``). + *Checked by:* V2, V3, V5, V10, and the vocabulary checks. + +M8.4 Every name that must resolve, resolves: entity names → registry; + state values → lifecycle transitions (inferred set); ``:schema:`` + types → ``ts:def`` blocks; ``:timer-ref:`` targets → entity timer + anchors; ``:identifier:`` names → identifier anchors; + ``:trigger: call`` refs → method files; ``:trigger: timer`` refs + → entity timers; notification channels → the component's + notification page. + + *Why:* this rule is what turns the corpus from a pile of pages + into a graph. Every derived artifact (state tables, persona + projections, sequence diagrams) is a traversal of these edges; a + dangling reference is a broken join that no amount of prose + repairs. + *Violation:* ``:observes-state: withdrawal-operation.state`` + (the field is ``status``) — the persona projection for the bank + customer silently loses the withdrawal states. + *Checked by:* V7, V8, V9, V10, V12, V13, V21. + +M8.5 State values and error codes appear in structured positions + first; prose mentions are decoration. + + *Why:* prose is unparseable. A state or error that exists only + in a sentence is invisible to every consumer — the machine has a + state the tooling cannot see. + *Violation:* "returns 410 when the purse is gone" in the + description, with no 410 entry in **Errors**. + *Checked by:* V14 (grep prose for known state names and + ``TALER_EC_*`` codes absent from the structured fields). + +M8.6 Version annotations follow the single style ``@since **vN**`` / + ``@deprecated **vN**``. + + *Why:* derived artifacts must be sliceable by protocol version — + an old wallet talking to a new exchange sees a genuinely + different machine, and only uniform field-level versioning makes + that computable. The style already exists as a convention; this + rule makes it enforceable. + *Violation:* "since v3" (unparseable variant), missing + annotations on newer fields. + *Checked by:* V16 (warn-only until backfilled). + +M8.7 Draft APIs carry ``:maturity: draft-no-consumers`` and a visible + banner; derivation tooling skips them. + + *Why:* documented intent is not behavior. Deriving use cases + from an API nothing implements manufactures fiction with the + false authority of a generated artifact. + *Violation:* a use-case table that includes apns device + registration as if it were a live flow. + *Checked by:* V17. + +M8.8 The ``ts:def`` layer is the codegen source of truth: type + definitions must be complete enough to generate client/server + types and validators without consulting prose. + + *Why:* the shape layer is what the spec-first model generates + code from (§1). A field that exists only in a sentence ("the + response also contains X when...") produces a generator-visible + type that disagrees with the documented behavior — the + documentation-drift disease, now with a compiler. + *Violation:* optional fields described only in running text; + union variants mentioned in prose but absent from the + ``ts:def``. + *Checked by:* partially by V4 (every outcome needs a schema); + fully only by codegen dry-runs diffed against implementations. + + +9. Validation rules (lint specification) +========================================= + +A doc lint (to live in ``taler-docs/_exts/``) enforces, as build +warnings/errors: + +.. list-table:: + :header-rows: 1 + + * - ID + - Rule + * - V1 + - Identity directive present exactly once per documented method + (variant groups: one directive per variant, §2); options parse. + * - V2 + - ``:since:`` / ``:deprecated:`` match ``v\d+``; ``:maturity:`` in + vocab. + * - V3 + - ``:visible-to:`` present; actors and capability annotations in + vocab; capability identifiers resolve to identifier anchors. + * - V4 + - Every ``:http:statuscode:`` has ``:schema:`` and/or ``:error:``; + non-2xx codes with ``:schema:`` have a ``progress`` or ``gate`` + entry in **Errors**. + * - V5 + - Every non-2xx outcome has an entry with ``:temporality:`` and + ``:recovery:`` in vocab. + * - V6 + - Mutating methods have non-``readonly`` ``:idempotency:`` and at + least one ``:effect:`` (or explicit "no observable effect" note). + * - V7 + - Entity names (in Effects, ``:observes-state:``, + ``:timer-ref:``) resolve to entity anchors; identifier names in + **Identifiers:** blocks resolve to identifier anchors. + * - V8 + - State values resolve to the named entity's inferred state set + (including modifier-expanded sets for wallet transaction + entities, §6). + * - V9 + - ``:observes-state:`` path exists in the response ``ts:def`` and + its type is an enumerated union. + * - V10 + - ``:timer-ref:`` targets exist among the named entity's declared + timers; roles in vocab; ``:field:`` present when the role is + ``arms`` or ``exposes``; every declared timer has at least one + ``arms`` reference or an ``:armed-by:`` in its anchor. + * - V11 + - ``:wait:`` present; long-poll parameters match the method's + documented ``:query:`` parameters. + * - V12 + - Every real-time channel of a component is declared in that + component's notification page; no push-channel metadata appears + in method files. + * - V13 + - Transition triggers resolve (``call`` → method file, ``timer`` + → declared entity timer, ``watch`` → ``component.observable`` + form); **Effects** transitions and lifecycle transitions agree + (two-sided check); ``:observable-by:`` refs resolve. + * - V14 + - No ``TALER_EC_*`` or state value appears only in prose. + * - V15 + - Filename and location follow §2.2. + * - V16 + - ``@since`` present on every ``ts:def`` field of public types + (grandfathered: warn-only until backfilled). + * - V17 + - ``draft-no-consumers`` / ``proposed-unimplemented`` methods + render a banner and are excluded from derived artifacts. + * - V18 + - wallet-core ``:transition:`` values match DD37 notation, resolve + against the ``TransactionMajorState`` / ``TransactionMinorState`` + enums documented in ``get-transaction-by-id.rst``, and edges + resolve against the transaction entity lifecycles in the + registry. + * - V19 + - Lifecycle integrity: every declared terminal state is reachable + from ``none`` and absorbing (no outgoing transitions); every + ``final-reactivatable`` state's outgoing transitions are + explicitly declared reactivation edges; every inferred + non-terminal state has at least one outgoing transition; every + inferred state is reachable from ``none``. + * - V20 + - ``:if:`` guards reference documented config/data fields + (best-effort resolvability: ``/keys`` fields, request fields, + documented config). + * - V21 + - Identifier joins: every identifier with a ``carries`` / + ``reads`` tag has exactly one ``creates`` across the corpus or + an internal-creation note in its anchor; every ``creates`` is + carried or read somewhere. diff --git a/design-documents/105-machine-readable-endpoint-documentation.rst b/design-documents/105-machine-readable-endpoint-documentation.rst @@ -0,0 +1,1336 @@ +DD 105: Machine-Readable Endpoint and Method Documentation +########################################################## + +:Design status: Draft +:Implementation status: Not started +:DD shepherd: TBD +:Historical contributors: TBD +:First published: 2026-10-06 +:Last substantive change: 2026-10-06 +:Normative references: :doc:`../core/endpoint-documentation-requirements` + +.. contents:: Table of Contents + :depth: 2 + +Summary +======= + +Every HTTP endpoint and every wallet-core operation in the Taler +documentation is described in exactly one ``.rst`` file, and every semantic +claim in that file that tooling could need — versioning, maturity, +idempotency, actor visibility, error semantics, state observation, world +effects, identifier roles, timers — is expressed in a directive option or +typed block, never only in prose. A small number of registries (entities +with lifecycles, timers, identifiers; per-component notification channels) +hold the cross-method anchors. From this corpus, tooling derives per-entity +state tables, per-actor visibility projections, cross-component flows, and +sequence diagrams, and generates client types and validators, with drift +against implementations caught in CI. + +The format is language-agnostic: the shape layer describes wire-level types +(JSON messages exchanged between client and server) via the historically +named ``ts:def`` / ``ts:op`` directives, and generated artifacts may target +any implementation language. + +This document records the *intent*, the *diagnostic of the current state*, +the *design decisions*, and the *migration plan*. The normative +field-by-field format, controlled vocabularies, and lint rules are +specified in :doc:`../core/endpoint-documentation-requirements`; where the +two disagree during evolution, the requirements document wins for format +questions and this DD wins for *why*. + +Motivation +========== + +The documentation corpus is large (~490 per-endpoint files across exchange, +merchant, bank-*, corebank, challenger, taldir, mailbox, donau, auditor, +plus 152 wallet-core operations) and already uniform in *shape*: identity +directive, request, response, prose details. But the semantics a reader +most needs are locked in prose or absent: + +* **Error codes are overloaded across flows.** A 404 on an unfunded + reserve means "wait"; a 404 on an expired order means "gone"; a 402 on + the order status page is the *normal* unpaid state. Same codes, opposite + meanings, not inferable. +* **Retry safety is never stated.** ``POST /withdraw`` is replay-safe by + careful design; ``POST /private/orders`` without an ``order_id`` + duplicates. In a system built on at-least-once networking, this is the + most frequent operational question a client faces, and the corpus answers + it only occasionally, in prose. +* **State lives between methods.** No per-endpoint file can honestly + describe the machine the endpoints collectively implement — the order + lifecycle, the withdrawal operation, the TAN challenge — so those + machines exist only implicitly, and cross-component causality (the + exchange's wirewatch driving a bank-owned state; the merchant tracking + exchange wire arrivals) exists only as narrative. +* **Docs and code drift silently.** Doc-versus-code audits on this corpus + repeatedly find stale state names, missing error codes, and endpoints + whose real behavior has moved on. One-sided declarations rot; the fix is + two-sided, machine-checkable declarations. +* **Deriving anything requires reading prose.** Use-case walkthroughs, + persona-specific API surfaces ("what can a tax auditor see?"), state + tables, and sequence diagrams are today hand-authored and immediately + stale. + +The goal is a documentation corpus that is the *authoritative source of +truth* in the spec-first sense: implementations conform to it, code is +generated from its shape layer, and derived artifacts are produced without +reading prose. + +Current state of the corpus +=========================== + +This section is the diagnostic: what the corpus provides today, and which +gaps the requirements document closes. + +What files contain today +------------------------ + +The current corpus already provides, per file: + +.. list-table:: + :header-rows: 1 + + * - ID + - Element + - Encoding today + * - E1 + - Method identity (HTTP method + path / op) + - ``.. http:get:: /p`` / ``.. ts:op:: op`` + * - E2 + - One-line + prose description + - directive body + * - E3 + - Request parameters + - ``:query:``, body as ``.. ts:def::`` + * - E4 + - Response per status code + - ``:http:statuscode:`` + ``ts:def`` + * - E5 + - Error codes + - ``TALER_EC_*`` names in status prose + * - E6 + - Field-level versioning + - ``@since **vN**`` / ``@deprecated`` comments + * - E7 + - Chapter-level version history + - "Version History" section of ``api-*.rst`` + * - E8 + - Chapter-level auth model + - prose section of ``api-*.rst`` + * - E9 + - Long-poll parameters + - ``:query timeout_ms:`` + ``:ref:`` to convention + * - E10 + - Idempotency + - *occasionally*, prose only (e.g. ``post-withdraw.rst``) + * - E11 + - Read-only / deprecated flags (wallet ops) + - ``:read-only:``, ``:deprecated:`` on ``ts:op`` + * - E12 + - Expected errors + side effects (wallet ops) + - prose lists in **Details:** + +Verified against the corpus (2026-10): the only options the identity +directives accept today are ``deprecated``/``noindex``/``synopsis`` (HTTP +domain) and ``read-only``/``deprecated`` (``ts:op``). None of the new +header options, and none of the ``entity`` / ``lifecycle`` / ``timer`` / +``identifier`` / ``notification-channel`` directives, exist anywhere +outside the specification documents themselves — all are new +``_exts/`` work. ``core/entities.rst`` does not exist. +``core/wallet-core/notifications.rst`` exists, but as a flat list of +notification types with payload definitions — no channel, delivery, or +event structure — and must be restructured into the channel form. + +Gaps to close +------------- + +.. list-table:: + :header-rows: 1 + + * - ID + - Missing element + - Why derivation needs it + * - G1 + - Method maturity (stable/draft/...) + - drafts must be excludable from derivation + * - G2 + - Per-method actor visibility + - persona closures are computed from who may call what + * - G3 + - Error semantics + - same status/code is transient in one flow, terminal in another, + and sometimes not an error at all (progress); cannot be inferred + * - G4 + - Idempotency label + - retries are safe or not — never guessable + * - G5 + - Lifecycles with triggers and guards + - the entity machine: what causes each transition, and when that + path exists at all (config/data forks such as KYC thresholds) + * - G6 + - Timers + - expiry/deadline edges of state tables + * - G7 + - Observation channels + - how an actor *learns* a state changed + * - G8 + - Identifier joins + - which values are created/carried/read/consumed where — + the cross-component join keys + * - G9 + - Effects (structured) + - what changed in the world beyond the response + +Structural mismatches found by verification +------------------------------------------- + +* **~20 files violate one-method-per-file as-is**, some deliberately: + ``auditor/delete-monitoring-records.rst`` carries 24 identity directives + (one per monitoring-record type); KYC webhook and AML transfer files + carry 2–4; long-poll variants (e.g. ``get-purses-PURSE_PUB-merge.rst``) + are documented as two directives in one file. +* **~12 files are proxy stanzas**, not methods: ``.. http:any::`` wildcard + reverse-proxy delegation declarations (``merchant/any-star.rst``, seven + corebank ``any-...-star.rst`` files) with no request/response semantics. +* **sync and terminal have no per-endpoint files at all**: 3 and 10 + endpoints respectively live inline in ``api-sync.rst`` / + ``api-terminal.rst``. +* **KYC state is cross-cutting**: 23+ KYC-related files across six + components; exchange, merchant, bank-wire, and wallet all hold + projections, so naive single-owner modeling fails. +* **Wallet transaction machines** (DD37) need four lifecycle-model + extensions; see D10. + +Requirements +============ + +R1 Each endpoint/method is documented in exactly one ``.rst`` file whose + semantic content is completely machine-readable. + +R2 From the corpus, without reading prose, tooling can derive: + per-entity state tables (states, transitions, timers, failure exits), + per-actor observation projections, cross-component identifier flows, + and sequence diagrams (messages from transitions, ``alt`` from guards, + ``loop`` from timers). + +R3 The shape layer (``ts:def`` types, request/response schemas) is + complete enough to generate client/server types and validators; drift + is caught by CI in the implementation repositories. + +R4 Documented *intent* (drafts, unimplemented proposals) is visibly and + mechanically distinguishable from documented *behavior*, and excluded + from derivation. + +R5 The format must cover the existing corpus without exceptions that + swallow rules: HTTP APIs, wallet-core operations, long-poll dialects, + non-2xx progress signals, capability-gated access, config/data forks + (KYC thresholds), and wallet transaction lifecycles per DD37. + +R6 Every cross-reference a lint can check must be resolvable: entity + names, state values, timers, identifiers, trigger targets, channel + events. Free strings are lint errors. + +R7 Migration is incremental: the current corpus is mostly compliant in + shape, and backfill proceeds slice by slice (bank-integrated withdrawal + first), with the lint warn-only until a slice is done. + +Proposed Solution +================= + +The full format is normatively specified in +:doc:`../core/endpoint-documentation-requirements`. This section states +the design as a sequence of decisions and their reasons. + +D1: Derive edges, declare anchors +--------------------------------- + +The organizing philosophy. Anything that is an *edge* — a transition, an +arming, an observation, a carriage of a value — is written exactly once, on +the edge itself (the lifecycle transition, the method header, the effect, +the identifier tag). Anything that acts as a *resolution anchor* — an +entity, a timer name, an identifier, a notification channel — is declared +once, minimally, so edges can be checked against it. If a fact can be +derived from edges it is never also declared; if a name must be resolvable +it is never left implicit. + +This is what keeps the system from collapsing under its own bookkeeping: +timers are slim anchors (name + prose) because their armed-by, exposed-in, +fires-to, and gates facts are all derivable from timer-refs, error entries, +and lifecycle triggers; identifiers are name + ``:capability:`` + prose +because their created-by/carried-by/read-by/consumed-by story is the union +of method-side tags. The alternative — restating everything at the anchor — +guarantees drift. + +D2: One file per method, machine-readable header +------------------------------------------------ + +Each method's identity card is its existing directive (``.. http:get::`` / +``.. ts:op::``) extended with options: version (``:since:`` / +``:deprecated:``), maturity, idempotency, actor visibility, observation +binding, timer relationships, and long-poll dialect. Every option exists +because a named consumer needs it (lint, generator, exclusion filter); none +is decorative. + +**Decision: the maturity option is named ``:maturity:``, not +``:status:``.** The vendored sphinxcontrib-httpdomain in ``_exts/`` +already accepts ``:status:`` as a field-name alias for status codes +(``names=('statuscode','status','code')``); a new header option with that +name collides. + +**Decision: file-naming and one-directive-per-file rules get two explicit +exemptions, not silent violations.** + +* *Variant groups*: a small number of files legitimately document several + directive instances of one logical method + (``auditor/delete-monitoring-records.rst`` with 24 record types, KYC + webhook provider variants, long-poll variants such as + ``exchange/get-purses-PURSE_PUB-merge.rst``). These are declared + variant groups and linted as one method with variants, rather than + being split into near-identical files. +* *Proxy stanzas*: ~12 files using ``.. http:any::`` with wildcard paths + (``merchant/any-star.rst``, the corebank ``any-...-star.rst`` reverse- + proxy delegation declarations) are not methods with request/response + semantics. They get their own minimal stanza class and are excluded + from request/response/idempotency requirements. + +**Decision: sync and terminal are split.** Both APIs currently live inline +in their chapter files (3 and 10 endpoints). They are split into +per-endpoint files like every other API; chapter files keep only version +history and shared conventions. + +D3: Errors carry temporality and recovery +----------------------------------------- + +Every non-2xx outcome — including codes that are *not failures* (202 +pending, 304 unchanged, 402 unpaid) — gets a structured entry with +``:temporality:`` (transient-state / terminal / fixable / rate-limited / +gate / progress) and ``:recovery:`` (retry-same / retry-modified / +retry-after / long-poll / poll-later / refresh-config / start-kyc / +user-action / abort / give-up / none). This pair is what makes the failure +and timer columns of derived state tables computable instead of guessed, +and what tells the generator when *not* to derive a failure edge +(``progress`` entries are state signals, not errors). + +D4: Idempotency is a first-class label +-------------------------------------- + +``readonly`` / ``replay-safe`` / ``keyed:<field>`` / ``unsafe``. Retries +are the universal response to network failure, and a request that +arrived-but-lost-its-response is indistinguishable from one that never +arrived; whether a verbatim replay is safe decides whether the client's +failure branch exists at all. The label also flags design smells: an +``unsafe`` mutating method in this ecosystem deserves review, and the +lint requires the field on every mutating method. + +D5: Authorization is one fact: the actor +---------------------------------------- + +``:visible-to:`` declares the actor surface (wallet-user, +merchant-staff[:scope], aml-officer[:readonly], exchange-operator, auditor, +tax-auditor, bank-customer, bank-admin, kyc-provider, any-unauthenticated, +external), and the credential mechanism is *implied* by a fixed table. +Declaring both actor and mechanism would duplicate one fact in two +drift-prone places. Capability-gated access (unguessable identifiers such +as ``wtid`` or ``withdrawal-id``) is annotated in parentheses and resolved +against the identifier registry, which is where the ``:capability:`` +property of the *value* lives. Persona projections are computed as +closures over this field. + +D6: Methods bind to entity machines in exactly two ways +------------------------------------------------------- + +*Mutators* declare transitions in an **Effects** block (entity, exact edge, +observation endpoint). *Readers* declare an observation binding +(``:observes-state: <entity>.<field>``) stating that the response *is* a +view onto that entity's machine. Both are checked two-sided against the +entity registry: the lifecycle says "call X drives A→B", the method says +"I drive A→B on entity E", and disagreement is a lint error. One-sided +declarations silently rot; that is the lesson of every doc-versus-code +audit on this corpus. + +D7: Identifiers are the join keys +--------------------------------- + +Methods tag their role in each identifier's life: ``creates`` / ``carries`` +/ ``reads`` / ``consumes``. The union over ~490 files yields the +cross-component value-flow graph and the lint's join checks (exactly one +creator per carried/read value, or an internal-creation note at the anchor +for values like ``wtid`` minted by the exchange's aggregator). The +registry holds only the anchor: name, ``:capability:``, prose. + +D8: Entities, lifecycles, triggers, guards +------------------------------------------ + +The entity registry declares, per entity: ``:owner:`` (the component +holding *authoritative* state and executing transitions — not merely where +the entity is displayed), a transition list, a declared terminal set, timer +anchors, and identifier anchors. States are *inferred* from transitions; +terminal states are *declared* so that use-case success conditions are +anchored, the absorbing property is checkable, and truncated machines are +visible (a non-terminal state with no outgoing edges is a lint warning). + +Every transition names its *trigger* — ``call <method>``, ``timer +<entity>.<timer>``, ``watch <component>.<observable>``, or ``external +<description>`` — and optionally a guard (``:if:``) for config/data forks +(KYC on/off, thresholds, feature flags). Triggers are the causality layer +no per-endpoint doc can see locally; guards keep one canonical machine with +guarded edges instead of a combinatorial family of near-identical machines. + +D9: Notifications live on per-component pages +--------------------------------------------- + +How an actor *learns* that state changed is declared once per component: +channel, registration, delivery semantics (``lossy-hint`` / +``at-least-once`` / ``rendezvous`` / honestly-``unspecified``), events, and +— critically — the *authoritative read* backing each event family, per the +doctrine *signals are hints; reads are truth*. Method files never declare +push channels; the link from effect to signal is derived. Long-poll is not +a notification channel: it is a property of the read endpoint (``:wait:``), +since a long-poll response is authoritative state delivered late, not a +hint. Components with no push channels (the exchange) declare that +negative fact explicitly. + +D10: Wallet transaction lifecycles need four model extensions +------------------------------------------------------------- + +The wallet-core variant maps HTTP concepts onto ``ts:op`` files, with +effects expressed as transitions on per-transaction-type entities +(``transaction-withdrawal``, ``transaction-payment``, ...) whose state +vocabulary is DD37's ``major[:minor][/working]``. Verification against +DD37 and the wallet-core implementation surfaced four places where the +base lifecycle model does not fit; each gets an explicit extension rather +than a workaround: + +**D10.1 Final states may be reactivatable.** DD37 ``done`` is not +absorbing: a payment is reactivated by refund queries and +``rebind-session``; a deposit in ``finalizing:track`` can fall back to +``pending`` when new KYC requirements appear. The lifecycle model +therefore distinguishes ``:terminal:`` (absorbing) from +``:final-reactivatable:`` (success anchor for use cases, but reactivation +edges are legal and must be listed). The absorbing lint applies only to +``:terminal:``. + +**D10.2 ``/working`` and the suspended shadow states are orthogonal +modifiers, not states.** DD37's public state value is a tuple +``(major, minor, working)``; ``working`` is a transitional presentation +flag explicitly slated to become major states later, and nearly every +non-terminal state has paired ``suspended`` / ``suspended-aborting`` / +``suspended-finalizing`` shadow states plus retry self-loops that DD37 +deliberately omits from its diagrams. Encoding all of this as literal +states and transitions would explode every table. Instead, wallet +transaction entities declare: + +* ``working`` as a lifecycle-level orthogonal flag (same mechanism as the + order entity's ``:modifiers:``); +* a ``:standard-edges: suspend-retry`` declaration that *implies* the + suspend/resume/retry edge family on every non-final state, so the + inferred state set and edge set include them without authoring them. + +State values in ``:transition:`` fields use DD37 notation, and DD37's +state-*expression* language (``pending:*``, ``pending:-``, ``/idle``, +comma alternation) is accepted wherever a state set is matched. + +**D10.3 Ownership is inside-out, and that is fine.** A wallet +transaction entity is owned by wallet-core (it holds the authoritative +record and executes transitions), but its triggers are almost entirely +``watch <component>.<observable>`` (bank withdrawal operations, exchange +reserves/KYC, merchant refunds, *another wallet* in p2p) plus local user +actions and timers — the inverse of the tan-challenge reference example +where the owner executes on ``call``. This is not a violation of the +owner rule but its second mode, and the worked examples below include a +watch-driven wallet entity alongside call-driven bank entities. +Corollaries: one logical p2p transfer is two entities (debit and credit) +with no shared machine ("public states describe each wallet's local +progress", DD37), and hidden child transactions (``internal-withdrawal``) +are separate entities, not states of the parent. + +**D10.4 Some truths are not derivable from the machine.** Three are +declared as explicit non-goals of derivation for wallet transactions: + +* ``txActions`` are computed per state *and* context and are **not** + derivable from the lifecycle (DD37: frontends must use the returned + ``txActions``). No "possible actions" column is generated. +* ``deleted`` is notification-only, never a stored state; its observation + is the *absence* of the record (``WALLET_TRANSACTION_NOT_FOUND``). The + observation model gains the notion "absence as signal" for exactly this + case (and wallet-core's ``none`` pseudo-state appearing in transition + notifications). +* Internal states are intentionally collapsed on exposure (e.g. + redenomination phases report as ``pending:withdraw/working``). The + registry describes the *public* machine; the mapping from internal + status enums is implementation detail, checked by CI drift detection, + not authored in docs. + +D11: DD37's tables are generated, not mirrored +---------------------------------------------- + +The registry entries for wallet transaction entities become the canonical +machine-readable form of DD37's machines, and DD37's per-type state tables +are *generated from the registry* (with DD37 remaining the human-oriented +prose and diagrams). Re-encoding DD37 by hand in the registry would create +the very two-sided drift problem this system exists to eliminate. The +lint's wallet rule resolves state *names* against the +``get-transaction-by-id.rst`` enums and state *edges* against the registry. + +D12: Lint as build checks, warn-only during migration +----------------------------------------------------- + +A doc lint in ``taler-docs/_exts/`` enforces the rules (identity, +vocabulary, resolvability, two-sided lifecycle agreement, identifier joins, +lifecycle integrity, filename/location) as Sphinx build warnings/errors. +New rules land warn-only per slice and flip to error when the slice is +backfilled. + +D13: KYC/AML is one exchange-owned entity; decisions are data, not states +------------------------------------------------------------------------- + +KYC is the hardest entity to model because its state is genuinely +cross-cutting: the wallet blocks transactions on it, the merchant shows a +status page for it, the bank carries its wire transfers, the auditor +observes its holds, and AML officers act on it. Verification against the +exchange's actual schema (DD23) shows that the authoritative state lives +entirely in the exchange: per wire target (``h_payto``), the exchange holds +the access token, the currently triggered legitimization measures, the +active decision/rule-set ledger, and the external provider processes. +Everything else — the wallet's ``ExchangeWalletKycStatus`` and transaction +minors, the merchant's ``MerchantAccountKycRedirect.status`` enum, the +KYC SPA's requirement list, the auditor's AML-hold view — is a projection, +named as such under the owner (per the owner rule's projection clause). + +**Decision: one entity, ``kyc-account``, owned by the exchange.** Its +observable surface is ``GET /kyc-check/$H_NORMALIZED_PAYTO`` (200 = no +mandatory action, 202 = redirect user to legitimization, 409 = account not +yet bound to a public key, i.e. KYC-auth wire transfer required) plus the +``aml_review`` flag, the monotonic ``rule_gen`` counter (the long-poll +rendezvous), and the access token that authorizes the SPA. Its states are +client-meaningful, not DB-meaningful: ``clean``, ``auth-required``, +``action-required`` (measures open), ``in-process`` (external provider +engaged), ``under-review`` (``aml_review``). There are no terminal states: +a bank account never stops existing; dormancy and closure are modifiers. + +**Decision: officer decisions and rule sets are guards and modifiers, not +states.** An AML officer decision (``POST /aml/$OFFICER_PUB/decision``) +does four things: installs a new ``LegitimizationRuleSet`` (which *changes +which transitions exist* — hence ``:if:`` guards on threshold rules, not +new states), sets account properties (``is_frozen``, ``was_reported``, +``pep``, ``high_risk`` — declared as ``:modifiers:`` on ``kyc-account``), +triggers immediate measures (a ``call`` trigger into ``action-required``), +and sets ``keep_investigating`` (the ``under-investigation`` modifier). +This is exactly how the implementation works (the active row of +``legitimization_outcomes`` is superseded, not stacked): decisions rewrite +the machine's guard data. The concurrency guard of the API (409 +``AML_DECISION_MORE_RECENT_PRESENT``) is an ordinary ``fixable`` error with +``retry-modified`` recovery (re-read, then re-decide). Officer +provisioning (``POST /management/aml-officers``, master-key signed) is the +``exchange-operator`` actor acting on the officer registry — separate +entity, not part of ``kyc-account``. + +**Decision: projections may be restricted or lossy, and that is +declared.** The KYC SPA's projection is *deliberately* restricted (the +law may forbid disclosing true status; ``aml_review`` is only a hint), so +``:observes-state:`` on the SPA endpoints is annotated as restricted. The +merchant's status enum mixes entity states (``kyc-required``, +``awaiting-aml-review``) with observation failures (``exchange-unreachable``, +``logic-bug``); the lint resolves only the former against the lifecycle — +projection-local failure members are declared at the observing method, not +added to the entity's machine. The wallet deliberately conflates +"user must act" with "staff review in progress" in ``pending:kyc``; the +docs must not model it as user-actionable only. + +**Decision: Challenger is a separate entity, plugged in by trigger.** An +address-validation process (Challenger or another provider) has its own +machine (``setup → challenged → solved/failed/expired``, OAuth-flavored), +owned by the provider component. It never decides KYC: its outcome enters +``kyc-account`` via ``watch kyc-provider.webhook`` / ``call +GET /kyc-proof/$PROVIDER`` triggers. Provider processes have their own +expiration timers, distinct from the account's rule-set expiration. + +Worked examples +=============== + +These examples show the target format applied to real corpus content. +They illustrate the requirements document; they are not themselves +normative additions to it. + +A compliant method file +----------------------- + +``core/bank-integration/get-withdrawal-operation-WITHDRAWAL_ID.rst`` +brought into compliance — added structure marked with ``※``: + +.. code-block:: rst + + .. http:get:: /withdrawal-operation/$WITHDRAWAL_ID + :since: v1 + :maturity: stable + :idempotency: readonly + :observes-state: withdrawal-operation.status ※ + :visible-to: wallet-user, bank-customer (capability withdrawal-id) + :wait: long-poll(timeout_ms, old_state) ※ + + Query information about a withdrawal operation, identified by the + ``WITHDRAWAL_ID``. + + **Request:** + + :query timeout_ms: *Optional.* + Timeout in milliseconds, for :ref:`long-polling <long-polling>`, + to wait for operation state to be different from ``old_state``. + Since protocol **v3**. + :query old_state: + *Optional.* Defaults to "pending". + :query long_poll_ms: *Optional.* + Deprecated in protocol **v3**. Use *timeout_ms* instead. + + **Response:** + + :http:statuscode:`200 OK`: + :schema: BankWithdrawalOperationStatus ※ + The withdrawal operation is known to the bank, and details + are given in the `BankWithdrawalOperationStatus` response body. + + **Errors:** ※ + + :error 404 none: + :temporality: terminal + :recovery: give-up + The operation was not found. (A not-yet-propagated operation + id may also 404 briefly; clients that just received the id from + a ``taler://withdraw`` URI may retry briefly.) + + **Identifiers:** ※ + + :identifier withdrawal-id: + :role: reads + :registry: :ref:`identifier-withdrawal-id` + The lookup key of the operation, conveyed via + ``taler://withdraw``. + :identifier reserve_pub: + :role: reads + :registry: :ref:`identifier-reserve-pub` + Wallet-selected reserve public key, returned here as part of + the operation state. + + **Details:** + + .. ts:def:: BankWithdrawalOperationStatus + ... (unchanged) ... + +Notes on the example: + +* The 404 entry shows the G3 problem and its resolution: the *same* + code is terminal for a stale id and transient for a not-yet-visible + one — the condition is documented, the default classification is + explicit. +* ``:visible-to:`` carries the only authorization fact: wallet user and + bank customer, holding the unguessable ``withdrawal-id``. The + mechanism (no credentials, capability in the URL) follows from the + actor vocabulary and the registry entry — no separate auth field. +* ``:wait:`` acknowledges long-polling and names its parameters (the + lint checks them against the ``:query:`` list). There is no push + channel for this entity; had there been one, it would be declared in + the bank's notification page, not here. +* The withdrawal operation's missing expiry timer is recorded once, at + the entity (next example) — not here, and not scattered. + +Entity, timer, and identifier anchors +------------------------------------- + +``withdrawal-operation`` (owner: bank): + +.. code-block:: rst + + .. entity:: withdrawal-operation + :owner: bank (libeufin/corebank; exposed via the + bank-integration API and reused by the terminal API) + + A bank-side operation coordinating one withdrawal: exchange and + reserve selection by the wallet, confirmation by the account + owner, wire transfer to the exchange. + + .. lifecycle:: + :terminal: aborted, confirmed + + :transition none -> pending: + :trigger: call POST /accounts/$USERNAME/withdrawals + :trigger: external bank customer creates the operation in + the bank UI + The operation exists; the ``withdrawal-id`` is distributed + via a ``taler://withdraw`` URI. + :transition pending -> selected: + :trigger: call POST /withdrawal-operation/$WITHDRAWAL_ID + The wallet submits its selection (exchange, ``reserve_pub``). + :transition selected -> confirmed: + :trigger: call POST .../withdrawals/$WITHDRAWAL_ID/confirm + :trigger: external account owner confirms (2FA) in the + bank UI + The bank registers the transfer; the exchange observes the + funded reserve through its incoming wire history + (``reserve_pub`` as wire subject). + :transition pending|selected -> aborted: + :trigger: call POST /withdrawal-operation/$WITHDRAWAL_ID/abort + :trigger: external bank-side abort + 409 ``CONFIRM_ABORT_CONFLICT`` if already confirmed — the + absorbing property of ``confirmed`` in action. + + .. timer:: operation-expiration + ⚠ not yet specified: no documented expiration exists today. + Recorded here as the single visible gap instead of silence + across all method files. + + .. identifier:: withdrawal-id + :capability: yes (unguessable; possession authorizes) + Bank-generated operation id, distributed via + ``taler://withdraw`` URIs. Created by + ``POST /accounts/$U/withdrawals`` or the bank UI; carried and + read across bank, wallet and exchange endpoints (derived from + the method files' **Identifiers:** blocks). + + .. identifier:: reserve_pub + :capability: yes + Wallet-generated reserve key; becomes the wire subject of the + funding transfer and names the reserve at the exchange. + +``tan-challenge`` (owner: corebank) is the reference example of a fully +specified entity: declared terminal set, trigger-typed transitions (call, +timer, guarded call), and two slim timer anchors whose structural facts +(fires-to, armed-by, exposed-in, gates) are all derived from transitions, +timer-refs and error entries — see the requirements document, §4. + +``order`` (owner: merchant backend) — implicit machine made explicit: + +.. code-block:: rst + + .. entity:: order + :owner: merchant backend + + .. lifecycle:: + :terminal: wired + :modifiers: refunded (any state at or after paid) + + :transition none -> unpaid: + :trigger: call POST /private/orders + :trigger: call POST /templates/$TEMPLATE_ID + Order created; a wallet can now claim it. + :transition unpaid -> claimed: + :trigger: call POST /orders/$ORDER_ID/claim + The wallet binds its nonce; contract terms signed. + :transition claimed -> unpaid: + :trigger: call POST /orders/$ORDER_ID/unclaim + The claim is released; another wallet may claim. + :transition claimed -> paid: + :trigger: call POST /orders/$ORDER_ID/pay + :if: coins cover the amount and any age/token requirements + Coins deposited; fulfillment unlocked. + :transition paid -> wired: + :trigger: watch exchange.outgoing-wire-arrivals + :trigger: call POST /private/transfers + The exchange aggregates and wires the funds; the merchant + confirms the bank credit and the backend reconciles. + + .. timer:: pay-deadline + After the deadline the order can no longer be paid. + (Armed by order creation's ``pay_deadline`` and exposed in + order status responses — both derived from timer-refs.) + + .. timer:: refund-deadline + Closes the refund window; wallet refund pickup returns 410 + afterwards. (Arming, exposure and gating all derived.) + + .. identifier:: order-id + :capability: yes + The public status/payment page is authorized by the + unguessable order id (plus claim token); management views + require bearer scopes instead — two different access + classes over the same value. + +Note that ``:modifiers:`` records orthogonal boolean flags +(``refunded``, ``confirmed``) that the API exposes alongside the main +chain — they extend the state space without entering the transition +graph. This is the same mechanism wallet transaction entities use for +``working`` and the suspended shadow states (D10.2). + +Notification pages +------------------ + +Wallet (``core/wallet-core/notifications.rst``, restructured from today's +flat list into channel form): + +.. code-block:: rst + + .. notification-channel:: wallet-message + :component: wallet-core + :registration: implicit (clients of the wallet-core message + protocol receive notifications) + :delivery: lossy-hint + :events: transaction-state-transition, exchange-state-transition, + balance-change, withdrawal-transition, ... + + All notifications are hints, not authoritative state + (``api-wallet-core.rst``): the authoritative read is the + corresponding getter — e.g. ``transaction-state-transition`` -> + ``getTransactionById``; ``balance-change`` -> ``getBalances``. + +Merchant (to be extracted from ``api-merchant.rst``): + +.. code-block:: rst + + .. notification-channel:: webhook + :component: merchant + :registration: POST /private/webhooks + :delivery: unspecified ⚠ retry/ordering/duplication semantics + are not documented today + :events: order_created, pay, refund, order_settled, + category_created, category_update, category_delete, + inventory_update, inventory_product_created, ... + + Authoritative reads: order events -> + ``GET /private/orders/$ORDER_ID``; inventory events -> + ``GET /private/products/$PRODUCT_ID``. + +Exchange (honest negative declaration): + +.. code-block:: rst + + .. notification-page:: exchange + + The exchange offers **no** push notification channels. All + observation is by pull; several status endpoints accept long-poll + parameters (``:wait: long-poll(...)`` in their method files), e.g. + ``GET /reserves/$RESERVE_PUB``, ``GET /purses/$PURSE_PUB/merge``, + ``GET /kyc-info/$ACCESS_TOKEN``. + +KYC account (owner: exchange) — the D13 machine +----------------------------------------------- + +.. code-block:: rst + + .. entity:: kyc-account + :owner: exchange (exposed via /kyc-check, /kyc-info, /kyc-spa and + the /aml/$OFFICER_PUB/... officer API; projected into the merchant's + /private/kyc status, the wallet's ExchangeWalletKycStatus and + transaction minors, and the auditor's AML-hold view) + + KYC/AML state of one bank account (wire target) at one exchange. + There are no terminal states; the account exists as long as the + exchange knows it. Officer-set properties (frozen, reported, pep, + high-risk) and investigation status are modifiers. + + .. lifecycle:: + :terminal: (none) + :modifiers: frozen, reported, pep, high-risk, under-investigation + + :transition none -> clean: + :trigger: watch bank.incoming-wire-arrivals + :trigger: call <any exchange operation naming the account> + The exchange learns the account exists; default rules apply. + :transition clean -> auth-required: + :trigger: call <any guarded operation, e.g. POST /withdraw> + :if: /keys.kyc_enabled and no KYC-auth wire transfer has + bound the account to a public key + The operation is denied (409 AUTHORIZATION_KEY_UNKNOWN or + 451 with bad_kyc_auth); the client must send a KYC-auth + transfer whose subject carries the account public key. + :transition auth-required -> clean: + :trigger: watch bank.incoming-kycauth-wires + The KYC-auth transfer binds target_pub; /kyc-check becomes + usable for this account. + :transition clean -> action-required: + :trigger: watch bank.incoming-wire-arrivals + :trigger: call POST /kyc-wallet (balance threshold pre-announcement) + :trigger: call POST /aml/$OFFICER_PUB/decision (new_measures) + :if: the active rule set's threshold for the operation type + (WITHDRAW/DEPOSIT/MERGE/BALANCE/CLOSE/AGGREGATE/REFUND/ + TRANSACTION) is crossed within its timeframe; "verboten" + measures deny instead of gating + The triggering operation is denied with 451 + LegitimizationNeededResponse; the wallet enters pending:kyc. + :transition action-required -> in-process: + :trigger: call POST /kyc-start/$ID (via the KYC SPA) + An external provider process is created with its own + expiration. + :transition in-process -> action-required: + :trigger: timer kyc-account.process-expiration + :trigger: watch kyc-provider.webhook (user-failure / + provider-failure) + :transition in-process -> clean: + :trigger: watch kyc-provider.webhook (success) + :trigger: call POST /kyc-upload/$ID (form evidence) + :trigger: call GET /kyc-proof/$PROVIDER (OAuth redirect) + The AML program evaluates the outcome and installs a new + rule set; the blocked operation may now be retried. + :transition action-required|in-process -> under-review: + :trigger: watch aml-program.outcome (to_investigate) + :trigger: call POST /aml/$OFFICER_PUB/decision + (keep_investigating) + Staff review; clients only see aml_review=true and wait. + :transition under-review -> clean: + :trigger: call POST /aml/$OFFICER_PUB/decision + The officer's new rule set supersedes the active outcome. + :transition clean -> action-required: + :trigger: timer kyc-account.rule-set-expiration + :if: a successor measure is configured + Rule-set expiry re-triggers legitimization. + + .. timer:: rule-set-expiration + LegitimizationRuleSet.expiration_time of the active outcome; + fires into the successor measure (or default rules). + .. timer:: process-expiration + Per external provider process; armed by /kyc-start, fires + in-process -> action-required. + .. timer:: kyc-auth-subject-expiration + Validity of KYC-auth wire instructions (merchant kycauth + response field ``expiration``). + .. timer:: wallet-threshold-expiration + WalletKycCheckResponse.expiration_time; the wallet's granted + balance threshold lapses and the wallet re-checks. + + .. identifier:: kyc-access-token + :capability: yes + Bearer of the SPA session; issued per wire target, distributed + via /kyc-check responses to authenticated clients. + .. identifier:: account-pub + :capability: no (bound by KYC-auth wire transfer) + The public key a KYC-auth transfer binds to the account. + +Notes: + +* ``GET /kyc-check`` carries ``:observes-state: kyc-account.status``; + its ``min_rule`` / ``lpt`` parameters are the long-poll dialect + (``:wait: long-poll(timeout_ms, min_rule, lpt)``), and ``rule_gen`` is + the monotonic rendezvous counter. +* The officer decision endpoint is ``:idempotency: unsafe`` in + appearance but guarded: 409 ``AML_DECISION_MORE_RECENT_PRESENT`` + (``:temporality: fixable``, ``:recovery: retry-modified``) forces the + officer to re-read the ledger before re-deciding. +* Challenger's address validation is a separate entity + (``address-validation``, owner: challenger) with states + ``setup / challenged / solved / failed / expired``; its OAuth code is + consumed by ``POST /token`` and its outcome reaches ``kyc-account`` + only through the ``watch kyc-provider.webhook`` trigger. + +Flow: bank-integrated withdrawal +-------------------------------- + +Entities in play: ``withdrawal-operation`` (bank, shown above), +``reserve`` (exchange), ``transaction-withdrawal`` (wallet-core). Join +key: ``reserve_pub`` — created by the wallet (``confirmWithdrawal`` / +``acceptManualWithdrawal``), carried to the bank by +``POST /withdrawal-operation/$WITHDRAWAL_ID``, becomes the wire subject +(read by the exchange's wirewatch), read by ``GET /reserves/$RESERVE_PUB`` +and ``POST /withdraw``. + +.. code-block:: rst + + .. entity:: reserve + :owner: exchange + + Funds pooled under a reserve public key by an incoming wire + transfer; drained by withdrawals. + + .. lifecycle:: + :terminal: closed + + :transition none -> open: + :trigger: watch bank.incoming-wire-arrivals + The wire subject is the reserve_pub. There is no create + call: the wallet long-polls GET /reserves/$RESERVE_PUB + (404 transient-state, recovery long-poll) until the wire + arrives. + :transition open -> closed: + :trigger: call POST /reserves/$RESERVE_PUB/close + :trigger: timer reserve.expiration + Residual value is wired back to last_origin. + + .. timer:: expiration + Armed by the funding wire (per exchange configuration); + exposed in ReserveSummary.reserve_expiration. + + .. entity:: transaction-withdrawal + :owner: wallet-core + + The wallet's withdrawal transaction group (DD37), here the + bank-integrated variant. Note the D10 extensions: working as a + modifier, implied suspend/retry edges, and watch-driven triggers. + + .. lifecycle:: + :terminal: done, failed, aborted + :modifiers: working + :standard-edges: suspend-retry + + :transition none -> dialog:proposed: + :trigger: call prepareBankIntegratedWithdrawal + The taler://withdraw URI is resolved against the + bank-integration API. + :transition dialog:proposed -> pending:bank-register-reserve/working: + :trigger: call confirmWithdrawal + The wallet creates reserve_pub and registers it with the + bank (POST /withdrawal-operation/$WITHDRAWAL_ID). + :transition pending:bank-register-reserve -> pending:bank-confirm-transfer: + :trigger: watch bank.withdrawal-operation.status (= selected) + :transition pending:bank-confirm-transfer -> pending:exchange-wait-reserve: + :trigger: watch bank.withdrawal-operation.status (= confirmed) + The account owner confirmed; the bank wires the funds. + :transition pending:exchange-wait-reserve -> pending:withdraw/working: + :trigger: watch exchange.reserve (GET /reserves/$RESERVE_PUB + long-poll returns 200) + :transition pending:withdraw -> done: + :trigger: internal coins withdrawn (POST /withdraw replay-safe) + :transition pending:* -> pending:kyc: + :trigger: call <guarded operation> + :if: exchange signals legitimization (451 / + wallet_balance_limit_without_kyc) + :transition pending:kyc -> pending:*: + :trigger: watch exchange.kyc-account (GET /kyc-check long-poll) + :transition pending:bank-confirm-transfer -> aborting:bank/working: + :trigger: call abortTransaction + :transition aborting:bank -> aborted:bank: + :trigger: watch bank.withdrawal-operation.status (= aborted) + A 409 from the bank abort means it confirmed concurrently — + the standard-edges retry path returns to exchange-wait-reserve. + +Flow: payment from a template +----------------------------- + +Entities in play: ``order`` (merchant, shown above — note its +``none -> unpaid`` transition already lists ``call POST +/templates/$TEMPLATE_ID`` as a second trigger), ``transaction-payment`` +(wallet-core). Identifiers: ``order-id`` (created by the template +instantiation), the claim ``nonce`` (created by the wallet at claim, +consumed at pay), ``h_contract_terms`` (derived from the claim response, +carried into every later public call and into the exchange's deposit). + +The template itself is a static merchant resource, not a lifecycle +entity: ``POST /private/templates`` and its siblings are CRUD with no +machine, and say so (``:idempotency: keyed:template_id`` and no Effects). + +.. code-block:: rst + + .. entity:: transaction-payment + :owner: wallet-core + + DD37 payment transaction. Demonstrates :final-reactivatable:: + a done payment is reactivated by explicit refund queries. + + .. lifecycle:: + :terminal: failed, aborted, expired + :final-reactivatable: done + :modifiers: working + :standard-edges: suspend-retry + + :transition none -> dialog:proposed: + :trigger: call preparePayForTemplateV2 + Instantiates the template at the merchant (public + POST /templates/$TEMPLATE_ID): order none -> unpaid. + :transition dialog:proposed -> pending:submit-payment/working: + :trigger: call confirmPay + The wallet claims the order (nonce bound) and signs. + :transition pending:submit-payment -> done: + :trigger: watch merchant.order (POST /orders/$ORDER_ID/pay 200) + :transition done -> pending:check-refund/working: + :trigger: call startRefundQuery + :trigger: timer order.auto-refund (auto-refund probing) + Reactivation edge out of a final-reactivatable state. + :transition pending:check-refund -> done: + :trigger: watch merchant.order.refunds (GET /orders/$ORDER_ID + long-poll await_refund_obtained) + :transition dialog:proposed -> expired: + :trigger: timer order.pay-deadline + :transition dialog:* -> dialog:waiting-for-other-wallet: + :trigger: call unclaimPayment + The claim nonce is released; another wallet may claim. + + .. timer:: auto-refund + Armed from contract terms (auto_refund); exposed to the wallet + in contract terms, fires the refund-probing reactivation. + +Flow: deposit to a bank account +-------------------------------- + +Entities in play: ``deposit`` (exchange), ``transaction-deposit`` +(wallet-core), and ``kyc-account`` (the deposit flow is its most +frequent client). Identifiers: ``h_wire`` (hash of the target payto), +``h_contract_terms``, ``wtid`` (created internally by the exchange's +aggregator, carried into the tracking response and onward to merchant +reconciliation). + +.. code-block:: rst + + .. entity:: deposit + :owner: exchange + + A batch deposit of coins to one wire target. + + .. lifecycle:: + :terminal: wired, refunded + + :transition none -> accepted: + :trigger: call POST /batch-deposit + Coins are deposited; the exchange wires after the refund + deadline and before wire_transfer_deadline. + :transition accepted -> held: + :trigger: watch aml-program.outcome + :if: aggregation KYC triggers (tracking returns 202 with + kyc_ok:false; auditor reports deferral_reason="KYC") + :transition held -> wired: + :trigger: watch exchange.kyc-account (clearance) + :transition accepted -> wired: + :trigger: timer deposit.wire-deadline (aggregation runs) + GET /deposits/$H_WIRE/... now returns 200 with wtid and + execution_time. + :transition accepted -> refunded: + :trigger: call <merchant refund before deadline> + + .. timer:: wire-deadline + Armed by BatchDepositRequest.wire_transfer_deadline ("never" + is rejected); exposed in tracking responses as execution_time. + + .. entity:: transaction-deposit + :owner: wallet-core + + DD37 deposit transaction — the KYC-richest machine: pre-submission + KYC (kyc-auth transfer) and post-submission aggregation KYC are + distinct guarded regions. + + .. lifecycle:: + :terminal: failed, aborted + :final-reactivatable: done + :modifiers: working + :standard-edges: suspend-retry + + :transition none -> pending:deposit/working: + :trigger: call createDepositGroup + :transition pending:deposit -> pending:kyc-auth: + :trigger: call POST /batch-deposit (denied) + :if: target account not yet bound (409 AUTHORIZATION_KEY_UNKNOWN) + kycAuthTransferInfo names the debit account that must send + the KYC-auth transfer. + :transition pending:kyc-auth -> pending:deposit: + :trigger: watch exchange.kyc-account (auth wire observed via + GET /kyc-check long-poll lpt=1) + :transition pending:deposit -> finalizing:track: + :trigger: watch exchange.deposit (POST /batch-deposit 200) + :transition finalizing:track -> pending:kyc: + :trigger: watch exchange.deposit (tracking 202, kyc_ok:false) + :if: aggregation KYC required + :transition pending:kyc -> finalizing:track: + :trigger: watch exchange.kyc-account (kyc_ok via lpt=2) + :transition finalizing:track -> done: + :trigger: watch exchange.deposit (tracking 200, wtid) + :transition pending:deposit -> aborting/working: + :trigger: call abortTransaction + :transition aborting -> aborted:deposit-abort-recovered: + :trigger: internal (refund + refresh completed) + :transition aborting -> done:deposit-abort-too-late: + :trigger: watch exchange.deposit (already wired) + Terminal done-with-minor: the money arrived despite the abort. + +Migration plan +============== + +Compliance snapshot (verified file counts, 2026-10): + +.. list-table:: + :header-rows: 1 + + * - Corpus + - Files + - Status vs. the requirements + * - wallet-core ops + - 152 (plus the notifications page) + - E1–E6, E11, E12 ✔; need: header options, structured errors, + ``:wait:``, effects/observation links to the DD37 machine via the + D10 extensions + * - exchange + - 65 + - E1–E7 ✔, retry/idempotency prose good → convert to fields; + need: lifecycle registry entries (reserve, purse), + ``:visible-to:``, notification page (declares: no push channels) + * - merchant + - 114 + - E1–E7 ✔; documented scopes become + ``:visible-to: merchant-staff:<scope>``; webhooks move to the + merchant notification page; need: order lifecycle, structured + errors + * - bank-side + - 65 (bank-integration 4, corebank 39, bank-wire 10, + bank-transfer 3, bank-revenue 2, bank-conversion-info 5, + account-directory 2) + - E1–E7 ✔; withdrawal/TAN states exist as unions → lifecycle + registry entries; need: error classification, timers, + cashout status gap flagged (API lacks a status field) + * - identity/misc + - 92 (challenger 7, taldir 5, mailbox 6, donau 13, ebisync 3, + auditor 58) + - mixed; challenger close to compliant; taldir/mailbox need + timers + error fields; drafts get ``:maturity:`` banners + * - sync, terminal + - 3 + 10 endpoints, inline today + - must be split into per-endpoint files first (D2) + +Steps, in order: + +1. Build the tooling in ``taler-docs/_exts/``: the new directive options, + the ``entity`` / ``lifecycle`` / ``timer`` / ``identifier`` / + ``notification-channel`` directives, and the lint (all rules warn-only + initially). +2. Backfill the bank-integrated withdrawal slice end to end: + bank-integration, corebank withdrawal + TAN, exchange reserve + + withdraw, wallet withdrawal ops, registry entries for + ``withdrawal-operation``, ``tan-challenge``, ``reserve``, + ``reserve_pub`` and ``withdrawal-id``, wallet and exchange + notification pages. Flip the lint to error for this slice. This is + the acceptance gate for the format itself. +3. Payment/refund slice: merchant order lifecycle, wallet payment and + refund ops, merchant notification page, ``order`` / ``order-id`` + anchors. +4. P2P slice: purse entities, wallet peer-push/peer-pull transaction + entities, deposit. +5. The long tail: KYC/AML (with the shared-ownership treatment of D8's + owner rule), sync + terminal splitting, auditor, drafts and banners. + +Corpus corrections folded into the requirements document +-------------------------------------------------------- + +Verification against the corpus produced these adjustments, which the +requirements document incorporates: + +* ``:maturity:`` instead of ``:status:`` (httpdomain field-name + collision; D2). +* Variant-group and proxy-stanza exemptions to the one-method-per-file + rule (D2). +* Sync and terminal APIs are to be split into per-endpoint files (D2). +* The wallet notification page exists but must be *restructured* into + channel form; it is not already compliant (D9). +* wallet-core op count is 152, not 153 (the 153rd file is + ``notifications.rst``). +* KYC state is cross-cutting and gets the projections-named-under-owner + treatment rather than a naive single owner. +* Wallet transaction entities get the four extensions of D10 and the + canonical-registry relationship to DD37 of D11. + +Test Plan +========= + +* The lint (V1–V21 in the requirements document) runs in the + documentation CI; the bank-integrated withdrawal slice (bank- + integration, corebank withdrawal + TAN, exchange reserve + withdraw, + wallet withdrawal ops, registry entries for ``reserve_pub`` and + ``withdrawal-id``) must lint clean as the acceptance gate for the + format itself. +* Codegen dry-run: generate client types and validators from the + slice's ``ts:def`` layer and diff against wallet-core's and the + exchange's corresponding wire types; mismatches fail the + implementation CI. +* Derivation smoke tests: generate the withdrawal-operation state table, + the bank-customer persona projection, and a withdrawal sequence diagram + from the corpus; review against the hand-written flow documentation. +* Wallet extension tests: lint the ``transaction-withdrawal`` registry + entry against DD37's generated tables; check that reactivation edges out + of ``done`` are legal under ``:final-reactivatable:`` and rejected under + ``:terminal:``. + +Definition of Done +================== + +* All new directives (``entity``, ``lifecycle``, ``timer``, ``identifier``, + ``notification-channel``, extended ``http:*``/``ts:op`` options) are + implemented in ``_exts/`` and render correctly. +* The lint enforces the full rule set as errors on the backfilled slices. +* Every per-endpoint file (including the newly split sync and terminal + endpoints) is compliant or carries an explicit draft/banner maturity. +* The entity registry covers at minimum the withdrawal, payment/refund, + and p2p flows end to end, including wallet transaction entities with + the D10 extensions. +* DD37's state tables are generated from the registry. +* At least one implementation repository runs codegen drift detection in + CI against the corpus. + +Alternatives +============ + +**OpenAPI/Swagger as the source of truth.** Covers the shape layer well +but has no home for lifecycles, temporality semantics, actor projections, +identifier roles, or notification doctrine — the parts this design exists +for. The ``ts:def`` layer already plays OpenAPI's shape role inside the +prose-capable Sphinx corpus; replacing the corpus would lose the prose +that remains the human interface. + +**Generate docs from code.** Inverts the authority relationship: the +design intent (idempotency guarantees, temporality classification, +capability properties) is not present in code in checkable form, so +generated docs would silently reflect implementation bugs as +specification. The spec-first model with CI drift detection keeps the +docs authoritative while still catching divergence. + +**Full anchor-side declarations** (state sets, timer facts, identifier +life stories restated at the registry). Rejected: every derivable fact +declared twice is a drift bug waiting to happen (D1). + +**Per-type substate unions instead of DD37's flat minor-state union.** +Rejected for this design: the flat global union is the deployed, +implemented vocabulary; partitioning it is a DD37 change, out of scope. + +**Modeling ``/working`` and suspended states as literal states.** +Rejected: combinatorial table explosion for zero informational gain; +orthogonal modifiers plus implied standard edges carry the same semantics +(D10.2). + +**A separate ``:auth:`` field.** Rejected: actor implies mechanism; +duplication invites contradiction (D5). + +Drawbacks +========= + +* **Authoring cost.** Compliant files are substantially more work than + prose, and ~490 files need backfill. Mitigated by incremental migration + and by the lint telling authors exactly what is missing. +* **Format rigidity.** Edge cases that do not fit (today: variant groups, + proxy stanzas, KYC's shared ownership) need explicit format extensions + rather than prose escape hatches, which slows documentation of unusual + endpoints. Accepted: silent exceptions are worse than visible ones. +* **Two specification artifacts to maintain.** This DD, the requirements + document, and (for wallets) DD37 must evolve together. Mitigated by the + normative-reference hierarchy and by generating DD37's tables from the + registry (D11). +* **Tooling must exist before the value does.** Until the lint and at + least one generator land, the new fields are overhead without payoff. + Mitigated by doing the withdrawal slice end-to-end first, tooling + included. +* **The wallet extensions add vocabulary** (``:final-reactivatable:``, + ``:standard-edges:``, modifier flags) that only wallet transaction + entities use, slightly diluting the "one vocab fits all" property. + +Discussion / Q&A +================ + +**Why not put triggers on the method files instead of the lifecycle?** +A method can honestly describe its own effects (and does, in **Effects**), +but timer firings, watches of other components, and external human actions +have no method file to live in. Causality is a property of the entity's +machine, so triggers live on transitions; the two-sided check with method +effects keeps both honest. + +**Why is long-poll not a notification channel?** +A long-poll response is authoritative state delivered late; a notification +is a hint that must be backed by a read. The delivery semantics, the lint +rules, and the generated sequence-diagram edges differ. Conflating them +would make "pull + long-poll only" components (the exchange) look like +they offer push. + +**Why does the tax auditor have no credentials?** +That is the system as designed: the auditor's access is authorized by +possession of unguessable identifiers (``wtid``). The format makes this +visible (``capability`` annotations resolving to ``:capability: yes`` +anchors) rather than hiding it in prose. + +**Are wallet transaction entities really "owned" by wallet-core if nothing +calls wallet-core to drive them?** +Yes: ownership is about authoritative state and transition execution, not +about the trigger kind. Wallet-core's DB record is the truth; its +transitions execute locally, driven by watches, timers, and user actions. +The bank-owned withdrawal operation the wallet watches is a *different* +entity with its own owner and machine — the two are joined by the +``watch`` trigger, which is exactly the cross-component causality link the +old prose never made.