taler-docs

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

105-machine-readable-endpoint-documentation.rst (62290B)


      1 DD 105: Machine-Readable Endpoint and Method Documentation
      2 ##########################################################
      3 
      4 :Design status: Draft
      5 :Implementation status: Not started
      6 :DD shepherd: TBD
      7 :Historical contributors: TBD
      8 :First published: 2026-10-06
      9 :Last substantive change: 2026-10-06
     10 :Normative references: :doc:`../core/endpoint-documentation-requirements`
     11 
     12 .. contents:: Table of Contents
     13    :depth: 2
     14 
     15 Summary
     16 =======
     17 
     18 Every HTTP endpoint and every wallet-core operation in the Taler
     19 documentation is described in exactly one ``.rst`` file, and every semantic
     20 claim in that file that tooling could need — versioning, maturity,
     21 idempotency, actor visibility, error semantics, state observation, world
     22 effects, identifier roles, timers — is expressed in a directive option or
     23 typed block, never only in prose.  A small number of registries (entities
     24 with lifecycles, timers, identifiers; per-component notification channels)
     25 hold the cross-method anchors.  From this corpus, tooling derives per-entity
     26 state tables, per-actor visibility projections, cross-component flows, and
     27 sequence diagrams, and generates client types and validators, with drift
     28 against implementations caught in CI.
     29 
     30 The format is language-agnostic: the shape layer describes wire-level types
     31 (JSON messages exchanged between client and server) via the historically
     32 named ``ts:def`` / ``ts:op`` directives, and generated artifacts may target
     33 any implementation language.
     34 
     35 This document records the *intent*, the *diagnostic of the current state*,
     36 the *design decisions*, and the *migration plan*.  The normative
     37 field-by-field format, controlled vocabularies, and lint rules are
     38 specified in :doc:`../core/endpoint-documentation-requirements`; where the
     39 two disagree during evolution, the requirements document wins for format
     40 questions and this DD wins for *why*.
     41 
     42 Motivation
     43 ==========
     44 
     45 The documentation corpus is large (~490 per-endpoint files across exchange,
     46 merchant, bank-*, corebank, challenger, taldir, mailbox, donau, auditor,
     47 plus 152 wallet-core operations) and already uniform in *shape*: identity
     48 directive, request, response, prose details.  But the semantics a reader
     49 most needs are locked in prose or absent:
     50 
     51 * **Error codes are overloaded across flows.**  A 404 on an unfunded
     52   reserve means "wait"; a 404 on an expired order means "gone"; a 402 on
     53   the order status page is the *normal* unpaid state.  Same codes, opposite
     54   meanings, not inferable.
     55 * **Retry safety is never stated.**  ``POST /withdraw`` is replay-safe by
     56   careful design; ``POST /private/orders`` without an ``order_id``
     57   duplicates.  In a system built on at-least-once networking, this is the
     58   most frequent operational question a client faces, and the corpus answers
     59   it only occasionally, in prose.
     60 * **State lives between methods.**  No per-endpoint file can honestly
     61   describe the machine the endpoints collectively implement — the order
     62   lifecycle, the withdrawal operation, the TAN challenge — so those
     63   machines exist only implicitly, and cross-component causality (the
     64   exchange's wirewatch driving a bank-owned state; the merchant tracking
     65   exchange wire arrivals) exists only as narrative.
     66 * **Docs and code drift silently.**  Doc-versus-code audits on this corpus
     67   repeatedly find stale state names, missing error codes, and endpoints
     68   whose real behavior has moved on.  One-sided declarations rot; the fix is
     69   two-sided, machine-checkable declarations.
     70 * **Deriving anything requires reading prose.**  Use-case walkthroughs,
     71   persona-specific API surfaces ("what can a tax auditor see?"), state
     72   tables, and sequence diagrams are today hand-authored and immediately
     73   stale.
     74 
     75 The goal is a documentation corpus that is the *authoritative source of
     76 truth* in the spec-first sense: implementations conform to it, code is
     77 generated from its shape layer, and derived artifacts are produced without
     78 reading prose.
     79 
     80 Current state of the corpus
     81 ===========================
     82 
     83 This section is the diagnostic: what the corpus provides today, and which
     84 gaps the requirements document closes.
     85 
     86 What files contain today
     87 ------------------------
     88 
     89 The current corpus already provides, per file:
     90 
     91 .. list-table::
     92    :header-rows: 1
     93 
     94    * - ID
     95      - Element
     96      - Encoding today
     97    * - E1
     98      - Method identity (HTTP method + path / op)
     99      - ``.. http:get:: /p`` / ``.. ts:op:: op``
    100    * - E2
    101      - One-line + prose description
    102      - directive body
    103    * - E3
    104      - Request parameters
    105      - ``:query:``, body as ``.. ts:def::``
    106    * - E4
    107      - Response per status code
    108      - ``:http:statuscode:`` + ``ts:def``
    109    * - E5
    110      - Error codes
    111      - ``TALER_EC_*`` names in status prose
    112    * - E6
    113      - Field-level versioning
    114      - ``@since **vN**`` / ``@deprecated`` comments
    115    * - E7
    116      - Chapter-level version history
    117      - "Version History" section of ``api-*.rst``
    118    * - E8
    119      - Chapter-level auth model
    120      - prose section of ``api-*.rst``
    121    * - E9
    122      - Long-poll parameters
    123      - ``:query timeout_ms:`` + ``:ref:`` to convention
    124    * - E10
    125      - Idempotency
    126      - *occasionally*, prose only (e.g. ``post-withdraw.rst``)
    127    * - E11
    128      - Read-only / deprecated flags (wallet ops)
    129      - ``:read-only:``, ``:deprecated:`` on ``ts:op``
    130    * - E12
    131      - Expected errors + side effects (wallet ops)
    132      - prose lists in **Details:**
    133 
    134 Verified against the corpus (2026-10): the only options the identity
    135 directives accept today are ``deprecated``/``noindex``/``synopsis`` (HTTP
    136 domain) and ``read-only``/``deprecated`` (``ts:op``).  None of the new
    137 header options, and none of the ``entity`` / ``lifecycle`` / ``timer`` /
    138 ``identifier`` / ``notification-channel`` directives, exist anywhere
    139 outside the specification documents themselves — all are new
    140 ``_exts/`` work.  ``core/entities.rst`` does not exist.
    141 ``core/wallet-core/notifications.rst`` exists, but as a flat list of
    142 notification types with payload definitions — no channel, delivery, or
    143 event structure — and must be restructured into the channel form.
    144 
    145 Gaps to close
    146 -------------
    147 
    148 .. list-table::
    149    :header-rows: 1
    150 
    151    * - ID
    152      - Missing element
    153      - Why derivation needs it
    154    * - G1
    155      - Method maturity (stable/draft/...)
    156      - drafts must be excludable from derivation
    157    * - G2
    158      - Per-method actor visibility
    159      - persona closures are computed from who may call what
    160    * - G3
    161      - Error semantics
    162      - same status/code is transient in one flow, terminal in another,
    163        and sometimes not an error at all (progress); cannot be inferred
    164    * - G4
    165      - Idempotency label
    166      - retries are safe or not — never guessable
    167    * - G5
    168      - Lifecycles with triggers and guards
    169      - the entity machine: what causes each transition, and when that
    170        path exists at all (config/data forks such as KYC thresholds)
    171    * - G6
    172      - Timers
    173      - expiry/deadline edges of state tables
    174    * - G7
    175      - Observation channels
    176      - how an actor *learns* a state changed
    177    * - G8
    178      - Identifier joins
    179      - which values are created/carried/read/consumed where —
    180        the cross-component join keys
    181    * - G9
    182      - Effects (structured)
    183      - what changed in the world beyond the response
    184 
    185 Structural mismatches found by verification
    186 -------------------------------------------
    187 
    188 * **~20 files violate one-method-per-file as-is**, some deliberately:
    189   ``auditor/delete-monitoring-records.rst`` carries 24 identity directives
    190   (one per monitoring-record type); KYC webhook and AML transfer files
    191   carry 2–4; long-poll variants (e.g. ``get-purses-PURSE_PUB-merge.rst``)
    192   are documented as two directives in one file.
    193 * **~12 files are proxy stanzas**, not methods: ``.. http:any::`` wildcard
    194   reverse-proxy delegation declarations (``merchant/any-star.rst``, seven
    195   corebank ``any-...-star.rst`` files) with no request/response semantics.
    196 * **sync and terminal have no per-endpoint files at all**: 3 and 10
    197   endpoints respectively live inline in ``api-sync.rst`` /
    198   ``api-terminal.rst``.
    199 * **KYC state is cross-cutting**: 23+ KYC-related files across six
    200   components; exchange, merchant, bank-wire, and wallet all hold
    201   projections, so naive single-owner modeling fails.
    202 * **Wallet transaction machines** (DD37) need four lifecycle-model
    203   extensions; see D10.
    204 
    205 Requirements
    206 ============
    207 
    208 R1  Each endpoint/method is documented in exactly one ``.rst`` file whose
    209     semantic content is completely machine-readable.
    210 
    211 R2  From the corpus, without reading prose, tooling can derive:
    212     per-entity state tables (states, transitions, timers, failure exits),
    213     per-actor observation projections, cross-component identifier flows,
    214     and sequence diagrams (messages from transitions, ``alt`` from guards,
    215     ``loop`` from timers).
    216 
    217 R3  The shape layer (``ts:def`` types, request/response schemas) is
    218     complete enough to generate client/server types and validators; drift
    219     is caught by CI in the implementation repositories.
    220 
    221 R4  Documented *intent* (drafts, unimplemented proposals) is visibly and
    222     mechanically distinguishable from documented *behavior*, and excluded
    223     from derivation.
    224 
    225 R5  The format must cover the existing corpus without exceptions that
    226     swallow rules: HTTP APIs, wallet-core operations, long-poll dialects,
    227     non-2xx progress signals, capability-gated access, config/data forks
    228     (KYC thresholds), and wallet transaction lifecycles per DD37.
    229 
    230 R6  Every cross-reference a lint can check must be resolvable: entity
    231     names, state values, timers, identifiers, trigger targets, channel
    232     events.  Free strings are lint errors.
    233 
    234 R7  Migration is incremental: the current corpus is mostly compliant in
    235     shape, and backfill proceeds slice by slice (bank-integrated withdrawal
    236     first), with the lint warn-only until a slice is done.
    237 
    238 Proposed Solution
    239 =================
    240 
    241 The full format is normatively specified in
    242 :doc:`../core/endpoint-documentation-requirements`.  This section states
    243 the design as a sequence of decisions and their reasons.
    244 
    245 D1: Derive edges, declare anchors
    246 ---------------------------------
    247 
    248 The organizing philosophy.  Anything that is an *edge* — a transition, an
    249 arming, an observation, a carriage of a value — is written exactly once, on
    250 the edge itself (the lifecycle transition, the method header, the effect,
    251 the identifier tag).  Anything that acts as a *resolution anchor* — an
    252 entity, a timer name, an identifier, a notification channel — is declared
    253 once, minimally, so edges can be checked against it.  If a fact can be
    254 derived from edges it is never also declared; if a name must be resolvable
    255 it is never left implicit.
    256 
    257 This is what keeps the system from collapsing under its own bookkeeping:
    258 timers are slim anchors (name + prose) because their armed-by, exposed-in,
    259 fires-to, and gates facts are all derivable from timer-refs, error entries,
    260 and lifecycle triggers; identifiers are name + ``:capability:`` + prose
    261 because their created-by/carried-by/read-by/consumed-by story is the union
    262 of method-side tags.  The alternative — restating everything at the anchor —
    263 guarantees drift.
    264 
    265 D2: One file per method, machine-readable header
    266 ------------------------------------------------
    267 
    268 Each method's identity card is its existing directive (``.. http:get::`` /
    269 ``.. ts:op::``) extended with options: version (``:since:`` /
    270 ``:deprecated:``), maturity, idempotency, actor visibility, observation
    271 binding, timer relationships, and long-poll dialect.  Every option exists
    272 because a named consumer needs it (lint, generator, exclusion filter); none
    273 is decorative.
    274 
    275 **Decision: the maturity option is named ``:maturity:``, not
    276 ``:status:``.**  The vendored sphinxcontrib-httpdomain in ``_exts/``
    277 already accepts ``:status:`` as a field-name alias for status codes
    278 (``names=('statuscode','status','code')``); a new header option with that
    279 name collides.
    280 
    281 **Decision: file-naming and one-directive-per-file rules get two explicit
    282 exemptions, not silent violations.**
    283 
    284 * *Variant groups*: a small number of files legitimately document several
    285   directive instances of one logical method
    286   (``auditor/delete-monitoring-records.rst`` with 24 record types, KYC
    287   webhook provider variants, long-poll variants such as
    288   ``exchange/get-purses-PURSE_PUB-merge.rst``).  These are declared
    289   variant groups and linted as one method with variants, rather than
    290   being split into near-identical files.
    291 * *Proxy stanzas*: ~12 files using ``.. http:any::`` with wildcard paths
    292   (``merchant/any-star.rst``, the corebank ``any-...-star.rst`` reverse-
    293   proxy delegation declarations) are not methods with request/response
    294   semantics.  They get their own minimal stanza class and are excluded
    295   from request/response/idempotency requirements.
    296 
    297 **Decision: sync and terminal are split.**  Both APIs currently live inline
    298 in their chapter files (3 and 10 endpoints).  They are split into
    299 per-endpoint files like every other API; chapter files keep only version
    300 history and shared conventions.
    301 
    302 D3: Errors carry temporality and recovery
    303 -----------------------------------------
    304 
    305 Every non-2xx outcome — including codes that are *not failures* (202
    306 pending, 304 unchanged, 402 unpaid) — gets a structured entry with
    307 ``:temporality:`` (transient-state / terminal / fixable / rate-limited /
    308 gate / progress) and ``:recovery:`` (retry-same / retry-modified /
    309 retry-after / long-poll / poll-later / refresh-config / start-kyc /
    310 user-action / abort / give-up / none).  This pair is what makes the failure
    311 and timer columns of derived state tables computable instead of guessed,
    312 and what tells the generator when *not* to derive a failure edge
    313 (``progress`` entries are state signals, not errors).
    314 
    315 D4: Idempotency is a first-class label
    316 --------------------------------------
    317 
    318 ``readonly`` / ``replay-safe`` / ``keyed:<field>`` / ``unsafe``.  Retries
    319 are the universal response to network failure, and a request that
    320 arrived-but-lost-its-response is indistinguishable from one that never
    321 arrived; whether a verbatim replay is safe decides whether the client's
    322 failure branch exists at all.  The label also flags design smells: an
    323 ``unsafe`` mutating method in this ecosystem deserves review, and the
    324 lint requires the field on every mutating method.
    325 
    326 D5: Authorization is one fact: the actor
    327 ----------------------------------------
    328 
    329 ``:visible-to:`` declares the actor surface (wallet-user,
    330 merchant-staff[:scope], aml-officer[:readonly], exchange-operator, auditor,
    331 tax-auditor, bank-customer, bank-admin, kyc-provider, any-unauthenticated,
    332 external), and the credential mechanism is *implied* by a fixed table.
    333 Declaring both actor and mechanism would duplicate one fact in two
    334 drift-prone places.  Capability-gated access (unguessable identifiers such
    335 as ``wtid`` or ``withdrawal-id``) is annotated in parentheses and resolved
    336 against the identifier registry, which is where the ``:capability:``
    337 property of the *value* lives.  Persona projections are computed as
    338 closures over this field.
    339 
    340 D6: Methods bind to entity machines in exactly two ways
    341 -------------------------------------------------------
    342 
    343 *Mutators* declare transitions in an **Effects** block (entity, exact edge,
    344 observation endpoint).  *Readers* declare an observation binding
    345 (``:observes-state: <entity>.<field>``) stating that the response *is* a
    346 view onto that entity's machine.  Both are checked two-sided against the
    347 entity registry: the lifecycle says "call X drives A→B", the method says
    348 "I drive A→B on entity E", and disagreement is a lint error.  One-sided
    349 declarations silently rot; that is the lesson of every doc-versus-code
    350 audit on this corpus.
    351 
    352 D7: Identifiers are the join keys
    353 ---------------------------------
    354 
    355 Methods tag their role in each identifier's life: ``creates`` / ``carries``
    356 / ``reads`` / ``consumes``.  The union over ~490 files yields the
    357 cross-component value-flow graph and the lint's join checks (exactly one
    358 creator per carried/read value, or an internal-creation note at the anchor
    359 for values like ``wtid`` minted by the exchange's aggregator).  The
    360 registry holds only the anchor: name, ``:capability:``, prose.
    361 
    362 D8: Entities, lifecycles, triggers, guards
    363 ------------------------------------------
    364 
    365 The entity registry declares, per entity: ``:owner:`` (the component
    366 holding *authoritative* state and executing transitions — not merely where
    367 the entity is displayed), a transition list, a declared terminal set, timer
    368 anchors, and identifier anchors.  States are *inferred* from transitions;
    369 terminal states are *declared* so that use-case success conditions are
    370 anchored, the absorbing property is checkable, and truncated machines are
    371 visible (a non-terminal state with no outgoing edges is a lint warning).
    372 
    373 Every transition names its *trigger* — ``call <method>``, ``timer
    374 <entity>.<timer>``, ``watch <component>.<observable>``, or ``external
    375 <description>`` — and optionally a guard (``:if:``) for config/data forks
    376 (KYC on/off, thresholds, feature flags).  Triggers are the causality layer
    377 no per-endpoint doc can see locally; guards keep one canonical machine with
    378 guarded edges instead of a combinatorial family of near-identical machines.
    379 
    380 D9: Notifications live on per-component pages
    381 ---------------------------------------------
    382 
    383 How an actor *learns* that state changed is declared once per component:
    384 channel, registration, delivery semantics (``lossy-hint`` /
    385 ``at-least-once`` / ``rendezvous`` / honestly-``unspecified``), events, and
    386 — critically — the *authoritative read* backing each event family, per the
    387 doctrine *signals are hints; reads are truth*.  Method files never declare
    388 push channels; the link from effect to signal is derived.  Long-poll is not
    389 a notification channel: it is a property of the read endpoint (``:wait:``),
    390 since a long-poll response is authoritative state delivered late, not a
    391 hint.  Components with no push channels (the exchange) declare that
    392 negative fact explicitly.
    393 
    394 D10: Wallet transaction lifecycles need four model extensions
    395 -------------------------------------------------------------
    396 
    397 The wallet-core variant maps HTTP concepts onto ``ts:op`` files, with
    398 effects expressed as transitions on per-transaction-type entities
    399 (``transaction-withdrawal``, ``transaction-payment``, ...) whose state
    400 vocabulary is DD37's ``major[:minor][/working]``.  Verification against
    401 DD37 and the wallet-core implementation surfaced four places where the
    402 base lifecycle model does not fit; each gets an explicit extension rather
    403 than a workaround:
    404 
    405 **D10.1  Final states may be reactivatable.**  DD37 ``done`` is not
    406 absorbing: a payment is reactivated by refund queries and
    407 ``rebind-session``; a deposit in ``finalizing:track`` can fall back to
    408 ``pending`` when new KYC requirements appear.  The lifecycle model
    409 therefore distinguishes ``:terminal:`` (absorbing) from
    410 ``:final-reactivatable:`` (success anchor for use cases, but reactivation
    411 edges are legal and must be listed).  The absorbing lint applies only to
    412 ``:terminal:``.
    413 
    414 **D10.2  ``/working`` and the suspended shadow states are orthogonal
    415 modifiers, not states.**  DD37's public state value is a tuple
    416 ``(major, minor, working)``; ``working`` is a transitional presentation
    417 flag explicitly slated to become major states later, and nearly every
    418 non-terminal state has paired ``suspended`` / ``suspended-aborting`` /
    419 ``suspended-finalizing`` shadow states plus retry self-loops that DD37
    420 deliberately omits from its diagrams.  Encoding all of this as literal
    421 states and transitions would explode every table.  Instead, wallet
    422 transaction entities declare:
    423 
    424 * ``working`` as a lifecycle-level orthogonal flag (same mechanism as the
    425   order entity's ``:modifiers:``);
    426 * a ``:standard-edges: suspend-retry`` declaration that *implies* the
    427   suspend/resume/retry edge family on every non-final state, so the
    428   inferred state set and edge set include them without authoring them.
    429 
    430 State values in ``:transition:`` fields use DD37 notation, and DD37's
    431 state-*expression* language (``pending:*``, ``pending:-``, ``/idle``,
    432 comma alternation) is accepted wherever a state set is matched.
    433 
    434 **D10.3  Ownership is inside-out, and that is fine.**  A wallet
    435 transaction entity is owned by wallet-core (it holds the authoritative
    436 record and executes transitions), but its triggers are almost entirely
    437 ``watch <component>.<observable>`` (bank withdrawal operations, exchange
    438 reserves/KYC, merchant refunds, *another wallet* in p2p) plus local user
    439 actions and timers — the inverse of the tan-challenge reference example
    440 where the owner executes on ``call``.  This is not a violation of the
    441 owner rule but its second mode, and the worked examples below include a
    442 watch-driven wallet entity alongside call-driven bank entities.
    443 Corollaries: one logical p2p transfer is two entities (debit and credit)
    444 with no shared machine ("public states describe each wallet's local
    445 progress", DD37), and hidden child transactions (``internal-withdrawal``)
    446 are separate entities, not states of the parent.
    447 
    448 **D10.4  Some truths are not derivable from the machine.**  Three are
    449 declared as explicit non-goals of derivation for wallet transactions:
    450 
    451 * ``txActions`` are computed per state *and* context and are **not**
    452   derivable from the lifecycle (DD37: frontends must use the returned
    453   ``txActions``).  No "possible actions" column is generated.
    454 * ``deleted`` is notification-only, never a stored state; its observation
    455   is the *absence* of the record (``WALLET_TRANSACTION_NOT_FOUND``).  The
    456   observation model gains the notion "absence as signal" for exactly this
    457   case (and wallet-core's ``none`` pseudo-state appearing in transition
    458   notifications).
    459 * Internal states are intentionally collapsed on exposure (e.g.
    460   redenomination phases report as ``pending:withdraw/working``).  The
    461   registry describes the *public* machine; the mapping from internal
    462   status enums is implementation detail, checked by CI drift detection,
    463   not authored in docs.
    464 
    465 D11: DD37's tables are generated, not mirrored
    466 ----------------------------------------------
    467 
    468 The registry entries for wallet transaction entities become the canonical
    469 machine-readable form of DD37's machines, and DD37's per-type state tables
    470 are *generated from the registry* (with DD37 remaining the human-oriented
    471 prose and diagrams).  Re-encoding DD37 by hand in the registry would create
    472 the very two-sided drift problem this system exists to eliminate.  The
    473 lint's wallet rule resolves state *names* against the
    474 ``get-transaction-by-id.rst`` enums and state *edges* against the registry.
    475 
    476 D12: Lint as build checks, warn-only during migration
    477 -----------------------------------------------------
    478 
    479 A doc lint in ``taler-docs/_exts/`` enforces the rules (identity,
    480 vocabulary, resolvability, two-sided lifecycle agreement, identifier joins,
    481 lifecycle integrity, filename/location) as Sphinx build warnings/errors.
    482 New rules land warn-only per slice and flip to error when the slice is
    483 backfilled.
    484 
    485 D13: KYC/AML is one exchange-owned entity; decisions are data, not states
    486 -------------------------------------------------------------------------
    487 
    488 KYC is the hardest entity to model because its state is genuinely
    489 cross-cutting: the wallet blocks transactions on it, the merchant shows a
    490 status page for it, the bank carries its wire transfers, the auditor
    491 observes its holds, and AML officers act on it.  Verification against the
    492 exchange's actual schema (DD23) shows that the authoritative state lives
    493 entirely in the exchange: per wire target (``h_payto``), the exchange holds
    494 the access token, the currently triggered legitimization measures, the
    495 active decision/rule-set ledger, and the external provider processes.
    496 Everything else — the wallet's ``ExchangeWalletKycStatus`` and transaction
    497 minors, the merchant's ``MerchantAccountKycRedirect.status`` enum, the
    498 KYC SPA's requirement list, the auditor's AML-hold view — is a projection,
    499 named as such under the owner (per the owner rule's projection clause).
    500 
    501 **Decision: one entity, ``kyc-account``, owned by the exchange.**  Its
    502 observable surface is ``GET /kyc-check/$H_NORMALIZED_PAYTO`` (200 = no
    503 mandatory action, 202 = redirect user to legitimization, 409 = account not
    504 yet bound to a public key, i.e. KYC-auth wire transfer required) plus the
    505 ``aml_review`` flag, the monotonic ``rule_gen`` counter (the long-poll
    506 rendezvous), and the access token that authorizes the SPA.  Its states are
    507 client-meaningful, not DB-meaningful: ``clean``, ``auth-required``,
    508 ``action-required`` (measures open), ``in-process`` (external provider
    509 engaged), ``under-review`` (``aml_review``).  There are no terminal states:
    510 a bank account never stops existing; dormancy and closure are modifiers.
    511 
    512 **Decision: officer decisions and rule sets are guards and modifiers, not
    513 states.**  An AML officer decision (``POST /aml/$OFFICER_PUB/decision``)
    514 does four things: installs a new ``LegitimizationRuleSet`` (which *changes
    515 which transitions exist* — hence ``:if:`` guards on threshold rules, not
    516 new states), sets account properties (``is_frozen``, ``was_reported``,
    517 ``pep``, ``high_risk`` — declared as ``:modifiers:`` on ``kyc-account``),
    518 triggers immediate measures (a ``call`` trigger into ``action-required``),
    519 and sets ``keep_investigating`` (the ``under-investigation`` modifier).
    520 This is exactly how the implementation works (the active row of
    521 ``legitimization_outcomes`` is superseded, not stacked): decisions rewrite
    522 the machine's guard data.  The concurrency guard of the API (409
    523 ``AML_DECISION_MORE_RECENT_PRESENT``) is an ordinary ``fixable`` error with
    524 ``retry-modified`` recovery (re-read, then re-decide).  Officer
    525 provisioning (``POST /management/aml-officers``, master-key signed) is the
    526 ``exchange-operator`` actor acting on the officer registry — separate
    527 entity, not part of ``kyc-account``.
    528 
    529 **Decision: projections may be restricted or lossy, and that is
    530 declared.**  The KYC SPA's projection is *deliberately* restricted (the
    531 law may forbid disclosing true status; ``aml_review`` is only a hint), so
    532 ``:observes-state:`` on the SPA endpoints is annotated as restricted.  The
    533 merchant's status enum mixes entity states (``kyc-required``,
    534 ``awaiting-aml-review``) with observation failures (``exchange-unreachable``,
    535 ``logic-bug``); the lint resolves only the former against the lifecycle —
    536 projection-local failure members are declared at the observing method, not
    537 added to the entity's machine.  The wallet deliberately conflates
    538 "user must act" with "staff review in progress" in ``pending:kyc``; the
    539 docs must not model it as user-actionable only.
    540 
    541 **Decision: Challenger is a separate entity, plugged in by trigger.**  An
    542 address-validation process (Challenger or another provider) has its own
    543 machine (``setup → challenged → solved/failed/expired``, OAuth-flavored),
    544 owned by the provider component.  It never decides KYC: its outcome enters
    545 ``kyc-account`` via ``watch kyc-provider.webhook`` / ``call
    546 GET /kyc-proof/$PROVIDER`` triggers.  Provider processes have their own
    547 expiration timers, distinct from the account's rule-set expiration.
    548 
    549 Worked examples
    550 ===============
    551 
    552 These examples show the target format applied to real corpus content.
    553 They illustrate the requirements document; they are not themselves
    554 normative additions to it.
    555 
    556 A compliant method file
    557 -----------------------
    558 
    559 ``core/bank-integration/get-withdrawal-operation-WITHDRAWAL_ID.rst``
    560 brought into compliance — added structure marked with ``※``:
    561 
    562 .. code-block:: rst
    563 
    564    .. http:get:: /withdrawal-operation/$WITHDRAWAL_ID
    565       :since: v1
    566       :maturity: stable
    567       :idempotency: readonly
    568       :observes-state: withdrawal-operation.status              ※
    569       :visible-to: wallet-user, bank-customer (capability withdrawal-id)
    570       :wait: long-poll(timeout_ms, old_state)                   ※
    571 
    572      Query information about a withdrawal operation, identified by the
    573      ``WITHDRAWAL_ID``.
    574 
    575      **Request:**
    576 
    577      :query timeout_ms: *Optional.*
    578        Timeout in milliseconds, for :ref:`long-polling <long-polling>`,
    579        to wait for operation state to be different from ``old_state``.
    580        Since protocol **v3**.
    581      :query old_state:
    582        *Optional.* Defaults to "pending".
    583      :query long_poll_ms: *Optional.*
    584        Deprecated in protocol **v3**. Use *timeout_ms* instead.
    585 
    586      **Response:**
    587 
    588      :http:statuscode:`200 OK`:
    589        :schema: BankWithdrawalOperationStatus                  ※
    590        The withdrawal operation is known to the bank, and details
    591        are given in the `BankWithdrawalOperationStatus` response body.
    592 
    593      **Errors:**                                               ※
    594 
    595      :error 404 none:
    596        :temporality: terminal
    597        :recovery: give-up
    598        The operation was not found.  (A not-yet-propagated operation
    599        id may also 404 briefly; clients that just received the id from
    600        a ``taler://withdraw`` URI may retry briefly.)
    601 
    602      **Identifiers:**                                          ※
    603 
    604      :identifier withdrawal-id:
    605        :role: reads
    606        :registry: :ref:`identifier-withdrawal-id`
    607        The lookup key of the operation, conveyed via
    608        ``taler://withdraw``.
    609      :identifier reserve_pub:
    610        :role: reads
    611        :registry: :ref:`identifier-reserve-pub`
    612        Wallet-selected reserve public key, returned here as part of
    613        the operation state.
    614 
    615      **Details:**
    616 
    617      .. ts:def:: BankWithdrawalOperationStatus
    618        ... (unchanged) ...
    619 
    620 Notes on the example:
    621 
    622 * The 404 entry shows the G3 problem and its resolution: the *same*
    623   code is terminal for a stale id and transient for a not-yet-visible
    624   one — the condition is documented, the default classification is
    625   explicit.
    626 * ``:visible-to:`` carries the only authorization fact: wallet user and
    627   bank customer, holding the unguessable ``withdrawal-id``.  The
    628   mechanism (no credentials, capability in the URL) follows from the
    629   actor vocabulary and the registry entry — no separate auth field.
    630 * ``:wait:`` acknowledges long-polling and names its parameters (the
    631   lint checks them against the ``:query:`` list).  There is no push
    632   channel for this entity; had there been one, it would be declared in
    633   the bank's notification page, not here.
    634 * The withdrawal operation's missing expiry timer is recorded once, at
    635   the entity (next example) — not here, and not scattered.
    636 
    637 Entity, timer, and identifier anchors
    638 -------------------------------------
    639 
    640 ``withdrawal-operation`` (owner: bank):
    641 
    642 .. code-block:: rst
    643 
    644    .. entity:: withdrawal-operation
    645       :owner: bank (libeufin/corebank; exposed via the
    646       bank-integration API and reused by the terminal API)
    647 
    648       A bank-side operation coordinating one withdrawal: exchange and
    649       reserve selection by the wallet, confirmation by the account
    650       owner, wire transfer to the exchange.
    651 
    652       .. lifecycle::
    653          :terminal: aborted, confirmed
    654 
    655          :transition none -> pending:
    656            :trigger: call POST /accounts/$USERNAME/withdrawals
    657            :trigger: external bank customer creates the operation in
    658              the bank UI
    659            The operation exists; the ``withdrawal-id`` is distributed
    660            via a ``taler://withdraw`` URI.
    661          :transition pending -> selected:
    662            :trigger: call POST /withdrawal-operation/$WITHDRAWAL_ID
    663            The wallet submits its selection (exchange, ``reserve_pub``).
    664          :transition selected -> confirmed:
    665            :trigger: call POST .../withdrawals/$WITHDRAWAL_ID/confirm
    666            :trigger: external account owner confirms (2FA) in the
    667              bank UI
    668            The bank registers the transfer; the exchange observes the
    669            funded reserve through its incoming wire history
    670            (``reserve_pub`` as wire subject).
    671          :transition pending|selected -> aborted:
    672            :trigger: call POST /withdrawal-operation/$WITHDRAWAL_ID/abort
    673            :trigger: external bank-side abort
    674            409 ``CONFIRM_ABORT_CONFLICT`` if already confirmed — the
    675            absorbing property of ``confirmed`` in action.
    676 
    677       .. timer:: operation-expiration
    678          ⚠ not yet specified: no documented expiration exists today.
    679          Recorded here as the single visible gap instead of silence
    680          across all method files.
    681 
    682       .. identifier:: withdrawal-id
    683          :capability: yes (unguessable; possession authorizes)
    684          Bank-generated operation id, distributed via
    685          ``taler://withdraw`` URIs.  Created by
    686          ``POST /accounts/$U/withdrawals`` or the bank UI; carried and
    687          read across bank, wallet and exchange endpoints (derived from
    688          the method files' **Identifiers:** blocks).
    689 
    690       .. identifier:: reserve_pub
    691          :capability: yes
    692          Wallet-generated reserve key; becomes the wire subject of the
    693          funding transfer and names the reserve at the exchange.
    694 
    695 ``tan-challenge`` (owner: corebank) is the reference example of a fully
    696 specified entity: declared terminal set, trigger-typed transitions (call,
    697 timer, guarded call), and two slim timer anchors whose structural facts
    698 (fires-to, armed-by, exposed-in, gates) are all derived from transitions,
    699 timer-refs and error entries — see the requirements document, §4.
    700 
    701 ``order`` (owner: merchant backend) — implicit machine made explicit:
    702 
    703 .. code-block:: rst
    704 
    705    .. entity:: order
    706       :owner: merchant backend
    707 
    708       .. lifecycle::
    709          :terminal: wired
    710          :modifiers: refunded (any state at or after paid)
    711 
    712          :transition none -> unpaid:
    713            :trigger: call POST /private/orders
    714            :trigger: call POST /templates/$TEMPLATE_ID
    715            Order created; a wallet can now claim it.
    716          :transition unpaid -> claimed:
    717            :trigger: call POST /orders/$ORDER_ID/claim
    718            The wallet binds its nonce; contract terms signed.
    719          :transition claimed -> unpaid:
    720            :trigger: call POST /orders/$ORDER_ID/unclaim
    721            The claim is released; another wallet may claim.
    722          :transition claimed -> paid:
    723            :trigger: call POST /orders/$ORDER_ID/pay
    724            :if: coins cover the amount and any age/token requirements
    725            Coins deposited; fulfillment unlocked.
    726          :transition paid -> wired:
    727            :trigger: watch exchange.outgoing-wire-arrivals
    728            :trigger: call POST /private/transfers
    729            The exchange aggregates and wires the funds; the merchant
    730            confirms the bank credit and the backend reconciles.
    731 
    732       .. timer:: pay-deadline
    733          After the deadline the order can no longer be paid.
    734          (Armed by order creation's ``pay_deadline`` and exposed in
    735          order status responses — both derived from timer-refs.)
    736 
    737       .. timer:: refund-deadline
    738          Closes the refund window; wallet refund pickup returns 410
    739          afterwards.  (Arming, exposure and gating all derived.)
    740 
    741       .. identifier:: order-id
    742          :capability: yes
    743          The public status/payment page is authorized by the
    744          unguessable order id (plus claim token); management views
    745          require bearer scopes instead — two different access
    746          classes over the same value.
    747 
    748 Note that ``:modifiers:`` records orthogonal boolean flags
    749 (``refunded``, ``confirmed``) that the API exposes alongside the main
    750 chain — they extend the state space without entering the transition
    751 graph.  This is the same mechanism wallet transaction entities use for
    752 ``working`` and the suspended shadow states (D10.2).
    753 
    754 Notification pages
    755 ------------------
    756 
    757 Wallet (``core/wallet-core/notifications.rst``, restructured from today's
    758 flat list into channel form):
    759 
    760 .. code-block:: rst
    761 
    762    .. notification-channel:: wallet-message
    763       :component: wallet-core
    764       :registration: implicit (clients of the wallet-core message
    765       protocol receive notifications)
    766       :delivery: lossy-hint
    767       :events: transaction-state-transition, exchange-state-transition,
    768                balance-change, withdrawal-transition, ...
    769 
    770       All notifications are hints, not authoritative state
    771       (``api-wallet-core.rst``): the authoritative read is the
    772       corresponding getter — e.g. ``transaction-state-transition`` ->
    773       ``getTransactionById``; ``balance-change`` -> ``getBalances``.
    774 
    775 Merchant (to be extracted from ``api-merchant.rst``):
    776 
    777 .. code-block:: rst
    778 
    779    .. notification-channel:: webhook
    780       :component: merchant
    781       :registration: POST /private/webhooks
    782       :delivery: unspecified  ⚠ retry/ordering/duplication semantics
    783         are not documented today
    784       :events: order_created, pay, refund, order_settled,
    785                category_created, category_update, category_delete,
    786                inventory_update, inventory_product_created, ...
    787 
    788       Authoritative reads: order events ->
    789       ``GET /private/orders/$ORDER_ID``; inventory events ->
    790       ``GET /private/products/$PRODUCT_ID``.
    791 
    792 Exchange (honest negative declaration):
    793 
    794 .. code-block:: rst
    795 
    796    .. notification-page:: exchange
    797 
    798    The exchange offers **no** push notification channels.  All
    799    observation is by pull; several status endpoints accept long-poll
    800    parameters (``:wait: long-poll(...)`` in their method files), e.g.
    801    ``GET /reserves/$RESERVE_PUB``, ``GET /purses/$PURSE_PUB/merge``,
    802    ``GET /kyc-info/$ACCESS_TOKEN``.
    803 
    804 KYC account (owner: exchange) — the D13 machine
    805 -----------------------------------------------
    806 
    807 .. code-block:: rst
    808 
    809    .. entity:: kyc-account
    810       :owner: exchange (exposed via /kyc-check, /kyc-info, /kyc-spa and
    811       the /aml/$OFFICER_PUB/... officer API; projected into the merchant's
    812       /private/kyc status, the wallet's ExchangeWalletKycStatus and
    813       transaction minors, and the auditor's AML-hold view)
    814 
    815       KYC/AML state of one bank account (wire target) at one exchange.
    816       There are no terminal states; the account exists as long as the
    817       exchange knows it.  Officer-set properties (frozen, reported, pep,
    818       high-risk) and investigation status are modifiers.
    819 
    820       .. lifecycle::
    821          :terminal: (none)
    822          :modifiers: frozen, reported, pep, high-risk, under-investigation
    823 
    824          :transition none -> clean:
    825            :trigger: watch bank.incoming-wire-arrivals
    826            :trigger: call <any exchange operation naming the account>
    827            The exchange learns the account exists; default rules apply.
    828          :transition clean -> auth-required:
    829            :trigger: call <any guarded operation, e.g. POST /withdraw>
    830            :if: /keys.kyc_enabled and no KYC-auth wire transfer has
    831              bound the account to a public key
    832            The operation is denied (409 AUTHORIZATION_KEY_UNKNOWN or
    833            451 with bad_kyc_auth); the client must send a KYC-auth
    834            transfer whose subject carries the account public key.
    835          :transition auth-required -> clean:
    836            :trigger: watch bank.incoming-kycauth-wires
    837            The KYC-auth transfer binds target_pub; /kyc-check becomes
    838            usable for this account.
    839          :transition clean -> action-required:
    840            :trigger: watch bank.incoming-wire-arrivals
    841            :trigger: call POST /kyc-wallet (balance threshold pre-announcement)
    842            :trigger: call POST /aml/$OFFICER_PUB/decision (new_measures)
    843            :if: the active rule set's threshold for the operation type
    844              (WITHDRAW/DEPOSIT/MERGE/BALANCE/CLOSE/AGGREGATE/REFUND/
    845              TRANSACTION) is crossed within its timeframe; "verboten"
    846              measures deny instead of gating
    847            The triggering operation is denied with 451
    848            LegitimizationNeededResponse; the wallet enters pending:kyc.
    849          :transition action-required -> in-process:
    850            :trigger: call POST /kyc-start/$ID (via the KYC SPA)
    851            An external provider process is created with its own
    852            expiration.
    853          :transition in-process -> action-required:
    854            :trigger: timer kyc-account.process-expiration
    855            :trigger: watch kyc-provider.webhook (user-failure /
    856              provider-failure)
    857          :transition in-process -> clean:
    858            :trigger: watch kyc-provider.webhook (success)
    859            :trigger: call POST /kyc-upload/$ID (form evidence)
    860            :trigger: call GET /kyc-proof/$PROVIDER (OAuth redirect)
    861            The AML program evaluates the outcome and installs a new
    862            rule set; the blocked operation may now be retried.
    863          :transition action-required|in-process -> under-review:
    864            :trigger: watch aml-program.outcome (to_investigate)
    865            :trigger: call POST /aml/$OFFICER_PUB/decision
    866              (keep_investigating)
    867            Staff review; clients only see aml_review=true and wait.
    868          :transition under-review -> clean:
    869            :trigger: call POST /aml/$OFFICER_PUB/decision
    870            The officer's new rule set supersedes the active outcome.
    871          :transition clean -> action-required:
    872            :trigger: timer kyc-account.rule-set-expiration
    873            :if: a successor measure is configured
    874            Rule-set expiry re-triggers legitimization.
    875 
    876       .. timer:: rule-set-expiration
    877          LegitimizationRuleSet.expiration_time of the active outcome;
    878          fires into the successor measure (or default rules).
    879       .. timer:: process-expiration
    880          Per external provider process; armed by /kyc-start, fires
    881          in-process -> action-required.
    882       .. timer:: kyc-auth-subject-expiration
    883          Validity of KYC-auth wire instructions (merchant kycauth
    884          response field ``expiration``).
    885       .. timer:: wallet-threshold-expiration
    886          WalletKycCheckResponse.expiration_time; the wallet's granted
    887          balance threshold lapses and the wallet re-checks.
    888 
    889       .. identifier:: kyc-access-token
    890          :capability: yes
    891          Bearer of the SPA session; issued per wire target, distributed
    892          via /kyc-check responses to authenticated clients.
    893       .. identifier:: account-pub
    894          :capability: no (bound by KYC-auth wire transfer)
    895          The public key a KYC-auth transfer binds to the account.
    896 
    897 Notes:
    898 
    899 * ``GET /kyc-check`` carries ``:observes-state: kyc-account.status``;
    900   its ``min_rule`` / ``lpt`` parameters are the long-poll dialect
    901   (``:wait: long-poll(timeout_ms, min_rule, lpt)``), and ``rule_gen`` is
    902   the monotonic rendezvous counter.
    903 * The officer decision endpoint is ``:idempotency: unsafe`` in
    904   appearance but guarded: 409 ``AML_DECISION_MORE_RECENT_PRESENT``
    905   (``:temporality: fixable``, ``:recovery: retry-modified``) forces the
    906   officer to re-read the ledger before re-deciding.
    907 * Challenger's address validation is a separate entity
    908   (``address-validation``, owner: challenger) with states
    909   ``setup / challenged / solved / failed / expired``; its OAuth code is
    910   consumed by ``POST /token`` and its outcome reaches ``kyc-account``
    911   only through the ``watch kyc-provider.webhook`` trigger.
    912 
    913 Flow: bank-integrated withdrawal
    914 --------------------------------
    915 
    916 Entities in play: ``withdrawal-operation`` (bank, shown above),
    917 ``reserve`` (exchange), ``transaction-withdrawal`` (wallet-core).  Join
    918 key: ``reserve_pub`` — created by the wallet (``confirmWithdrawal`` /
    919 ``acceptManualWithdrawal``), carried to the bank by
    920 ``POST /withdrawal-operation/$WITHDRAWAL_ID``, becomes the wire subject
    921 (read by the exchange's wirewatch), read by ``GET /reserves/$RESERVE_PUB``
    922 and ``POST /withdraw``.
    923 
    924 .. code-block:: rst
    925 
    926    .. entity:: reserve
    927       :owner: exchange
    928 
    929       Funds pooled under a reserve public key by an incoming wire
    930       transfer; drained by withdrawals.
    931 
    932       .. lifecycle::
    933          :terminal: closed
    934 
    935          :transition none -> open:
    936            :trigger: watch bank.incoming-wire-arrivals
    937            The wire subject is the reserve_pub.  There is no create
    938            call: the wallet long-polls GET /reserves/$RESERVE_PUB
    939            (404 transient-state, recovery long-poll) until the wire
    940            arrives.
    941          :transition open -> closed:
    942            :trigger: call POST /reserves/$RESERVE_PUB/close
    943            :trigger: timer reserve.expiration
    944            Residual value is wired back to last_origin.
    945 
    946       .. timer:: expiration
    947          Armed by the funding wire (per exchange configuration);
    948          exposed in ReserveSummary.reserve_expiration.
    949 
    950    .. entity:: transaction-withdrawal
    951       :owner: wallet-core
    952 
    953       The wallet's withdrawal transaction group (DD37), here the
    954       bank-integrated variant.  Note the D10 extensions: working as a
    955       modifier, implied suspend/retry edges, and watch-driven triggers.
    956 
    957       .. lifecycle::
    958          :terminal: done, failed, aborted
    959          :modifiers: working
    960          :standard-edges: suspend-retry
    961 
    962          :transition none -> dialog:proposed:
    963            :trigger: call prepareBankIntegratedWithdrawal
    964            The taler://withdraw URI is resolved against the
    965            bank-integration API.
    966          :transition dialog:proposed -> pending:bank-register-reserve/working:
    967            :trigger: call confirmWithdrawal
    968            The wallet creates reserve_pub and registers it with the
    969            bank (POST /withdrawal-operation/$WITHDRAWAL_ID).
    970          :transition pending:bank-register-reserve -> pending:bank-confirm-transfer:
    971            :trigger: watch bank.withdrawal-operation.status (= selected)
    972          :transition pending:bank-confirm-transfer -> pending:exchange-wait-reserve:
    973            :trigger: watch bank.withdrawal-operation.status (= confirmed)
    974            The account owner confirmed; the bank wires the funds.
    975          :transition pending:exchange-wait-reserve -> pending:withdraw/working:
    976            :trigger: watch exchange.reserve (GET /reserves/$RESERVE_PUB
    977              long-poll returns 200)
    978          :transition pending:withdraw -> done:
    979            :trigger: internal coins withdrawn (POST /withdraw replay-safe)
    980          :transition pending:* -> pending:kyc:
    981            :trigger: call <guarded operation>
    982            :if: exchange signals legitimization (451 /
    983              wallet_balance_limit_without_kyc)
    984          :transition pending:kyc -> pending:*:
    985            :trigger: watch exchange.kyc-account (GET /kyc-check long-poll)
    986          :transition pending:bank-confirm-transfer -> aborting:bank/working:
    987            :trigger: call abortTransaction
    988          :transition aborting:bank -> aborted:bank:
    989            :trigger: watch bank.withdrawal-operation.status (= aborted)
    990            A 409 from the bank abort means it confirmed concurrently —
    991            the standard-edges retry path returns to exchange-wait-reserve.
    992 
    993 Flow: payment from a template
    994 -----------------------------
    995 
    996 Entities in play: ``order`` (merchant, shown above — note its
    997 ``none -> unpaid`` transition already lists ``call POST
    998 /templates/$TEMPLATE_ID`` as a second trigger), ``transaction-payment``
    999 (wallet-core).  Identifiers: ``order-id`` (created by the template
   1000 instantiation), the claim ``nonce`` (created by the wallet at claim,
   1001 consumed at pay), ``h_contract_terms`` (derived from the claim response,
   1002 carried into every later public call and into the exchange's deposit).
   1003 
   1004 The template itself is a static merchant resource, not a lifecycle
   1005 entity: ``POST /private/templates`` and its siblings are CRUD with no
   1006 machine, and say so (``:idempotency: keyed:template_id`` and no Effects).
   1007 
   1008 .. code-block:: rst
   1009 
   1010    .. entity:: transaction-payment
   1011       :owner: wallet-core
   1012 
   1013       DD37 payment transaction.  Demonstrates :final-reactivatable::
   1014       a done payment is reactivated by explicit refund queries.
   1015 
   1016       .. lifecycle::
   1017          :terminal: failed, aborted, expired
   1018          :final-reactivatable: done
   1019          :modifiers: working
   1020          :standard-edges: suspend-retry
   1021 
   1022          :transition none -> dialog:proposed:
   1023            :trigger: call preparePayForTemplateV2
   1024            Instantiates the template at the merchant (public
   1025            POST /templates/$TEMPLATE_ID): order none -> unpaid.
   1026          :transition dialog:proposed -> pending:submit-payment/working:
   1027            :trigger: call confirmPay
   1028            The wallet claims the order (nonce bound) and signs.
   1029          :transition pending:submit-payment -> done:
   1030            :trigger: watch merchant.order (POST /orders/$ORDER_ID/pay 200)
   1031          :transition done -> pending:check-refund/working:
   1032            :trigger: call startRefundQuery
   1033            :trigger: timer order.auto-refund (auto-refund probing)
   1034            Reactivation edge out of a final-reactivatable state.
   1035          :transition pending:check-refund -> done:
   1036            :trigger: watch merchant.order.refunds (GET /orders/$ORDER_ID
   1037              long-poll await_refund_obtained)
   1038          :transition dialog:proposed -> expired:
   1039            :trigger: timer order.pay-deadline
   1040          :transition dialog:* -> dialog:waiting-for-other-wallet:
   1041            :trigger: call unclaimPayment
   1042            The claim nonce is released; another wallet may claim.
   1043 
   1044       .. timer:: auto-refund
   1045          Armed from contract terms (auto_refund); exposed to the wallet
   1046          in contract terms, fires the refund-probing reactivation.
   1047 
   1048 Flow: deposit to a bank account
   1049 --------------------------------
   1050 
   1051 Entities in play: ``deposit`` (exchange), ``transaction-deposit``
   1052 (wallet-core), and ``kyc-account`` (the deposit flow is its most
   1053 frequent client).  Identifiers: ``h_wire`` (hash of the target payto),
   1054 ``h_contract_terms``, ``wtid`` (created internally by the exchange's
   1055 aggregator, carried into the tracking response and onward to merchant
   1056 reconciliation).
   1057 
   1058 .. code-block:: rst
   1059 
   1060    .. entity:: deposit
   1061       :owner: exchange
   1062 
   1063       A batch deposit of coins to one wire target.
   1064 
   1065       .. lifecycle::
   1066          :terminal: wired, refunded
   1067 
   1068          :transition none -> accepted:
   1069            :trigger: call POST /batch-deposit
   1070            Coins are deposited; the exchange wires after the refund
   1071            deadline and before wire_transfer_deadline.
   1072          :transition accepted -> held:
   1073            :trigger: watch aml-program.outcome
   1074            :if: aggregation KYC triggers (tracking returns 202 with
   1075              kyc_ok:false; auditor reports deferral_reason="KYC")
   1076          :transition held -> wired:
   1077            :trigger: watch exchange.kyc-account (clearance)
   1078          :transition accepted -> wired:
   1079            :trigger: timer deposit.wire-deadline (aggregation runs)
   1080            GET /deposits/$H_WIRE/... now returns 200 with wtid and
   1081            execution_time.
   1082          :transition accepted -> refunded:
   1083            :trigger: call <merchant refund before deadline>
   1084 
   1085       .. timer:: wire-deadline
   1086          Armed by BatchDepositRequest.wire_transfer_deadline ("never"
   1087          is rejected); exposed in tracking responses as execution_time.
   1088 
   1089    .. entity:: transaction-deposit
   1090       :owner: wallet-core
   1091 
   1092       DD37 deposit transaction — the KYC-richest machine: pre-submission
   1093       KYC (kyc-auth transfer) and post-submission aggregation KYC are
   1094       distinct guarded regions.
   1095 
   1096       .. lifecycle::
   1097          :terminal: failed, aborted
   1098          :final-reactivatable: done
   1099          :modifiers: working
   1100          :standard-edges: suspend-retry
   1101 
   1102          :transition none -> pending:deposit/working:
   1103            :trigger: call createDepositGroup
   1104          :transition pending:deposit -> pending:kyc-auth:
   1105            :trigger: call POST /batch-deposit (denied)
   1106            :if: target account not yet bound (409 AUTHORIZATION_KEY_UNKNOWN)
   1107            kycAuthTransferInfo names the debit account that must send
   1108            the KYC-auth transfer.
   1109          :transition pending:kyc-auth -> pending:deposit:
   1110            :trigger: watch exchange.kyc-account (auth wire observed via
   1111              GET /kyc-check long-poll lpt=1)
   1112          :transition pending:deposit -> finalizing:track:
   1113            :trigger: watch exchange.deposit (POST /batch-deposit 200)
   1114          :transition finalizing:track -> pending:kyc:
   1115            :trigger: watch exchange.deposit (tracking 202, kyc_ok:false)
   1116            :if: aggregation KYC required
   1117          :transition pending:kyc -> finalizing:track:
   1118            :trigger: watch exchange.kyc-account (kyc_ok via lpt=2)
   1119          :transition finalizing:track -> done:
   1120            :trigger: watch exchange.deposit (tracking 200, wtid)
   1121          :transition pending:deposit -> aborting/working:
   1122            :trigger: call abortTransaction
   1123          :transition aborting -> aborted:deposit-abort-recovered:
   1124            :trigger: internal (refund + refresh completed)
   1125          :transition aborting -> done:deposit-abort-too-late:
   1126            :trigger: watch exchange.deposit (already wired)
   1127            Terminal done-with-minor: the money arrived despite the abort.
   1128 
   1129 Migration plan
   1130 ==============
   1131 
   1132 Compliance snapshot (verified file counts, 2026-10):
   1133 
   1134 .. list-table::
   1135    :header-rows: 1
   1136 
   1137    * - Corpus
   1138      - Files
   1139      - Status vs. the requirements
   1140    * - wallet-core ops
   1141      - 152 (plus the notifications page)
   1142      - E1–E6, E11, E12 ✔; need: header options, structured errors,
   1143        ``:wait:``, effects/observation links to the DD37 machine via the
   1144        D10 extensions
   1145    * - exchange
   1146      - 65
   1147      - E1–E7 ✔, retry/idempotency prose good → convert to fields;
   1148        need: lifecycle registry entries (reserve, purse),
   1149        ``:visible-to:``, notification page (declares: no push channels)
   1150    * - merchant
   1151      - 114
   1152      - E1–E7 ✔; documented scopes become
   1153        ``:visible-to: merchant-staff:<scope>``; webhooks move to the
   1154        merchant notification page; need: order lifecycle, structured
   1155        errors
   1156    * - bank-side
   1157      - 65 (bank-integration 4, corebank 39, bank-wire 10,
   1158        bank-transfer 3, bank-revenue 2, bank-conversion-info 5,
   1159        account-directory 2)
   1160      - E1–E7 ✔; withdrawal/TAN states exist as unions → lifecycle
   1161        registry entries; need: error classification, timers,
   1162        cashout status gap flagged (API lacks a status field)
   1163    * - identity/misc
   1164      - 92 (challenger 7, taldir 5, mailbox 6, donau 13, ebisync 3,
   1165        auditor 58)
   1166      - mixed; challenger close to compliant; taldir/mailbox need
   1167        timers + error fields; drafts get ``:maturity:`` banners
   1168    * - sync, terminal
   1169      - 3 + 10 endpoints, inline today
   1170      - must be split into per-endpoint files first (D2)
   1171 
   1172 Steps, in order:
   1173 
   1174 1. Build the tooling in ``taler-docs/_exts/``: the new directive options,
   1175    the ``entity`` / ``lifecycle`` / ``timer`` / ``identifier`` /
   1176    ``notification-channel`` directives, and the lint (all rules warn-only
   1177    initially).
   1178 2. Backfill the bank-integrated withdrawal slice end to end:
   1179    bank-integration, corebank withdrawal + TAN, exchange reserve +
   1180    withdraw, wallet withdrawal ops, registry entries for
   1181    ``withdrawal-operation``, ``tan-challenge``, ``reserve``,
   1182    ``reserve_pub`` and ``withdrawal-id``, wallet and exchange
   1183    notification pages.  Flip the lint to error for this slice.  This is
   1184    the acceptance gate for the format itself.
   1185 3. Payment/refund slice: merchant order lifecycle, wallet payment and
   1186    refund ops, merchant notification page, ``order`` / ``order-id``
   1187    anchors.
   1188 4. P2P slice: purse entities, wallet peer-push/peer-pull transaction
   1189    entities, deposit.
   1190 5. The long tail: KYC/AML (with the shared-ownership treatment of D8's
   1191    owner rule), sync + terminal splitting, auditor, drafts and banners.
   1192 
   1193 Corpus corrections folded into the requirements document
   1194 --------------------------------------------------------
   1195 
   1196 Verification against the corpus produced these adjustments, which the
   1197 requirements document incorporates:
   1198 
   1199 * ``:maturity:`` instead of ``:status:`` (httpdomain field-name
   1200   collision; D2).
   1201 * Variant-group and proxy-stanza exemptions to the one-method-per-file
   1202   rule (D2).
   1203 * Sync and terminal APIs are to be split into per-endpoint files (D2).
   1204 * The wallet notification page exists but must be *restructured* into
   1205   channel form; it is not already compliant (D9).
   1206 * wallet-core op count is 152, not 153 (the 153rd file is
   1207   ``notifications.rst``).
   1208 * KYC state is cross-cutting and gets the projections-named-under-owner
   1209   treatment rather than a naive single owner.
   1210 * Wallet transaction entities get the four extensions of D10 and the
   1211   canonical-registry relationship to DD37 of D11.
   1212 
   1213 Test Plan
   1214 =========
   1215 
   1216 * The lint (V1–V21 in the requirements document) runs in the
   1217   documentation CI; the bank-integrated withdrawal slice (bank-
   1218   integration, corebank withdrawal + TAN, exchange reserve + withdraw,
   1219   wallet withdrawal ops, registry entries for ``reserve_pub`` and
   1220   ``withdrawal-id``) must lint clean as the acceptance gate for the
   1221   format itself.
   1222 * Codegen dry-run: generate client types and validators from the
   1223   slice's ``ts:def`` layer and diff against wallet-core's and the
   1224   exchange's corresponding wire types; mismatches fail the
   1225   implementation CI.
   1226 * Derivation smoke tests: generate the withdrawal-operation state table,
   1227   the bank-customer persona projection, and a withdrawal sequence diagram
   1228   from the corpus; review against the hand-written flow documentation.
   1229 * Wallet extension tests: lint the ``transaction-withdrawal`` registry
   1230   entry against DD37's generated tables; check that reactivation edges out
   1231   of ``done`` are legal under ``:final-reactivatable:`` and rejected under
   1232   ``:terminal:``.
   1233 
   1234 Definition of Done
   1235 ==================
   1236 
   1237 * All new directives (``entity``, ``lifecycle``, ``timer``, ``identifier``,
   1238   ``notification-channel``, extended ``http:*``/``ts:op`` options) are
   1239   implemented in ``_exts/`` and render correctly.
   1240 * The lint enforces the full rule set as errors on the backfilled slices.
   1241 * Every per-endpoint file (including the newly split sync and terminal
   1242   endpoints) is compliant or carries an explicit draft/banner maturity.
   1243 * The entity registry covers at minimum the withdrawal, payment/refund,
   1244   and p2p flows end to end, including wallet transaction entities with
   1245   the D10 extensions.
   1246 * DD37's state tables are generated from the registry.
   1247 * At least one implementation repository runs codegen drift detection in
   1248   CI against the corpus.
   1249 
   1250 Alternatives
   1251 ============
   1252 
   1253 **OpenAPI/Swagger as the source of truth.**  Covers the shape layer well
   1254 but has no home for lifecycles, temporality semantics, actor projections,
   1255 identifier roles, or notification doctrine — the parts this design exists
   1256 for.  The ``ts:def`` layer already plays OpenAPI's shape role inside the
   1257 prose-capable Sphinx corpus; replacing the corpus would lose the prose
   1258 that remains the human interface.
   1259 
   1260 **Generate docs from code.**  Inverts the authority relationship: the
   1261 design intent (idempotency guarantees, temporality classification,
   1262 capability properties) is not present in code in checkable form, so
   1263 generated docs would silently reflect implementation bugs as
   1264 specification.  The spec-first model with CI drift detection keeps the
   1265 docs authoritative while still catching divergence.
   1266 
   1267 **Full anchor-side declarations** (state sets, timer facts, identifier
   1268 life stories restated at the registry).  Rejected: every derivable fact
   1269 declared twice is a drift bug waiting to happen (D1).
   1270 
   1271 **Per-type substate unions instead of DD37's flat minor-state union.**
   1272 Rejected for this design: the flat global union is the deployed,
   1273 implemented vocabulary; partitioning it is a DD37 change, out of scope.
   1274 
   1275 **Modeling ``/working`` and suspended states as literal states.**
   1276 Rejected: combinatorial table explosion for zero informational gain;
   1277 orthogonal modifiers plus implied standard edges carry the same semantics
   1278 (D10.2).
   1279 
   1280 **A separate ``:auth:`` field.**  Rejected: actor implies mechanism;
   1281 duplication invites contradiction (D5).
   1282 
   1283 Drawbacks
   1284 =========
   1285 
   1286 * **Authoring cost.**  Compliant files are substantially more work than
   1287   prose, and ~490 files need backfill.  Mitigated by incremental migration
   1288   and by the lint telling authors exactly what is missing.
   1289 * **Format rigidity.**  Edge cases that do not fit (today: variant groups,
   1290   proxy stanzas, KYC's shared ownership) need explicit format extensions
   1291   rather than prose escape hatches, which slows documentation of unusual
   1292   endpoints.  Accepted: silent exceptions are worse than visible ones.
   1293 * **Two specification artifacts to maintain.**  This DD, the requirements
   1294   document, and (for wallets) DD37 must evolve together.  Mitigated by the
   1295   normative-reference hierarchy and by generating DD37's tables from the
   1296   registry (D11).
   1297 * **Tooling must exist before the value does.**  Until the lint and at
   1298   least one generator land, the new fields are overhead without payoff.
   1299   Mitigated by doing the withdrawal slice end-to-end first, tooling
   1300   included.
   1301 * **The wallet extensions add vocabulary** (``:final-reactivatable:``,
   1302   ``:standard-edges:``, modifier flags) that only wallet transaction
   1303   entities use, slightly diluting the "one vocab fits all" property.
   1304 
   1305 Discussion / Q&A
   1306 ================
   1307 
   1308 **Why not put triggers on the method files instead of the lifecycle?**
   1309 A method can honestly describe its own effects (and does, in **Effects**),
   1310 but timer firings, watches of other components, and external human actions
   1311 have no method file to live in.  Causality is a property of the entity's
   1312 machine, so triggers live on transitions; the two-sided check with method
   1313 effects keeps both honest.
   1314 
   1315 **Why is long-poll not a notification channel?**
   1316 A long-poll response is authoritative state delivered late; a notification
   1317 is a hint that must be backed by a read.  The delivery semantics, the lint
   1318 rules, and the generated sequence-diagram edges differ.  Conflating them
   1319 would make "pull + long-poll only" components (the exchange) look like
   1320 they offer push.
   1321 
   1322 **Why does the tax auditor have no credentials?**
   1323 That is the system as designed: the auditor's access is authorized by
   1324 possession of unguessable identifiers (``wtid``).  The format makes this
   1325 visible (``capability`` annotations resolving to ``:capability: yes``
   1326 anchors) rather than hiding it in prose.
   1327 
   1328 **Are wallet transaction entities really "owned" by wallet-core if nothing
   1329 calls wallet-core to drive them?**
   1330 Yes: ownership is about authoritative state and transition execution, not
   1331 about the trigger kind.  Wallet-core's DB record is the truth; its
   1332 transitions execute locally, driven by watches, timers, and user actions.
   1333 The bank-owned withdrawal operation the wallet watches is a *different*
   1334 entity with its own owner and machine — the two are joined by the
   1335 ``watch`` trigger, which is exactly the cross-component causality link the
   1336 old prose never made.