Ghostlight/docs/architecture/ghostlight-library-extraction-postmortem.md
Meta Vens d86ed9f4a2 Close L0: postmortem, ledger, target and state reconciled
The kernel extraction is finished. The world kernel is the ghostlight
library crate; Dungeon consumes it across a boundary sealed by admission,
proven from outside the crate.

The cut map records the last two fix batches and Q1-10, defaulted: the
minter scanner stays a tripwire, with Clippy recorded as the fix. The
subtraction ledger missed its estimate, +210 net source lines against about
+65. The reasons: the scanner grew chasing evasions, the boundary needed
about 90 more public items than planned, and the rejection tests buy a
ruling made after the estimate.

The postmortem lists what Soul caught, the operator corrections, the
incidents, and the open follow-ups. The target states which invariants
hold and how. The plan, map.yaml and the handoff point at L1. The L0
evidence record is rewritten as its distilled lessons; the previous text is
archived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 12:24:49 +02:00

10 KiB
Raw Blame History

Ghostlight library extraction (L0) postmortem

Written 2026-09-16. Evidence: the cut map (ghostlight-library-extraction-cut.md), the target (ghostlight-library-extraction.md), commits in Ghostlight and gamecult-ops, and the Soul reports recorded in the map.

Summary

The world kernel moved out of the ghostlight-dungeon binary into the ghostlight library crate, and Dungeon became a consumer across a public boundary. Thirteen files (about 52k lines) moved by rename with no content change. The boundary is sealed by admission: external code may build a syntactically valid ID, digest or opportunity, and the kernel rejects every one it did not issue, proven by rejection tests written as an external crate. gamecult-ops' build commands run the library tests they would otherwise have skipped.

State at writing: landed and pushed in both repos, Windows-verified. The Linux release is Idunn's recipe run on Yggdrasil and has not been exercised; no Windows result proves it. The operator owes nothing for L0.

Scope and invariants

The target set seven invariants: sealed kernel at the crate boundary, no behavior or schema change, no Dungeon concept in the library, dependencies pointing one way, a working release path, unaffected external pins, and one new crate with no feature matrix. Excluded: every library capability of plan step 15 L1–L4, schema renames, the network path for outside consumers, and deployment cutover.

Invariant 1 changed meaning during the migration (ruling Q1-9). It began as "sealed items are unreachable from outside" and ended as "the kernel rejects anything it did not issue", because the first reading was not true of the code and could not be made true without a design change L0 excluded.

Timeline

Cut Repo, commits Soul passes Notable finding
Map Ghostlight 02bbf62, 6ddabca — Classifying the fixture tests found no kernel test to move; Q1-7 and Q1-8 followed
1 Ghostlight 108b691..f859d2c 1 F1: public types deriving Deserialize are constructible from outside; the compile_fail tests pin names only
2 gamecult-ops 6aba281 1 The specified assertion was vacuous (substring match); the acceptance path had an unlisted fourth site
2 fix gamecult-ops 803f2c7 1 Exact-line pins prove presence, not execution (if false, true ||, heredoc)
Q1-9 Ghostlight 7c1d1e9 — Operator ruled unforgeable admission
1 fix Ghostlight a8d3377..2865ea7 1 F3's premise was wrong; forgeries differed from real digests in length
S batch Ghostlight bde05de..af9c95a 1 Case-insensitive and hex-only comparisons survived
T batch Ghostlight b14507c, ab95d78 1 None

Structural delta

Area Estimate Actual
Crate sources, including in-module tests about +65 net +1202 / −992, +210 net
Integration tests not planned +479 (crates/ghostlight/tests/external_admission.rs)
Manifests and lock about +50 +50 / −2
Ghostlight recipe about +6 +10 / −5
gamecult-ops about +5 +61 / −9

The source overrun has two causes. The single-minter scanner in crates/ghostlight-dungeon/src/app_session.rs grew about 110 lines chasing Soul's evasions, and it is still evadable (S4). The public boundary needed roughly 90 more items than the map enumerated, each forced by a compile error or a private_interfaces warning. The integration tests were not in the estimate because Q1-9, which requires them, was ruled after it.

Deleted: mod world;, nine unused re-exports, the cfg(test) re-export block, five inference fixtures, fixture_controller_opportunities, and Dungeon's own principal evidence struct. Added: one library crate, a public pub use boundary, port constructors, ten compile_fail doc-tests, the external admission tests, and two Dungeon soul tests. Parked: nothing. Features added: none. Binaries added: none.

What Soul caught

  • Untested rulings. Ruling Q1-4 said sealed constructors must be unreachable under every configuration. The code satisfied the letter (no feature gate) and missed the intent: Deserialize is a public constructor. Ten green compile_fail tests could not see it, and adding an ID-minting Default impl left all ten green. It would have failed as a false guarantee in any later review that trusted the target doc.
  • Tests pinning spelling, not behavior. Every text scanner lost to a short evasion: the minter count to an alias, a subdirectory, a type alias and a function value; the gamecult-ops wiring test to a two-line deletion that reattached the pinned line to the wrong container; its fix to control flow around exact lines. Each would have let a real regression through with a green check.
  • Forgery shape. Rejection tests that used short fake digests passed against a kernel comparing only length, only a prefix, all but the last character, case-insensitively, or only the hex after the label. Each would have admitted a digest the kernel never issued.
  • Spec errors. The map's vacuous require_text, its unlisted acceptance site, its miscount of Dungeon tests, and Soul's own F3 premise (the two accessors it called uncalled are called) were all wrong, and all were caught before landing a fix built on them.
  • Unmeasured claims. "No new package in Cargo.lock", "zero fixture_ hits", "a guard that never fired", and a 13-warning ceiling nobody could reproduce.

Operator corrections

  • Session Zero is Dungeon authority. The first design placed a player flow on the kernel. Missing context: no document named which features belonged to the library and which to a consumer.
  • Focus was too narrow. Focus as a set of subjects became a typed detail rule, a function from place to detail level. Missing context: Delvehold's constraints as a consumer; the design generalized from Dungeon alone.
  • Extract first. The library capabilities were designed before the crate they belong to existed.
  • The titled elaborators had been cut without an operator decision; the cut conflated a verifier with the lenses it verified.
  • Proportion. Asked how much Q1-10 mattered, the operator said not at all. It was a tripwire against the project's own code, and Self should have defaulted it instead of posing it.

Incidents

  • A Hands agent returned its private to-do list as its report while its own background cargo job held the locks. Cost: one resume round. Rule: briefs now say to wait for long jobs and never end a turn with a to-do list.
  • A mutation run was invalidated by git checkout reverting uncommitted fix code. Cost: two mutations rerun. Rule: run mutations against committed code, or restore with a diff rather than a checkout.
  • Cached no-op builds reported zero warnings. Cargo replays warnings only when it rebuilds. Rule: measure warnings from a forced rebuild, and compare distinct messages by cmp.
  • A commit message described code replaced before commit (fac90e6). Left in history; recorded in the map.
  • A pre-existing cargo fmt --check failure across the library, found in passing.

What worked

  • Classifying tests before moving them. Q1-4's "move the tests" had nothing to move, and the classification turned a vague ruling into two concrete ones.
  • Soul building an external scratch crate to break the seal. The serde route was invisible from inside the crate and to every doc-test.
  • Holding map edits until Soul finished, so every Soul pass read a clean tree.
  • Having Hands report spec discrepancies instead of redesigning: the vacuous assertion, the fourth acceptance site, the recovery_required collision.
  • Parallel Hands in separate repos only when the second did not depend on the first landing.

What to change in the pipeline

  • Brief Soul to attack constructibility, not names, on any sealed boundary. Evidence: F1 on Cut 1, found only because the root agent added the serde route to the brief by hand.
  • Treat a text scanner as a tripwire with stated limits, never as proof of a semantic property. Prefer a semantic tool (Clippy lints, the type system) or record the property as unenforced. Evidence: every scanner in L0 lost.
  • Rejection tests must use forgeries shaped like real values. Evidence: S1 and the two survivors after it.
  • Default proportionate questions instead of asking. A fork that guards only against the project's own code is a default with a recorded follow-up. Evidence: Q1-10.

Open follow-ups

Item Owner Where Why it can wait
S4 minter scanner evadable; Clippy disallowed-methods recorded as the fix Dungeon crates/ghostlight-dungeon/src/app_session.rs:523-668 Guards only against the project's own code
S5 credential-name test splits on one literal marker Dungeon crates/ghostlight-dungeon/src/runtime.rs:3584 Same class as S4, pre-existing
S6 stable rustc does not enforce compile_fail error codes Library crates/ghostlight/src/lib.rs:15-57 Admission is proven by the external tests; codes need a snapshot tool
S2 world_id check redundant, S3 decline path unreachable from outside Library lib.rs:4193, lib.rs:1603 Redundant by construction and crate-private respectively
ScopeDigest, WorkLane, ScopeComponents public through public enums Library KernelError, ControllerWorkCustody, NarrativeCheckpoint Closing them reshapes public enums, a design change
cargo fmt --check fails across the library Library crates/ghostlight Pre-existing; formatting only
Recipe schema drift (foundation.v0, controller_work.v3 against consumer.v4, controller_work.v15) Deployment gate deployment/idunn/recipe.toml Changes no behavior in L0
build_provenance not run by the recipe Deployment gate deployment/idunn/recipe.toml A recipe change with its own reason
Redeploy of a sealed commit runs no tests; exact-line pins cannot see control flow; set -e unpinned; duplicate blocks pass; web container and Connector steps pinned by substring; 18-line option window Deployment gate gamecult-ops/scripts/deploy-ghostlight-yggdrasil.sh, test-ghostlight-yggdrasil-wiring.sh Legacy actuator under a do-not-invoke warning, retired at the gate
Linux release unverified Operator, via Idunn Yggdrasil Needs the deployment gate