ADR-0049: AmberCalc formula model + the enrichment/DOSSIER FFI surface (launch-app, dossier-commit, dossier-has?, assert-balance)
Context
Section titled “Context”ADR-0042 named AmberCalc as first-party program #7 (a columnar spreadsheet) and owed the launch-app primitive to an ADR-0005 amendment. That amendment landed as a logged entry (ADR-0005 Amendment Log, 2026-06-21) for three primitives — launch-app, dossier-commit, dossier-has? — but the entry deliberately left the exact shapes open: “the world-handle type, the result / key types … finalize with the engineering spike.” The enrichment-contract §8 and dossier-data-model §8 both flag the same unfinished business and a gap D: the DOSSIER refile action has no named FFI home.
The AmberCalc + Black Ledger v1 design (deleted 2026-07-29; recover from git history) (GWP-603, accepted with Josh 2026-06-25) is the forcing function. It is the first concrete consumer of all three primitives, and it makes four design commitments that need a normative home before implementation can start under TDD:
- AmberCalc’s formula model — host Fe sexp (cell = bound symbol, range = real list, per-column printers), the “SES model.” The stub still lists “KEC Lisp vs. a constrained mini-DSL” as an open question (ambercalc.md Q1).
- Dataset ingestion is
:world(a live handle to a mission-data region), not:payload(a snapshot blob) — fixing a documented contradiction between the enrichment contract and the walkthrough/stub shorthand (ambercalc.md Q2). - Export to DOSSIER is a typed fact-node via
dossier-commit, never a raw write (ambercalc.md Q3) — which forces the:linkssemantics and the refile-FFI gap D to be pinned down. - Reconcile-by-assertion (
assert-balance) is the Black Ledger win mechanic (design §4.4) and needs a home: core FFI primitive vs. cart-side helper.
Forcing functions
Section titled “Forcing functions”- The design is accepted; the spec rewrites are queued behind it.
ambercalc.md(stub → full) andblack-ledger.md(April-2026, stale → rewrite) cannot be written until the formula model and the three FFI signatures are locked — they would otherwise restate a moving target. - The 2026-06-21 logged FFI entry is unsafe to build against. “Exact signatures finalize with the engineering spike” means the C bridge (
nosh_lisp_bridge.c) has no contract to bind. The first cart (blackledger.lsprework) is the spike; the contract must precede it. - Gap D is load-bearing for this cart. Black Ledger’s whole verdict loop is “find a fact → commit it.” If
dossier-commitlands in the inbox but nothing names the refile, the cart can’t drive its own evidence chain to a durable result.
Constraints
Section titled “Constraints”- No raw DOSSIER / UDS writes (ADR-0040 §6). Every durable consequence routes through the engine’s sanctioned-write path. AmberCalc is a calculator over a table; it cannot mutate durable state directly.
- Carts enrich, never gate (ADR-0042).
launch-appon a bare AmberCalc (no:world) must still run. - Canonical hardware spec is immovable. AmberCalc renders on the 128×75 surface, content Rows 1–73; Row 0/74 firmware-owned; CIPHER OLED-exclusive. This ADR references those values; it does not restate or fork them (Canonical Hardware Specification, Spec Hygiene Rules 5/6).
- Minimize the core primitive surface. ADR-0005 is a tight, enumerated contract. A new core primitive is justified only when it cannot be composed from existing ones.
Decision
Section titled “Decision”Lock the AmberCalc formula model and finalize the four FFI shapes the design needs, amending ADR-0005. Concretely:
1. The AmberCalc formula model is host Fe sexp (the SES model) — ratified as canonical
Section titled “1. The AmberCalc formula model is host Fe sexp (the SES model) — ratified as canonical”Formulas are KEC Lisp expressions, not a bespoke mini-DSL. This is the native design on a machine that already tree-walks Fe, and it resolves ambercalc.md Q1:
- A cell name is a bound symbol;
(cell @3$2)resolves its stored value. Addresses are all-numeric@row$col(design §3.2): absolute by default, signed-relative as@-1$0. - A range is a real Lisp list —
(range @2$1 @4$1)— so(sum (range …)),(map fn (range …)),(count …)are ordinary function calls, not DSL keywords. - The aggregate floor is
sum avg min max count+ arithmetic +if, exposed as ordinary Fe builtins (the 47-year VisiCalc set). - Value ≠ display: a column carries a printer (
currency/date/acct/raw) — stored value is distinct from rendered glyphs (SES printer functions), satisfying the screen-design right-align/no-truncate rule. - First-keystroke type routing: a digit starts a number,
"a label,(a formula. No separate “is this text or a value” step.
AmberCalc’s formula surface is therefore a binding-set layered on the cart/program Fe context (the ADR-0005 2026-06-14 clarification model: device primitives bound onto the vendored language). cell, range, and the aggregate floor are AmberCalc-program builtins, not new NoshAPI primitives — they live in the AmberCalc program (runtime/programs/ambercalc/), bound when AmberCalc is the live mode, exactly as ADR-0046 frames a program-as-mode. They do not enlarge the ADR-0005 enumeration.
2. launch-app — finalize the Tier-1 signature, the :world handle shape, and :payload semantics
Section titled “2. launch-app — finalize the Tier-1 signature, the :world handle shape, and :payload semantics”Ratify the logged signature with the shapes pinned:
(launch-app prog &key payload world) → resultprog— a program id keyword (:ambercalc,:conduit, …). Routes through DeckRunner’s screen router (ADR-0042 Decision-4).:world— an opaque run-state handle to a mission-data region (ADR-0006 run-state; enrichment-contract §4), not a structured ref the caller can dereference field-by-field. The program reads/navigates the region live through program-side accessors; a node the cart reveals stays revealed. The handle is:- opaque — Lisp sees a
FE_TPTRhandle (the same GC-rooted foreign-pointer mechanism as cells), not an inspectable record. Carts pass it; they do not walk it. - read-only to durable state — nothing a program does to the world region touches UDS or DOSSIER (enrichment-contract §4). Consequence happens only through key emission (Decision §3).
- mission-instance-scoped — the region (and therefore every handle into it) resets at the mission-instance boundary, like the rest of run state (DeckRunner §6). A handle captured in one mission instance is invalid in the next; using a stale handle raises
:invalid-world. - bare-launch default —
launch-appwith no:worldyields the program’s empty/default world (enrichment-contract §3). AmberCalc opens on an empty grid; it still runs. Enrichment never gates.
- opaque — Lisp sees a
:payload— a small, self-contained snapshot of launch parameters for this launch: a view selector, an initial scale, a threat hint. It is a value (a quoted plist), copied by value, not a handle and not the dataset. Large or live data is:world;:payloadis parameters only. This resolves the enrichment-contract underspecification and supersedes the walkthrough/stub shorthand(launch-app :ambercalc :payload ledger-dataset):
(launch-app :ambercalc :world ledger-topology ; opaque handle to the live ledger region :payload '(:view register :threat 3)) ; snapshot params: initial view + scaleresult— the capability-call result struct already canonical in ADR-0005 (:outcome/:trace/:extracted/:turns/:bonuses), additive-by-key.launch-appreturns it when the program surface exits back to the caller.
Tier: 1 (All-Carts), unchanged from the logged entry. A program is always launchable.
3. dossier-commit / dossier-has? — finalize the Tier-2 signatures and the sanctioned write path; close gap D
Section titled “3. dossier-commit / dossier-has? — finalize the Tier-2 signatures and the sanctioned write path; close gap D”(dossier-commit key) → bool ; sanctioned capture; never a raw write(dossier-has? key &key min-conf) → bool ; read-only gating querykeyis the typed, addressable fact-node key — a colon-path symbol the cart authored as discoverable, e.g.acct:shell:caribbean-trust:beneficiary(dossier-data-model §3). It is not a free-form string and not the value; the value, provenance, confidence, and links are carried on the authored fact-node the key names (the cart’s static-data block defines the node;dossier-commitdeposits that node).dossier-commitis the sanctioned capture half of the two-step write (dossier-data-model §4, ADR-0040 §6): the engine lands the fact in the inbox, then performs the sanctioned refile under the matching profile. Auto-refile when the key’s type names a known profile (acct:shell:caribbean-trust:*→ thecaribbean-trust:accountprofile, creating it if new). Otherwise the fact waits in the inbox for an operator refile. A program / cart / player script cannot raw-write; it reports a discovery and the engine commits. Returns#ton accepted capture,#fif the key is unknown/unauthored.dossier-has?is the read-only gating query (dossier-data-model §6): is the fact present at ≥min-conf?:min-confis one of the confidence symbols:rumored/:inferred/:verified(ordered; default:rumored— “present at any confidence”). It hard-gates (absent ⇒ blocked) or de-risks (present ⇒ soft-bias). Pure read; never mutates.
Gap D (the refile FFI) — DECIDED: no new primitive. Refile is not a cart/program FFI call. The engine performs auto-refile inside the dossier-commit sanctioned path; the only manual refile is an operator action in the DOSSIER program UI (a sanctioned action on the inbox, ADR-0046 mode command — not a typed command, not raw Lisp). This keeps the write boundary intact (ADR-0040): carts capture, the engine/operator refiles. No dossier-refile primitive enters the ADR-0005 enumeration. Gap D is closed by absence-by-design, documented here so a future reader does not re-open it looking for the missing verb.
Fact-node :links semantics (enough to build). A committed fact carries typed edges (dossier-data-model §5) drawn from the canonical edge-verb set :owns · :controls · :routes-to · :employs · :located-at · :corroborates · :contradicts. For Black Ledger the load-bearing subset is :routes-to (a posting’s destination account — the follow-the-money edge), :owns (beneficiary → account / shell → shell), and :contradicts (a reconcile-by-assertion delta that breaks a prior balance fact). Links are authored on the fact-node, deposited atomically with dossier-commit (they are part of the node the key names) — there is no separate “link” FFI. Expand-in-place navigation walks them (person → fact → the account it names → that account's facts). A fact is immutable once committed; new information is a new fact that may :corroborate or :contradict an older one. This is sufficient to implement the cart’s evidence chain; the confidence-from-corroboration tuning (how a :contradicts edge downgrades :conf) stays a deferred tuning item (dossier-data-model §8), not an FFI question.
Tier: 2 (Mission-Context), unchanged from the logged entry. Both raise :not-in-mission outside an accepted contract.
4. assert-balance is a cart-side helper, not a core primitive (RECOMMENDED, ADOPTED)
Section titled “4. assert-balance is a cart-side helper, not a core primitive (RECOMMENDED, ADOPTED)”Reconcile-by-assertion (design §4.4) is implemented as a cart-side helper over dossier-commit, not a new NoshAPI primitive. The operator obtains an external “true” figure (a recon fact from DOSSIER, or a mission-authored statement), asserts it against an account at a date, and the helper computes the delta against the live :world ledger. There is no strong reason for a core primitive: the comparison is ordinary arithmetic over data AmberCalc already exposes (the ledger region via :world, the true figure via dossier-has? / a fact value), and the consequence (surfacing the gap as a clue, recording it) routes through the already-sanctioned dossier-commit. A core primitive would add C surface for logic that is pure Lisp over existing reads.
The helper, in the Black Ledger cart (carts/carts/blackledger.lsp):
;; cart-side; no new FFI. Reads the live world region + a known-true figure,;; computes the delta, and commits the result as a fact-node via the sanctioned path.(defn assert-balance (acct date amount) (let (actual (ledger-balance-at world acct date) ; program-side read of the :world region delta (- amount actual)) (when (/= delta 0) (dossier-commit ; sanctioned; engine refiles (fact-key :acct acct :balance :asserted date))) ; → :acct:<path>:balance:asserted delta)) ; the delta IS the clue (design §4.4)The :acct:…:balance:asserted key schema (defined either way):
:acct:<colon-path>:balance:asserted:<iso-date>A typed fact-node key whose :value is the asserted (externally-true) amount, :conf is :verified (it came from a seized statement / corroborated recon), and whose :links carry :contradicts to the prior :acct:<path>:balance fact when the delta is non-zero (the broken assertion is the contradiction). This key shape is normative whether assertion stays a helper or is ever promoted — so a future promotion to a core primitive (should profiling demand it) is a drop-in that emits the same key. TRACE between the last good assertion and the broken one locates the hidden transfer (design §4.4 step 3).
5. Scout-flagged seams — resolved enough to build
Section titled “5. Scout-flagged seams — resolved enough to build”-
Grammar-vs-procedural boundary for Black Ledger — DECIDED: pure-grammar exemplar, no procedural mini-game. Black Ledger stays the grammar-capability exemplar (capability-shapes.md: “Black Ledger’s predicate/forensic queries”). The gameplay is the verb-noun forensic grammar over the ledger tree — five killer verbs (TYPE-COLUMN / TRACE=CAR / FLAG=QUOTE / CROSS=EQ / CHAIN=CONS) + the dive/pop spine + the query surface (TERM). There is no real-time arcade loop; the “minigame feel” comes from the verbs and navigation, not a control loop. This keeps the cart entirely on the Mission Runner’s inspectable/scriptable/automatable Lisp surface (lambdas in Lambda Slots) and explicitly does not invoke the ADR-0047 experience/tactical pixel tier — Black Ledger is cell-API + grammar, full stop. Procedural generation is confined to world generation (the shell-company ledger from
ledger-topology/shell-depth/conspiracy-signatureseeds via the existing LFSR primitives) — generating the world is procedural; playing it is grammar. -
80×25 → 128×75 view consequence — DECIDED. AmberCalc and the Black Ledger rework target the canonical 128×75 surface, content Rows 1–73 (Row 0 status / Row 74 action firmware-owned; Canonical Hardware Specification, Spec Hygiene Rule 5). A dense transaction table renders at 1× (≈73 body rows); headers at 2×; per-region integer scale (design §3.1). The legacy April-2026
black-ledger.md80×25 wireframes are stale and must be redrawn to 128×75 in the spec rewrite (design §8) — this is a tracked spec-rewrite consequence, not an FFI question, but it is locked here so the rewrite has an unambiguous target. CIPHER feedback/verdict beats move to the OLED CIPHER-LINE 256×64 (OLED-exclusive, Spec Hygiene Rule 6); no CIPHER on the main grid. -
TRACE-back ownership — DECIDED: Black Ledger is not the v1 satisfier. verb-partition.md flags TRACE-back (verb #23, “reveal a hostile process’s origin”) as a CORE with no cart satisfier — a real coverage gap, listing Black Ledger or Depthcharge as candidates. Call: TRACE-back is a live-case action (“follow the forged signature back” — design §10), and the v1 Black Ledger scope is explicitly cold case only (ANALYZE + OBTAIN + OBSERVE; FORGE/DESTROY and the live-case heat/knSALK path are deferred to the ICE BREAKER → Black Ledger campaign expansion — design §1.2, §5). Therefore Black Ledger v1 does not satisfy TRACE-back; the gap stays open, re-pointed at the deferred live-case Black Ledger expansion (its most natural home, paired with FORGE/DESTROY) rather than Depthcharge. This is a scoping decision, not new FFI: TRACE-back needs no primitive beyond CAR/TRACE already in the grammar; it needs the live-case world (a forged signature with an origin), which v1 does not generate.
Options Considered
Section titled “Options Considered”Option A: Lock the formula model + finalize the three logged FFI shapes + keep assert-balance a cart helper. (ACCEPTED)
Section titled “Option A: Lock the formula model + finalize the three logged FFI shapes + keep assert-balance a cart helper. (ACCEPTED)”Ratify the SES formula model as canonical; pin launch-app’s opaque mission-scoped :world handle and snapshot :payload; pin dossier-commit / dossier-has? signatures and the sanctioned capture→refile path; close gap D by absence-by-design; implement assert-balance as cart Lisp over dossier-commit.
Chosen because it unblocks both spec rewrites and the cart implementation with a minimal, composable core surface — zero net new NoshAPI primitives (the three were already logged; the formula builtins and assert-balance are program/cart-side), while still fixing every underspecification the design and the two contract docs flagged. It keeps the ADR-0040 write boundary intact and the ADR-0005 enumeration tight.
Option B: Make assert-balance a core NoshAPI primitive.
Section titled “Option B: Make assert-balance a core NoshAPI primitive.”Add (assert-balance acct date amount) → delta to Tier 2.
Rejected because it adds C bridge surface for logic that is pure Lisp over data already reachable (:world region read + dossier-has? + arithmetic), and its only durable effect is a dossier-commit that already exists. The design itself recommends the helper “unless there’s a strong reason”; there is none. Promotion remains available later (the :acct:…:balance:asserted key schema is fixed either way, so promotion is a drop-in) if on-device profiling ever shows the helper is hot — but speculatively widening the core contract violates the minimize-core-surface constraint.
Option C: A bespoke formula mini-DSL for AmberCalc.
Section titled “Option C: A bespoke formula mini-DSL for AmberCalc.”Give AmberCalc a constrained spreadsheet expression language (the ambercalc.md Q1 alternative).
Rejected because the machine already tree-walks Fe; a parallel DSL is a second evaluator, a second grammar to teach, and a wall between formulas and the player’s REPL/automation. The SES model (cell = symbol, range = list) makes (sum (range …)) just a function call — inspectable, scriptable, and consistent with the canonical Lisp-primitive key semantics the whole device reuses. A DSL would forfeit the project’s core “grammar is the game” principle.
Option D: Add a dossier-refile primitive to close gap D.
Section titled “Option D: Add a dossier-refile primitive to close gap D.”Give carts/programs an FFI verb to attach an inbox fact under a profile.
Rejected because it punches a hole in the ADR-0040 sanctioned-write boundary — refile is the durable attach, and exposing it to cart Lisp lets a cart durably shape DOSSIER directly. Auto-refile (engine, inside dossier-commit) + operator refile (DOSSIER UI action) already cover every case; a cart-callable refile would be the one raw-ish write the boundary exists to forbid.
Option E: Pass :world as a structured, walkable ref instead of an opaque handle.
Section titled “Option E: Pass :world as a structured, walkable ref instead of an opaque handle.”Let carts dereference the world region’s fields directly from Lisp.
Rejected because a large, live world region (a 16-node shell ledger) is exactly the case the enrichment contract reserves for a handle, not a copy (enrichment-contract §4). A walkable structured ref invites carts to snapshot/mutate the region’s internals, blurs the run-state boundary, and couples cart code to the region’s representation. The opaque FE_TPTR handle + program-side accessors keeps the program as the sole navigator of its own world and the representation free to change.
Trade-off Analysis
Section titled “Trade-off Analysis”| Dimension | A — lock model + helper (chosen) | B — core assert-balance | C — formula DSL | D — dossier-refile prim | E — structured :world ref |
|---|---|---|---|---|---|
| Net new NoshAPI primitives | 0 (3 already logged) | +1 | 0 (but +1 evaluator) | +1 | 0 |
| Unblocks ambercalc.md + black-ledger.md rewrites | ✓ | ✓ | ✓ | ✓ | ✓ |
| Preserves ADR-0040 write boundary | ✓ | ✓ | ✓ | ✗ punctures it | ◐ invites region writes |
| Keeps “grammar is the game” | ✓ | ✓ | ✗ second grammar | ✓ | ✓ |
| Core C surface to maintain/test | ✓ minimal | ◐ +1 entry point | ◐ +parser | ◐ +1 entry point | ✓ minimal |
| Representation coupling | ✓ low (opaque handle) | ✓ low | ✓ low | ✓ low | ✗ high |
| Future promotion path preserved | ✓ key schema fixed | n/a | — | — | — |
Honest cost of A: the assertion logic lives in cart Lisp, so each forensic cart that wants reconcile-by-assertion re-imports the helper (or a shared ui/-level forensic library when a second consumer appears — the ADR-0046 rule-of-three extraction). That is the right trade against permanently widening the core FFI for one cart’s mechanic.
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Both spec rewrites are unblocked against a fixed target:
ambercalc.md(stub → full, Q1–Q3 resolved) andblack-ledger.md(stale → 128×75 / Lisp / program-cart seam / OLED-CIPHER rewrite). - The 2026-06-21 logged FFI entry becomes buildable —
nosh_lisp_bridge.chas concrete signatures (opaqueFE_TPTR:world, snapshot:payload,:min-confconfidence enum) to bind, and TDD can start onblackledger.lsp. - Zero net new core primitives — the ADR-0005 enumeration count is unchanged; the formula builtins and
assert-balanceare program/cart-side binding-sets, consistent with the ADR-0005 2026-06-14 vendor-as-library model. - The write boundary is reaffirmed and gap D is closed without a new verb — capture (
dossier-commit) → auto/operator refile, no cart-callable raw write. - The
:acct:…:balance:assertedkey schema is locked independent of where assertion lives, so a future helper→primitive promotion is a no-contract-change drop-in. - TRACE-back’s coverage gap has a clear, recorded disposition (deferred live-case Black Ledger), so the next reader doesn’t mistake it for an oversight.
Negative / Accepted costs
Section titled “Negative / Accepted costs”- Per-cart helper duplication for
assert-balanceuntil a second forensic consumer triggers extraction into a shared library (accepted; rule-of-three). - The April-2026
black-ledger.mdwireframes (80×25) are now formally stale and must be redrawn to 128×75 in the rewrite — tracked work the design (§8) already names. - The exact
:world-region schema (the shell-ledger node shape) is still per-program design (enrichment-contract §9 “per-program world schemas”) — this ADR fixes the handle contract, not the ledger’s internal layout. The Black Ledger spec rewrite owns that.
Follow-on work this ADR creates
Section titled “Follow-on work this ADR creates”- AmberCalc program spec →
docs/software/programs/ambercalc.md(replace stub; encode the SES model,@row$col, printers, the:world/:payloadlaunch contract, the DOSSIER export). - Black Ledger module-spec rewrite →
docs/software/cartridges/modules/black-ledger.md(128×75, Lisp, program/cart seam, recon→DOSSIER gating, OLED-CIPHER,assert-balancehelper, COLD LEDGER mission template). - Implementation (TDD) — AmberCalc program (
runtime/programs/ambercalc/) + Black Ledger cart rework (carts/carts/blackledger.lsp);nosh_lisp_bridge.cbinds the three finalized FFI signatures; tests forlaunch-apphandle lifecycle (incl.:invalid-worldon stale handle),dossier-commitcapture→auto-refile,dossier-has?:min-confordering. - verb-partition.md — re-point the TRACE-back gap note at the deferred live-case Black Ledger expansion.
- enrichment-contract.md §8 / dossier-data-model.md §8 — flip the FFI bullets from “finalize with the engineering spike” to “finalized — ADR-0049.”
Documentation Updates (REQUIRED — Spec Hygiene Rule 3)
Section titled “Documentation Updates (REQUIRED — Spec Hygiene Rule 3)”-
docs/adr/ADR-0049-ambercalc-formula-model-and-ffi.md— this file (NEW). -
docs/adr/ADR-0005-ffi-surface.md— amendment (this PR): finalizelaunch-app/dossier-commit/dossier-has?signatures; flip the 2026-06-21 amendment-log status from “logged” to ADR-0049-ratified; recordassert-balanceas a cart-side helper (no enumeration change). Header**Amended:**line + Amendment Log entry added. -
docs/adr/README.md— index entry added at the top of the active list. -
docs/software/programs/ambercalc.md— replace stub with full design; resolve Open-questions Q1/Q2/Q3 (separate PR — GWP-606 spec rewrite). -
docs/software/cartridges/modules/black-ledger.md— staleness remediation rewrite (128×75 / Lisp / program-cart seam / OLED-CIPHER / recon gating) (separate PR — GWP-607 spec rewrite). -
docs/software/programs/enrichment-contract.md§8 — flip thelaunch-app/dossier-commitFFI bullet from “finalize with the engineering spike” to “finalized — ADR-0049” (separate PR with the spec rewrite). -
docs/software/programs/dossier-data-model.md§4/§8 — record gap D closed (nodossier-refileprimitive; auto-refile + operator-action); flip §8 FFI bullet to “finalized — ADR-0049” (separate PR with the spec rewrite). -
docs/software/cartridges/authoring/verb-partition.md— re-point the TRACE-back “no satisfier yet — gap” note to the deferred live-case Black Ledger expansion (separate PR with the spec rewrite). -
CLAUDE.md(umbrella) — no change: this ADR references the Canonical Hardware Specification (128×75, OLED-exclusive CIPHER) without altering any value; the ADR-0005 primitive-count summary in CLAUDE.md is unaffected (zero net new primitives).
The five unticked boxes are the spec-rewrite consequences this ADR creates — they are explicitly scoped to follow-on PRs (the design’s decomposition Next-steps 1/2) and are not part of this docs-locking PR. The two normative changes this PR lands — the new ADR and the ADR-0005 amendment — are ticked.
Narrative (for the design history)
Section titled “Narrative (for the design history)”AmberCalc was named a year of decisions ago as “program #7, a spreadsheet,” and the verbs that let a cart hand it a world — launch-app, dossier-commit, dossier-has? — were logged into the FFI ledger but left deliberately blurry: exact shapes “to finalize with the engineering spike.” The AmberCalc + Black Ledger design is that spike’s forcing function, and it made four calls that needed a home before any code could be written. This ADR gives them one. It ratifies the elegant choice that the spreadsheet’s formula language is just Fe itself — a cell is a symbol, a range is a list, sum is a function — so formulas are as inspectable and scriptable as everything else on the deck. It pins launch-app’s world as an opaque handle that dies at the mission boundary, not a copyable blob, so the cart installs a living ledger and the program navigates it without anyone ever raw-writing durable state. It closes the long-open “gap D” not by adding a verb but by deciding there shouldn’t be one — facts are captured by carts and filed by the engine or the operator, and that asymmetry is the whole point of the write boundary. And it keeps Black Ledger what it was always meant to be: not an arcade game bolted onto a ledger, but a forensic grammar — TRACE is CAR, CHAIN is CONS, CROSS is EQ — where the satisfying click of “follow the money” is a list operation you could have typed at the REPL yourself. The one verb the survey called the strongest, reconcile-by-assertion, stays a few lines of cart Lisp over the primitives that already exist, because the delta between a seized statement and the books is the clue, and computing a delta never needed to be carved into C.
Amendment Log
Section titled “Amendment Log”Amendment 2026-06-27 — The :world handle is opaque + LIVE (re-sync), with tick delivery, for ticking worlds (per GWP-625)
Section titled “Amendment 2026-06-27 — The :world handle is opaque + LIVE (re-sync), with tick delivery, for ticking worlds (per GWP-625)”Status effect: Accepted (unchanged). This amendment tightens Decision §2 and narrows the Option-E rejection; it does not change any FFI signature, tier, primitive count (still 74), or the :invalid-world semantics. No byte-level / format change.
Why now — the forcing function
Section titled “Why now — the forcing function”ADR-0049 §2 already named the :world handle opaque and said “the program reads/navigates the region live.” But it was drafted (2026-06-25) against AmberCalc + Black Ledger, whose v1 scope is cold-case (Decision §5: “v1 Black Ledger scope is explicitly cold case only”) — a static seized ledger that does not change underfoot. The shipped GWP-614 launch-app→AmberCalc transport is therefore copy-by-value: serialize the world plist across the cart/program Fe-context split, rebind it in the program context, no re-sync (Black Ledger world handoff (deleted 2026-07-29; recover from git history); MEMORY: Black Ledger world handoff). That is correct for a static world and was never wrong.
The world-gen architecture decision (2026-06-26) changes the requirement for every other first-party program. First-party data/recon programs are lenses + verbs over a central World Engine — one generated, coherent world advanced by a tick loop (hybrid: move-driven by default; carts opt into wall-clock heat), reactive + scheduled (MEMORY: Program world-gen architecture; software/gameplay-framework.md; the per-program “Tick-aware” clauses in programs/bzbx.md §“The world it reads”, programs/kommander.md, programs/keyring.md). A program now must see mid-mission world changes:
- bzbx — a host that comes online mid-scan, or a node another program revealed, must appear live in the open
top-style table; the shared-region:revealed?flag is the bzbx → CONDUIT invariant (programs/bzbx.md§“Open / forthcoming”: “the bzbx → CONDUIT live reveal … depends on the opaque live handle — tracked as a forthcoming ADR-0049 amendment”). - Kommander — a
:remotemount drops when its CONDUIT session is traced/killed (a World-Clock heat event); a copy in flight is cut off (programs/kommander.md: “the shipped copy-by-value transport can’t re-sync mid-mission; tracked as a forthcoming ADR-0049 amendment”). - Keyring — a crack races a live
:hold(trace) timer; the program must observe the trace advance mid-crack (programs/keyring.md: “needs the opaque live handle to see mission-state changes live”).
Copy-by-value cannot serve these: a snapshot taken at launch is frozen at launch. The three specs above each pre-recorded this exact gap as “a forthcoming ADR-0049 amendment.” This is that amendment. It does not introduce a new decision so much as ratify the §2 wording the world-gen decision now makes load-bearing, and reconcile it with the shipped static transport.
What this amendment ratifies (normative)
Section titled “What this amendment ratifies (normative)”N1 — For a LIVE / ticking world, :world is an opaque, mission-instance-scoped FE_TPTR handle into the live mission-data region — NORMATIVE. The handle is not snapshot-backed: program-side accessors read the region’s current state on every call, so a change the World Engine (or another program) makes to the region is visible to the active program without re-launch. This is Decision §2’s “reads/navigates the region live” promoted from a description to a requirement for live worlds. The handle properties from Decision §2 are unchanged and still hold: opaque (carts pass it, never walk it), read-only to durable state (consequence only via dossier-commit), mission-instance-scoped (resets at the mission boundary; a stale handle raises :invalid-world).
N2 — A static world MAY ship as a by-value snapshot — PRESERVED. When a program’s world does not tick within the mission (the cold-case class — a seized ledger, a frozen filesystem image), launch-app MAY deliver :world as the GWP-614 copy-by-value transport (serialize plist → rebind in the program context, no re-sync). This stays valid and is the shipped path for AmberCalc / Black Ledger v1. A by-value world is observationally a live handle whose region simply never changes — so a program written against the live-accessor contract (N1) runs unmodified over a static snapshot. The accessor contract is the single surface; “static” is the special case where re-sync is a no-op, not a second API.
N3 — A World-Clock tick/event reaches the active program over the event bus; the program re-renders on the delivered event — NORMATIVE. Re-sync is event-driven, not polled. The World Engine’s clock — riding the ADR-0045 scheduler/deferred-tick substrate (sched_every / sched_after on the virtual clock; power-off catch-up coalesces to one fat dt) — emits a typed world-event (a node coming online, a mount drop, a :hold/trace advance) onto the existing nosh_event_bus (the same bus Keyring’s crack-progress / key-cracked events already use; programs/keyring.md §“Engine reuse”). The active program receives the event as an ordinary event-loop wake and re-renders against the now-current :world region (the event-driven redraw model — render idles, fires on event or at the 20 fps animation cap; ADR-0036 / CLAUDE.md). A program does not busy-poll the handle; it re-reads through its accessors when woken. The hybrid trigger is unchanged: move-driven ticks fire on operator action; wall-clock heat ticks fire from the scheduler when a cart opts in. The typed world-event vocabulary (event kinds + payloads) is owned by the World Engine spec (GWP-626) — this amendment fixes only that delivery is via the event bus and re-render is event-driven, not the event taxonomy.
N4 — The run-state-vs-durable boundary is preserved verbatim. A live :world handle reads volatile, mission-scoped state — a mid-mission reveal/drop/trace-advance is run-state, never UDS or DOSSIER (ADR-0040 §6; enrichment-contract §4). The region (and every handle into it) resets at the mission-instance boundary; a handle from a prior instance raises :invalid-world (Decision §2, unchanged). Liveness does not widen the write boundary: the only durable consequence is still dossier-commit (sanctioned capture → engine/operator refile). Seeing the world change live and recording a fact about it remain two different acts — observation is free and volatile; the durable write is sanctioned. A program reacting to a tick (re-rendering, updating its table) writes nothing durable by doing so.
Reconciling the shipped copy-by-value transport (the migration)
Section titled “Reconciling the shipped copy-by-value transport (the migration)”The GWP-614 transport is not removed and not wrong — it is re-classified as the static-world (N2) path. Concretely:
- AmberCalc / Black Ledger v1 (cold case): keep copy-by-value. The seized ledger does not tick within a mission instance; re-sync would be a no-op. No code change is forced by this amendment.
- Live-world programs (bzbx / Kommander / Keyring, and future ticking worlds): require the live
FE_TPTRaccessor path (N1) + event delivery (N3). The implementation work — the World Engine generator/clock/event bus, the live region + program-side accessors, and wiring the event-bus wake into the program render loop — lands with the World Engine spec (GWP-626) and the per-program builds; it is out of scope for this docs amendment, which fixes the contract those builds target. The shipped copy-by-value serializer becomes the explicitstatic-class branch oflaunch-app’s:worlddelivery, not the only branch. - No FFI signature change:
launch-appkeeps its Decision §2 signature. Whether:worldis delivered live (N1) or by snapshot (N2) is a property of the world’s class (ticking vs. static), decided by the mission/World Engine, not a new keyword the cart passes. The program reads through the same accessors either way (N2).
This is the same shape as ADR-0045’s catch-up: one fn(dt) path, where the static case is just dt-into-a-no-op. One accessor contract, where static is re-sync-into-a-no-op.
What did not change
Section titled “What did not change”- Option E (structured walkable
:worldref) stays rejected. This amendment reverses the rejection only for the snapshot-vs-live axis — it makes the opaque handle live rather than a frozen copy. It does not make the handle walkable/structured: carts still pass theFE_TPTR, never dereference the region’s fields; the program is still the sole navigator of its own world (Decision §2 / Option E rationale: representation-coupling and region-mutation risk are unchanged and still disqualify a walkable ref). “Opaque + live” ≠ “structured + walkable.” - All FFI signatures, tiers, the
resultstruct,:payloadby-value semantics, thedossier-commit/dossier-has?contract, gap-D closure, andassert-balanceas a cart helper — unchanged. Primitive count unchanged (74). :invalid-worldon a stale (cross-instance) handle — unchanged; it is the existing lifecycle guard, now also the guard a live handle hits when its region has reset.- The
:world-region schema (per-program node shapes) is still per-program / World-Engine design (Decision §“Negative”, enrichment-contract §9) — this amendment fixes the handle liveness + tick-delivery contract, not any region’s internal layout.
Edits in this amendment
Section titled “Edits in this amendment”- Front-matter
Amended:line — appended the 2026-06-27 / GWP-625 entry. Related:line — added ADR-0045, the forthcoming World Engine spec (GWP-626) viasoftware/gameplay-framework.md, and the three live-handle dependents (programs/bzbx.md,programs/kommander.md,programs/keyring.md).- This Amendment Log section — N1–N4 + the copy-by-value migration.
Authority trail. World-gen architecture decision 2026-06-26 (central World Engine + hybrid tick; MEMORY: Program world-gen architecture); the live-handle gap pre-recorded in programs/bzbx.md / programs/kommander.md / programs/keyring.md §“Open / forthcoming” as “a forthcoming ADR-0049 amendment”; the scheduler substrate the World-Clock rides (ADR-0045); the enrichment-contract §4 (“a handle, not a copy … sees mission-state changes live”) and §3 (bare-launch default) the live handle realizes. Follow-on (out of this amendment’s scope): the World Engine spec (GWP-626) owns the generator + clock + typed world-event vocabulary; the per-program builds (runtime/programs/{bzbx,kommander,keyring}/) own the live-region accessors + event-bus render wake; enrichment-contract.md §3/§4 and the three program specs’ “forthcoming amendment” notes flip to “ratified — ADR-0049 (2026-06-27 amendment)” when the World Engine spec lands.
Amendment 2026-06-28 — The live :world handle threads across a program → program handoff (GWP-630)
Section titled “Amendment 2026-06-28 — The live :world handle threads across a program → program handoff (GWP-630)”Status effect: Accepted (unchanged). This amendment realizes N1/N3 for the cross-program case and fixes the host-seam mechanics that make it safe. It adds no new core NoshAPI primitive (the run-state world verbs below are program/cart-tier builtins, like AmberCalc’s cell/range in Decision §1) and changes no existing FFI signature, tier, or :invalid-world semantics.
Why now — the forcing function
Section titled “Why now — the forcing function”The 2026-06-27 amendment ratified the live :world handle (N1–N4) for a single launched program. GWP-630 (Phase 3 — cross-program threading) is the first consumer that needs the live world to survive a handoff between two programs in one mission — an operator going RIPSAW → CONDUIT → Kommander → AmberCalc must stay in one live world and one accumulating DOSSIER. Two facts about the shipped runtime made this a real gap rather than a no-op:
- Programs do not share an Fe context. Post-GWP-647 each program loads lazily into its own per-program context (
prog_S), and activating a different program closes the prior one’s context (screen_router.c). AnFE_TPTR:worldhandle minted into program A’s context is therefore destroyed when program B loads — it cannot be passed by Fe-value across the boundary. - The dispatch + DOSSIER FFI was bound only in the cart context.
launch-app/dossier-commit/dossier-has?were registered into the cart Fe context (nosh_lisp_bridge_register), not into the per-program contexts the router creates. The first-party programs were authored anticipating this seam — each guards those calls with(bound? '…)and degrades to nil — so the seam was a deliberate stub, not a bug.
The underlying WorldRegion and the DossierStore are already process-global run-state (they survive a context switch); only the Fe access and the by-reference transport were missing.
What this amendment ratifies (normative)
Section titled “What this amendment ratifies (normative)”N5 — A program-initiated launch-app threads the live :world by reference, re-minting the SAME region into the target context. When a program hands off ((launch-app :other …)) and the caller holds a live :world (passed explicitly as :world, or auto-carried from the caller’s own *world* when no :world is given), the host re-mints a fresh FE_TPTR over the same process-global region (by its region-id + generation) into the target program’s freshly-opened context. A reveal/online/trace change made by program A is visible live through program B’s *world* after the handoff — N1 across a context switch. The handle value crossing the seam is the region identity, never the Fe wrapper (which is context-local); this is the cross-context analogue of N1’s “not snapshot-backed.” A non-live launch (no live handle in play) keeps the N2 copy-by-value snapshot path unchanged.
N6 — DOSSIER is the durable cross-program thread; the dispatch + DOSSIER FFI is bound into every program context. launch-app / dossier-commit / dossier-has? (plus mission-active? and the run-state world verbs below) are bound into the System context and every per-program context via a host-installed program-bind seam, so a program can hand off, commit a finding, and gate on a prior finding. Because the DossierStore is process-global run-state, a fact program A commits is immediately visible to program B’s dossier-has? — the shared DOSSIER is the durable thread across the handoff (the live :world is the volatile thread; DOSSIER is the durable one). The program-context dossier-commit / dossier-has? are soft: outside a mission they return nil rather than raising :not-in-mission (a first-party program legitimately runs bare — enrich-never-gate, ADR-0042); the cart-context pair still raises (a cart calling them outside a phase is an authoring error). N4’s write boundary is preserved verbatim — the only durable consequence remains dossier-commit.
N7 — A program-initiated handoff is DEFERRED, not synchronous. Because activating the target closes the caller’s prog_S, a program’s launch-app must not switch while that context is on the C stack (a use-after-free). It therefore stages the request (target + live-handle value + serialized payload) and returns; the host drains the staged launch after the key/render dispatch returns, at a safe point — the same shape as the existing *deck-route* drain. A cart launch-app stays synchronous (the cart context is never the one torn down) and is made live-aware on the same path. This is a host-seam mechanic, invisible to cart/program authors: launch-app reads the same either way.
N8 — Two run-state world verbs are added at the program/cart tier (not core NoshAPI). (world-install! seed archetype) generates one coherent live world and returns its opaque handle — the seam by which a worked mission (a cart) installs the world and hands it to the first program; (world-reveal! w address) reveals a node in the live region (run-state). Both are program/cart-tier builtins bound alongside the dispatch set, not additions to the ADR-0005 core enumeration (same status as Decision §1’s formula builtins and Decision §4’s assert-balance). They never widen the durable-write boundary (N4) — reveal is volatile run-state; the durable write is still dossier-commit.
Cross-program seams this unblocks (built in GWP-630)
Section titled “Cross-program seams this unblocks (built in GWP-630)”- CONDUIT ↔ Keyring — a Keyring crack commits
host:<id>:cred(DOSSIER) and captures the key to the durable keystore; CONDUIT opens pre-authenticated from either the DOSSIER cred fact (this mission) or an owned key in the durable keystore (a:method :keyhost — cross-mission de-risk). - Kommander “open with” — the LINK verb dispatches the focused target by type: a file → AmberCalc (
launch-app :ambercalc :payload (:open <path>), carrying the live world), a directory → the CONDUIT-into-dir seam; exfil/located commits to DOSSIER unchanged. - RIPSAW node-of-interest → DOSSIER —
promote-node!commits the focused entity as a profile candidate (<type>:<id>:profile), distinct from the FOLLOW→COMMIT edge path, so a surfaced node becomes a kept cross-lens fact.
What did not change
Section titled “What did not change”- All FFI signatures, tiers, the core primitive count, the
dossier-commit/dossier-has?contract, gap-D closure,assert-balanceas a cart helper, and:invalid-world— unchanged. N5–N8 add no core primitive; the world verbs are program/cart-tier builtins. - Option E (structured walkable
:worldref) stays rejected. The handle remains opaque; the cross-program transport passes the region identity, never a walkable structure. - N1–N4 — unchanged; N5–N8 extend the live-handle contract to the handoff case and fix the host seam, they do not revise the single-program contract.
- The DOSSIER viewer program is still unbuilt (
:dossieris on the roster but has no screen); opening a viewer on a promoted node is deferred — the run-state promotion (the commit) is the seam GWP-630 delivers.
Edits in this amendment
Section titled “Edits in this amendment”- Front-matter
Amended:line — appended the 2026-06-28 / GWP-630 entry. - This Amendment Log section — N5–N8 + the cross-program seams.
Authority trail. GWP-630 (Phase 3 — cross-program threading + a worked multi-program mission), Josh’s 2026-06-28 design decisions (full-live-now + launch-app-payload dispatch + both cred-pull paths); the per-program “forthcoming ADR-0049 amendment” notes the live handle realizes; GWP-647 (lazy per-program contexts) the deferral works around; the process-global DossierStore (ADR-0040) the durable thread rides.