Ghostlight/docs/architecture/ghostlight-library-extraction-cut.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

56 KiB
Raw Blame History

Ghostlight library extraction — cut map (plan step 15, L0)

Status: cut map. Ends are owned by ghostlight-library-extraction.md; this document owns the means. Written 2026-09-15 against d83534c on codex/ghostlight-dungeon-mvp; first version committed at 02bbf62; refreshed 2026-09-16 with the operator's rulings.

Cut 1 landed at Ghostlight 108b691..f859d2c. 108b691 is 13 renames with zero content change; f859d2c builds. Verified by Soul's own runs, not the Hands report: library 419 passed + 1 ignored with test names identical to the base world:: set; 10 compile_fail doc-tests pass, also under --all-features; Dungeon 50 + 1 ignored, all 49 base names present and the +2 exactly the two new soul tests; no [features], cfg(feature, or cfg(any(test in the library; no schema string, _NAMESPACE, derive_id, serde attribute, or Preimage changed; the rewritten tests fail under mutation of what they pin. Windows only; the Linux release remains Idunn's.

Soul found six defects. Triage:

  • F1 seal is name-based (high, open): every pub ID derives Deserialize (crates/ghostlight/src/lib.rs:186-200), as do ScopeDigest (:532) and DecisionOpportunity (:559), so an external crate mints IDs, reproduces ScopeDigest::fixture, and forges a DecisionOpportunity into derive_cover from JSON. The reducer still rejects forged opportunities (lib.rs:4192-4201), so kernel integrity holds and the invariant's wording does not. Dungeon's own Eve payloads deserialize these types, so removing the derives is not available. Operator question Q1-9 below.
  • F2 single-minter test is bypassable (medium, fix): the count at crates/ghostlight-dungeon/src/app_session.rs:530-555 greps a literal and misses <VerifiedPrincipalEvidence>::new(, an alias, a renamed test module, or a subdirectory.
  • F3 surface wider than bought (low, fix): 71 pub identifiers are unreferenced by Dungeon; ScopeDigest, WorkLane, and ScopeComponents compile as pub(crate) and were opened only to quiet private_interfaces.
  • F4 "Eve" named in library error strings (low, fix): crates/ghostlight/src/controllers.rs:1494,2576,2582,2584. Pre-existing; invariant 3 is still unmet.
  • F5 two Hands claims false as stated (cosmetic, recorded): Cargo.lock did gain a ghostlight package entry (no new external package), and fixture_[a-z] has 8 benign hits in Dungeon.
  • F6 recipe schema drift (pre-existing, recorded): deployment/idunn/recipe.toml declares world_state.foundation.v0 and controller_work.v3 against code at consumer.v4 and controller_work.v15. Outside L0; it changes no behavior here.

Cut 2 landed at gamecult-ops 6aba281 (text only; nothing live was run). The library test step is in the runbook, the deploy script, and the wiring test, ordered as recipe.toml orders it. Two spec defects Hands reported rather than redesigned: the assertion this map specified was vacuous (require_text is grep -Fq, and the string given is a substring of the persona-projection line, so it passed with the library step deleted), so Hands used the file's existing exact-line helper and proved it fails under deletion; and the acceptance test path lives in acceptance_test= at scripts/deploy-ghostlight-yggdrasil.sh:34, a fourth site this map did not list, pinned by the wiring test and repeated in the runbook prose. Operator consequence: changing acceptance_test changes the acceptance binding, so sealed witnesses under /srv/ghostlight/acceptance/<binding-sha>/ from before this commit will miss and regenerate.

Soul passed on Cut 2 and found the assertions pin text, not invocations:

  • C2-F1 (high, fixing): deleting the two lines above the pinned cargo line in scripts/deploy-ghostlight-yggdrasil.sh leaves it byte-identical as trailing argv on the persona-projection container, so the kernel tests never run and the wiring test still exits 0. Prefixing the docker run line with : does the same.
  • C2-F2 (medium, fixing): nothing pins the order of the four cargo steps; the library block moved after the release build and the wiring test passed, which would seal an artifact before the kernel tests ran.
  • C2-F3 (medium, fixing): the wiring test never reads the runbook, so only three of the four acceptance sites are enforced to agree.
  • C2-F4 (medium, pre-existing, recorded): deploy-…sh:937 takes a reuse branch when the release directory for a commit already exists, so a redeploy of a sealed commit runs no cargo steps at all. This bounds "the deploy runs the kernel tests" to a commit's first build. Operator owns whether that is right.
  • C2-F5 (low, recorded): docs/repo-census-2026-09/* still shows src/world/.... Dated census; no deploy effect.

Held: the step sits in the same branch, image and mounts as its neighbours, and set -euo pipefail with trap means a library-test failure does abort the deploy. The acceptance test exists at crates/ghostlight/src/controllers.rs:8980 under -p ghostlight --lib.

C2-F1, C2-F2 and C2-F3 are fixed at gamecult-ops 803f2c7 (wiring test only). A new require_invocation helper pins each of the four cargo steps as a whole contiguous docker invocation and returns its start line; three numeric comparisons assert the recipe order; the runbook became a checked file, tying all four acceptance sites together. All four of Soul's mutations now exit 1 where they previously exited 0, and every pre-existing assertion still passes. require_order was unusable here because its last_line resolves a substring and would have bound the library step to the acceptance invocation.

Soul passed on the fix batch and confirmed it does what it claimed: whole-invocation pinning survives image swaps, line swaps, re-indentation, continuation splits and appended flags; the order checks catch a move into the reuse branch; the runbook is tied in both directions; fail inside a command substitution does abort, because set -e fires on the assignment's status; nothing was weakened. It also found the structural limit, recorded and not fixed:

  • C2-F6 (high, recorded): an exact-line pin proves the lines exist, not that they run. Wrapping the block in if false, prefixing the opener with true || \, or swallowing it in a heredoc keeps every pinned line byte-identical and the checker exits 0.
  • C2-F7 (medium, recorded): the four steps carry no explicit || exit; abort depends entirely on set -euo pipefail at deploy-…sh:2, which nothing pins. Removing the e leaves the checker green, so a failing kernel suite would be followed by a sealed release.
  • C2-F8 (low, recorded): a duplicated block passes, since the helper takes the first match and never asserts a count.

These stay recorded rather than fixed: that script is a legacy actuator under Ghostlight's do-not-invoke warning and is retired at the deployment gate, and the authority that actually runs the tests is Ghostlight's own deployment/idunn/recipe.toml. Closing them means control-flow analysis in a bash checker for a script we intend to delete. Revisit only if the deployment gate keeps the script.

Follow-ups outside this migration, from the fix batch:

  • scripts/deploy-ghostlight-yggdrasil.sh:974, the web container, is pinned by substring and survives a : prefix that neuters it.
  • The Codex Connector's four cargo steps are bare substring pins with no invocation or order assertion: the connector-side C2-F1 and C2-F2.
  • require_command_options searches a fixed 18-line window, so a block that grows past it stops being checked silently.

Cut 1 fix batch landed at Ghostlight 7c1d1e9..2865ea7: a8d3377 external rejection tests (Q1-9), fac90e6 single-minter check by name (F2), 7cbb9e3 Eve removed from library text (F4), 2865ea7 public surface 262 → 258 (F3). Hands' correction to F3 is confirmed by Soul: Dungeon calls bound_scope_digest and fresh_scope_digest, and ScopeDigest, WorkLane and ScopeComponents are held public by KernelError::ScopeChanged, ControllerWorkCustody::Uncertain::lane and NarrativeCheckpoint variant fields; closing them means reshaping public enums, which F3 does not license. fac90e6's commit message describes a replaced brace-counting scanner; the landed code and its doc comment are correct.

Soul held: the rejection tests are a true external crate with no #[path] or include!, driving ControllerPort::submit_controller through the mailbox into reduce with no pre-validation; every commit moves revision and state_digest, so the unchanged-state assertions are sound; the positive test goes red when issued opportunities are refused; counts, the 14 distinct warnings, and both manifests are unchanged.

Soul found:

  • S1 (medium, fixing): both forged digests differ in length from a real sha256: + 64 hex, so comparing only lengths at crates/ghostlight/src/lib.rs:4203 leaves all seven tests green. The realistic forgery is a stale same-length digest from an earlier revision.
  • S4 (medium, open, Q1-10): the minter scanner at crates/ghostlight-dungeon/src/app_session.rs:523-668 misses a pub type alias, a multi-line alias, a } // tests closing brace, non-rustfmt indentation, and ::new taken as a function value.
  • S7 (low, fixing): controllers.rs:122 provider_model closes to pub(crate) with no error and no warning change.
  • S8 (cosmetic, fixing): mailbox.rs:580 names AppSessionOwner and controllers.rs:2606 names AppSession.
  • S2 (low, recorded): neutering the world_id check at lib.rs:4193 leaves the suite green because ScopePreimage includes world_id, so the scope lookup refuses first. Redundant by construction; not isolable from outside the crate.
  • S3 (low, recorded): the decline path (lib.rs:1603) is pub(crate) and unreachable from an integration test.
  • S5 (low, recorded, pre-existing): runtime.rs:3584's credential-name test splits on one literal #[cfg(test)] marker and is evadable the same way the minter test was.
  • S6 (medium, recorded): stable rustc does not enforce compile_fail error codes; E0603 changed to E0999 still passes. All ten doc-tests prove "does not compile", not privacy, and the three #[cfg(test)] items (SubjectId::issue, ScopeDigest::fixture, the fixture_* fns) prove non-existence. Enforcing codes needs a snapshot tool such as trybuild; recorded, not added.

Q1-10 how the single minter is enforced (defaulted, 2026-09-16). Asked how much it mattered, the operator said not at all. The scanner stays as a tripwire with stated limits: it guards only against the project's own Dungeon code, and what evades it is deliberate. S4 is a recorded follow-up, with Clippy's disallowed-methods for ghostlight::VerifiedPrincipalEvidence::new noted as the fix if a second minter ever becomes plausible.

S1, S7 and S8 landed at Ghostlight bde05de..af9c95a. S1 submits a genuinely issued digest made stale by another controller's speech, plus the current digest with its last digit altered. Soul confirmed the premise check fails when staleness does not happen and that no earlier check intercepts the forgery, then found two surviving comparison mutants (case-insensitive, hex-only after the label). Those, and "player" vocabulary in library tests, landed at b14507c (uppercased-letter, bare-hex and relabelled forgeries, all six comparison mutants killed) and ab95d78 (test-only rename to "human", one test name changed). Soul's final pass found nothing to fix.

L0 closed, 2026-09-16. Postmortem: ghostlight-library-extraction-postmortem.md. Follow-ups are listed there.

Q1-9 what sealing means (ruled, operator, 2026-09-16): option A. The invariant is unforgeable admission, not unconstructible types. External code may hold a syntactically valid ID, digest, or opportunity; the kernel must reject any it did not issue, and that is proven by rejection tests written from outside the crate. The compile_fail set stays for the genuinely private items. The question as first posed: A: restate invariant 1 as unforgeable admission — external code may hold a syntactically valid ID or opportunity, and the kernel must reject any it did not issue — and prove it with rejection tests from an external crate plus the existing compile_fail set. B: make the types unconstructible from outside, which means Dungeon stops deserializing DecisionOpportunity in its Eve payloads and receives an opaque token instead, a design change L0 excludes. Recommended: A, because the kernel already enforces it and B moves work into L0 that the target puts out of scope. Whichever is chosen, F2 and F3 are fixed in the same Hands batch.

Rulings (operator, 2026-09-15):

  • Q1-1 crate name: "crate name is ghostlight."
  • Q1-2 adapters: "vault.rs and sdk_inference.rs both go in the library."
  • Q1-3 principal seam: "option A, library-owned VerifiedPrincipalEvidence with a public new, plus the Dungeon count test pinning the single minter."
  • Q1-4 fixtures: "NOT the test-support feature. The operator takes the alternative: the sealed ID issue() constructors and ScopeDigest::fixture stay unreachable from outside the library under every configuration, including feature-enabled builds, and the Dungeon tests that currently need them move into the library instead. Rationale: a sealed constructor exposed behind a feature is still reachable by any crate that turns the feature on, which contradicts invariant 1." (Supersedes the test-support design in the 02bbf62 map; that text is history and is not repeated here. The classification below found that none of the affected tests is a kernel test, which raises Q1-7 and Q1-8.)
  • Q1-5 persona projection: "ghostlight-persona-projection stays a separate crate."
  • Q1-6 gamecult-ops: "edit the five gamecult-ops cargo lines in Cut 2; do not retire the scripts (retirement belongs to the deployment gate)." Correction to the 02bbf62 map: Ghostlight's handoff does call those helpers unsafe — notes/fresh-workspace-handoff.md:389-391: "Do not invoke the current bounded Connector or Ghostlight redeploy helpers; they fail at the root/Idunn Git boundary and still embody duplicate target deployment authority." The earlier claim that no such doc existed was wrong; the ruling to edit rather than retire stands on the deployment gate owning retirement, not on the scripts being live.

Rulings (2026-09-16), taken by Self under the operator's standing "Go" after Q1-4's classification found no kernel test to move. Either may be reversed by the operator; both are recorded here as the live design:

  • Q1-7 port value constructors: option A. PreparedInference::prepare (renamed from prepare_invocation), InferenceOutput::new, and InferenceFault::{retryable, recovery_required, integrity_violation} become the port's public contract; the five fixture_* items are deleted and TracingInferencePort becomes an ungated decorator. A public trait whose return types only the library can construct is not a port, none of these touches a sealed item, and the change is a net deletion.
  • Q1-8 cover source: option A. The tick-ordering test derives its cover from a real world through derive_cover and asserts 2 cells, with its ordering and shared-tick assertions unchanged; fixture_controller_opportunities, the only outside caller of a sealed issue()/ScopeDigest::fixture, is deleted.

Together these satisfy Q1-4's intent: the sealed ID issuers and ScopeDigest::fixture stay private #[cfg(test)] items with no feature gate anywhere, unreachable from outside the library under every configuration.

Open: nothing blocking Cut 1. Operator checks remain on the Linux release (Idunn's recipe run), which no Windows probe can prove. Follow-ups outside this migration: see "Findings not assigned to a cut".

Probe names versus real names: probes 1–4 built the crate as ghostlight-world (use ghostlight_world::…); probe 5 rebuilt it as ghostlight (use ghostlight::…). The difference is the package name, the directory crates/ghostlight, and the path prefix; every mechanism finding held in both. This map uses the real name throughout.

What the probes established

All probes ran in scratch worktrees at d83534c (both removed, repo clean) on the Windows workstation with cargo check and cargo test --doc; they prove the crate graph and visibility on Windows only. The Linux proof is Idunn's recipe run on Yggdrasil. Logs are in the session scratchpad (probe1*-check.log, probe3*-visibility.log, probe5.log, base-test-list.log); they are not repo artifacts.

  • Base test census (cargo test -p ghostlight-dungeon --bin ghostlight-dungeon -- --list, repo target dir): 469 unit tests in the bin, of which 420 are under world:: (action 34, clock_tests 45, consumer 30, controllers 81, cover 18, custody_tests 17, elaboration 14, journal 24, knowledge_tests 21, mailbox 15, patch 34, sdk_inference 23, soul_knowledge_tests 9, tests 32, vault 6, witness_tests 17) and 49 are Dungeon's own (app_session 7, eve 5, heimdall 5, idunn_health 4, mesh 4, runtime 24). tests/build_provenance.rs has 1. ghostlight-persona- projection has 13 unit, 0 doc. Two tests are #[ignore]: world::controllers::tests::real_codex_connector_cognition_modes_commit_speech and runtime::tests::live_smoke_seeds_then_ticks_a_world_against_the_connector.
  • Base warning: mod.rs:28-32 re-exports nine names nobody in the crate uses (ControllerOpenError, NarrativeCapture, NarrativeDecision, NarrativePending, OperationalCapture, OperationalDecision, OperationalPending, SourceRange, TranslationGapSummary). Their deletion is inside Cut 1.
  • The move compiles (probe 1d). Moving src/world/* to the new crate (mod.rs → lib.rs), rewriting crate::world:: → crate:: inside it and → ghostlight:: in Dungeon, and moving VerifiedPrincipalEvidence into the library root: cargo check on the library, its tests, the Dungeon bin, and the Dungeon tests all pass. Two mechanical faults: the 94 pub(super) sites in the former mod.rs are illegal at a crate root (become pub(crate)), and consumer.rs:62 needs serde_bytes in the library manifest.
  • What Dungeon needs public (probe 3, four compiler rounds from every item pub(crate)): 54 production items, 22 test-only items, 38 fields on 10 structs, 36 methods, the opaque IDs AffordanceId/CommandId/ SubjectId, and FictionalMinutes(u64) (one Dungeon test). Nothing sealed appears: WorldKernel, WorldState, reduce, apply_effect, CommandEnvelope, AuthenticatedCaller, the journal, *::issue() and ScopeDigest::fixture are never named by Dungeon. Under the narrowed library cargo check -p ghostlight emits 646 dead-code warnings — a second enumeration of the same boundary (an item used only by Dungeon is dead inside the library until it is pub).
  • The sealed set under the Q1-4 ruling (probe 5, crate ghostlight, no cargo features, fixtures left #[cfg(test)]): ten compile_fail doc-tests on the library root pass under cargo test -p ghostlight --doc and again under cargo test -p ghostlight --all-features --doc (the crate declares no features, so the second run is the same build; it is the command Soul re-runs if a feature is ever added). cargo check -p ghostlight-dungeon --bin ghostlight-dungeon passes: production needs no fixture. cargo check -p ghostlight-dungeon --tests fails on exactly four errors — runtime.rs:3081 (ControllerWork… family, Inference… types, three fixture_inference_*), :3570 (fixture_controller_opportunities), :2582 (TracingInferencePort), :2581 (InferencePort, because the base re-export at mod.rs:66 is cfg(test)). Those four sites are the whole cross-boundary test surface; they are classified under Cut 1.
  • Compile-fail mechanism: doc-tests compile as an external crate, which is exactly the boundary; no trybuild, no scratch crate. The ten (use ghostlight::WorldKernel, reduce, WorldState, CommandEnvelope, AuthenticatedCaller, journal::WorldJournal, SubjectId::issue(), ScopeDigest::fixture("x"), fixture_controller_opportunities(&[]), fixture_inference_output(..)) are pinned to E0603, E0599, E0599, E0425, E0425.
  • Negative-grep collisions checked in src/world/: axum, tower_http, jsonwebtoken, heimdall, eve::, idunn_health, Session Zero have zero legitimate hits. Dungeon collides with the vault fixture path Spoilers/Dungeons/Provenance.md (vault.rs:489,581): use \bDungeon\b. player collides with tests::player() (consumer.rs:966): do not publish a bare player pattern. app_session has 5 hits, all the principal seam, and must reach 0.
  • Line endings: index and working tree are LF (git ls-files --eol reports i/lf w/lf); core.autocrlf=true rewrote every fresh scratch worktree to CRLF. Hands edits in F:\Projects\Ghostlight itself, never in a fresh worktree; if a worktree is unavoidable, run git ls-files --eol crates after the move and refuse any w/crlf. sdk_inference.rs:1831 normalizes \r\n before scanning source.

Cut 0. Captures

  • Repo/branch: Ghostlight codex/ghostlight-dungeon-mvp at 02bbf62 (source identical to d83534c). No commit; output lives in the session scratchpad.
  • First:
    • Test census: cargo test -p ghostlight-dungeon --bin ghostlight-dungeon -- --list > base-tests.txt (PowerShell > writes UTF-8 BOM + CRLF; compare post-cut with the same redirection and Compare-Object on trimmed lines after stripping the world:: prefix). Expected 469.
    • A world.cc written at base. No store exists on disk (F:\Projects\Ghostlight-smoke\ghostlight holds no world.cc), so produce one: in a scratch copy at d83534c, add an ignored test beside mailbox.rs:966 fixture() that opens WorldMailbox::open(<capture>/world.cc), creates through create_fixture, approves, activates, and ticks three times through submit_clock; record snapshot().revision, the state_digest, and the last commit_digest from operator_log to capture.txt. Copy world.cc and capture.txt to the scratchpad. After Cut 1 the same test body, in the library's mailbox tests, reopens that file and asserts the three values are equal. The controller-work.cc layout is pinned by the untouched CONTROLLER_WORK_SCHEMA/CONTROLLER_WORK_ROW constants (controllers.rs:75-76) and the existing v7/v8 refusal tests (controllers.rs:6487,10654); no separate capture.
    • Get-ChildItem target -Recurse | Measure-Object Length -Sum: 7.5 GiB at the start of this pass.

Cut 1. Extract the library crate

  • Repo/branch: Ghostlight codex/ghostlight-dungeon-mvp from 02bbf62. One commit; the crate graph does not build half-moved. Depends on Cut 0 and rulings Q1-7, Q1-8.

  • First: Cut 0 captures in hand; git status clean; working in the main checkout (LF).

  • Deletes first:

    • crates/ghostlight-dungeon/src/world/mod.rs:28-32 — the nine unused re-exported names above (leave CellRun, ConnectorBinding, ControllerError, ControllerModels, ControllerPendingReason, ControllerRunner, ControllerWorkCustody, NarrativeRun, OperationalRun, SubmissionDisposition, open_controller_work, open_inference).
    • crates/ghostlight-dungeon/src/world/mod.rs:61-72 — the #[cfg(test)] re-export block (12 lines). The port traits and their value types in it return as ungated production pub use under Adds; the three fixture_inference_* names do not return.
    • crates/ghostlight-dungeon/src/world/mod.rs:483-508 — fixture_controller_opportunities (26 lines), per Q1-8 A; and controllers.rs:372-382,409-432 — fixture_prepared_inference, fixture_inference_output, fixture_inference_events (about 40 lines) and controllers.rs:221-237 — InferenceFault::fixture_* (17 lines), per Q1-7 A. Under Q1-7 B / Q1-8 B these stay #[cfg(test)] inside the library and the Dungeon tests that used them are deleted instead (see the classification).
    • crates/ghostlight-dungeon/src/app_session.rs:54-91 — the VerifiedPrincipalEvidence struct, accessors, and fixture (38 lines).
    • crates/ghostlight-dungeon/src/main.rs:7 — mod world;.
    • crates/ghostlight-dungeon/Cargo.toml:27 — the direct ghostlight-persona-projection dependency (controllers.rs:35 is the only importer and it moves).
  • Keeps and moves:

    • git mv crates/ghostlight-dungeon/src/world/{action,clock,consumer, controllers,cover,elaboration,journal,mailbox,patch,sdk_inference, tool_schema,vault}.rs crates/ghostlight/src/ and git mv .../world/mod.rs crates/ghostlight/src/lib.rs. Verify with git diff --cached -M --stat that all 13 show as renames.
    • Inside the moved files: crate::world:: → crate:: (199 sites; super::super:: paths need no change); pub(super) → pub(crate) in lib.rs only (94 sites; submodules keep theirs); crate::app_session::VerifiedPrincipalEvidence → crate::VerifiedPrincipalEvidence (mailbox.rs:10, controllers.rs:10714,10790,10988,11473).
    • vault.rs and sdk_inference.rs move with the rest (Q1-2). Their fixture writes (sdk_inference.rs:1900,1983) resolve from crates/ghostlight at the same depth.
    • build.rs, tests/build_provenance.rs, runtime.rs:375 (web/dist) stay in Dungeon; CARGO_MANIFEST_DIR/../.. still resolves.
    • No test moves between crates. Every test under world:: was already a library test and moves with its file (420); every Dungeon test stays in Dungeon (49). See the classification.
  • Adds:

    • Cargo.toml (workspace): member crates/ghostlight.
    • crates/ghostlight/Cargo.toml: package ghostlight; no [features]; dependencies async-trait, chrono, codex-connector, cultcache-rs, ghostlight-persona-projection = { path = "../ghostlight-persona-projection" }, rmp-serde, serde, serde_bytes = "0.11", serde_json, sha2, thiserror, tokio, tracing, uuid, zeroize (all .workspace = true except serde_bytes, which the workspace does not declare); dev tempfile = "3". Every one is already in Cargo.lock; no new package.
    • crates/ghostlight-dungeon/Cargo.toml: ghostlight = { path = "../ghostlight" } in [dependencies]; nothing in dev-dependencies.
    • Cargo.lock: the new [[package]] name = "ghostlight" entry. Commit it; Idunn builds --locked.
    • crates/ghostlight/src/lib.rs, root: pub struct VerifiedPrincipalEvidence { account_subject_hash: String, valid_until: DateTime<Utc> } with pub fn new, pub fn account_subject_hash, pub fn valid_until, and #[cfg(test)] fn fixture delegating to new (Q1-3). Doc comment: the consumer verifies; the library bounds by valid_until at mailbox.rs:231.
    • crates/ghostlight/src/lib.rs, the public boundary, one ungated list. pub use clock::{FictionalMinutes, TickMinutes}; consumer::{CONSUMER_BODY_LIMIT, CONSUMER_CREDENTIALS_ENVIRONMENT, CONSUMER_PATCH_SCHEMA, CONSUMER_RECEIPT_SCHEMA, ConsumerRegistry, admit_document, encode_receipt}; controllers::{CellRun, ConnectorBinding, ControllerError, ControllerModels, ControllerPendingReason, ControllerRunner, ControllerWork, ControllerWorkCustody, ControllerWorkLookup, ControllerWorkStore, ControllerWorkStoreError, ControllerWorkWrite, InferenceEvent, InferenceFault, InferenceOutput, InferencePort, InferencePurpose, InferenceRequest, NarrativeRun, OperationalRun, PreparedInference, SubmissionDisposition, ToolResultOracle, open_controller_work, open_inference} (plus TracingInferencePort under Q1-7 A); cover::{AgencyGraph, Cell, Cover, CoverBudget, TickIndex, derive_cover}; elaboration::{SeedOutcome, select_row}; mailbox::{ConsumerPort, ControllerPort, MailboxError, SeedPort, WorldMailbox}; patch::{JurisdictionKey, Statement}; sdk_inference::{DEFAULT_SDK_MODEL_PREFIX, SdkBinding}; vault::VaultEvidenceSource. Root items made pub: STATE_SCHEMA, state_schema_compatibility_tag, AffordanceId, CommandId, SubjectId (the opaque_uuid! macro at mod.rs:98-106 emits pub(crate) struct; give it a visibility parameter and emit pub for these three only — the issue() impls stay private and #[cfg(test)]), PrincipalId, WorldPhase, SubjectKind, ControllerMode, CreateWorldIntent, CreateJurisdictionIntent, DecisionOpportunity, DecisionInvocation, PrincipalCommandIntent, CommandBody, WorldSnapshot, OperatorEvent, CommitReceipt, SubmitReceipt, KernelError, ScaleDeficitRow, and the snapshot row types Dungeon reads through WorldSnapshot fields (SubjectSnapshot, PlaceSnapshot, AffordanceSnapshot, and whichever others the compiler names; probe 3 stopped at the WorldSnapshot field layer). GroupedRun (controllers.rs) is reached through ControllerRunner return values and needs pub with its five fields.
    • How the ports stay public without the fixture constructors becoming reachable: InferencePort (controllers.rs:259) and ControllerWorkStore (:1541) are traits whose method signatures name InferenceRequest, PreparedInference, InferenceOutput, InferenceFault, ToolResultOracle, ControllerWork, ControllerWorkLookup, ControllerWorkWrite, ControllerWorkCustody, ControllerWorkStoreError. Those types become pub; their fields stay private except where listed below. Nothing about them reaches an ID issue() or ScopeDigest::fixture, which are private free functions and #[cfg(test)] impls (mod.rs:200-226,448) with no pub path of any kind and no cargo feature — reachable only from inside the library's own test build. The fixture_* functions are deleted (Q1-7 A / Q1-8 A) or stay #[cfg(test)] (B), and the ten doc-tests pin both outcomes.
    • Fields that become pub (38, from probe 3): CommitReceipt.{command_id, commit_digest, resulting_revision, resulting_state_digest}, Constituent.subject, Cover.{cells, oversubscribed, tick}, CoverBudget.{cells, constituent_cap, urgency_slots}, DecisionOpportunity.{controller_mode, world_id}, GroupedRun.{cell, needs, pending, resolution, submissions}, OperatorEvent.{revision, speech}, PreparedInference.purpose, ScaleDeficitRow.{deficit, jurisdiction, kind, target}, WorldSnapshot.{affordances, draft_approvals, now, opportunities, owner, phase, places, required_approvers, revision, scale_deficit, subjects, title, world_id}. FictionalMinutes.0 stays pub(crate): under Q1-8 A the one Dungeon use (runtime.rs:3579) is replaced by snapshot.now. Fields of the row structs reached through WorldSnapshot follow in the next compiler round.
    • Methods that become pub (36, from probe 3, by definition site): WorldMailbox::{open, create, submit_principal, submit_clock, agency_graph, operator_log, snapshot} (mailbox.rs:93,105,226,355,448, 461,474), ConsumerPort::new (:552), SeedPort::new (:594), ElaborationPort::new (:660), ControllerPort::{new, snapshot, submit_controller} (:692,696,721), ControllerRunner::{open, elaborator, seeder, custody_probe, run_narrative, run_operational, run_cell} (controllers.rs:2691,2712,2728,2744,2748,2838,3057), the receipt/pending accessors at controllers.rs:2397,2413,2421,2453, 2457,2461,2465,2469,2515,2519,2523,2543,2591,2595 (persona_turn, re_lowering, into_parts, subject, bound_scope_digest, fresh_scope_digest, gap, mode, reason, persona_prose), sweep (elaboration.rs:607,1746), name (elaboration.rs:1511), Cover::{singletons, groups, validated} (cover.rs:163,170,198), ConsumerRegistry::{empty, from_secret_file} (consumer.rs:168,176), TickMinutes::{new, minutes} (clock.rs:45,51), CommandId::new (mod.rs:122), PrincipalId::new (mod.rs:239), Statement::new (patch.rs:281). Under Q1-7 A add PreparedInference::prepare (renamed from prepare_invocation, controllers.rs:388), InferenceOutput::new, InferenceFault::{retryable, recovery_required, integrity_violation}, InferenceRequest::provider_model (:120), and TracingInferencePort::new (:285).
    • crates/ghostlight/src/lib.rs crate docs: the ten compile_fail doc-tests from probe 5, each pinned to its error code.
    • crates/ghostlight-dungeon/src/runtime.rs (tests): a ~25-line copy of soul_no_credential_name_appears_in_the_ports_own_source scanning runtime.rs only, because the library test cannot read another crate's source (see per-file sdk_inference.rs:1824-1828).
    • crates/ghostlight-dungeon/src/app_session.rs (tests): one test asserting VerifiedPrincipalEvidence::new( occurs exactly once in the crate's production source, at account_for_cookie (same shape as the from_secret_file count at sdk_inference.rs:1860). This is the single-minter rule the deleted private fields used to enforce (Q1-3).
  • Cross-boundary test classification (Q1-4). Every Dungeon test that reaches an item which is #[cfg(test)] in the library today. Method: probe 5's four compile errors plus a read of each test body.

    # File:line Test Exercises Reaches Class
    1 runtime.rs:3270 the_tick_driver_never_exceeds_its_controller_permit_pool run_cover_tick and state.controller_permits: two singleton cells, pool of one, no overlapping infer fixture_prepared_inference, fixture_inference_output (via CountingInferencePort, :3241); AlwaysFreshWorkStore (:3194) uses only pub enum variants Dungeon runtime
    2 runtime.rs:3337 quarantine_raised_mid_tick_stops_the_sibling_cell_and_every_later_tick the driver's controller_quarantined flag across sibling cell and next tick fixture_prepared_inference, InferenceFault::fixture_integrity_violation (QuarantiningInferencePort, :3308) Dungeon runtime
    3 runtime.rs:3500 a_second_mid_turn_change_reaches_the_drivers_interrupted_arm the driver's Interrupted arm reached through run_cover_tick, commits through ControllerPort::submit_controller (production) fixture_prepared_inference, fixture_inference_output, fixture_inference_events, InferenceFault::fixture_recovery_required, InferenceEvent::ToolCall (pub variant), PreparedInference.purpose (InterruptingCoverPort, :3432) Dungeon runtime
    4 runtime.rs:3567 drive_one_tick_runs_every_cell_before_the_clock_and_all_share_one_tick drive_one_tick ordering: every cell before the clock, all cells carry the cover's tick fixture_controller_opportunities (the only caller of the sealed WorldId/SubjectId/ControllerId::issue() and ScopeDigest::fixture from outside world/), FictionalMinutes(u64), AgencyGraph::default, Cell, TickIndex, CommitReceipt literal (controller_commit, :2492) Dungeon runtime
    5 runtime.rs:3697 (ignored) live_smoke_seeds_then_ticks_a_world_against_the_connector the road test: seed from a real Vault, activate, tick against a real connector; GHOSTLIGHT_SMOKE_TRACE writes the whole membrane (notes/local-live-smoke.md:214-246) TracingInferencePort (fixture_with, :2581-2582) Dungeon runtime

    Counts: 5 tests reach test-only library items; 0 are kernel tests; 5 are Dungeon runtime tests. Two other Dungeon tests touch the boundary but need nothing test-only: runtime.rs:2502 no_proposal_projection_carries_the_canonical_world_commit builds a CommitReceipt literal (four pub fields, in the boundary list) and runtime.rs:4293 soul_verified_evidence_outlives_the_session_that_minted_it uses VerifiedPrincipalEvidence::fixture → ::new (Q1-3). Every other Dungeon test (42) uses production ingress only.

    Consequence: the ruling's "move the tests into the library" has no test to move. Tests 1–4 exercise run_cover_tick, drive_one_tick, the permit pool, and quarantine — Dungeon's tick driver — and test 5 is Dungeon's road test; moving any of them would move the driver. What they need divides into two kinds, and each is an operator question below rather than a mechanism this map invents:

    • tests 1, 2, 3, 5 need to construct port values (PreparedInference, InferenceOutput, InferenceFault) and a port decorator; none of these is a sealed item under invariant 1 (mutable state, ID allocator, reducer, journal, authenticated-caller constructor) — Q1-7;
    • test 4 needs a Cover to walk, and today gets it from fabricated opportunities minted with sealed IDs — Q1-8.

    Expected post-move counts, under Q1-7 A and Q1-8 A: library 420 unit (the base world:: list, prefix stripped) + 10 doc; Dungeon bin 49 (the same 49 names; tests 1–4 rewritten in place, test 5 unchanged); build_provenance 1; persona-projection 13; ignored 1 + 1. Under Q1-7 B tests 1–3 and the trace half of test 5 are deleted (Dungeon 46, or 45 if test 4 goes under Q1-8 B); the library count is unchanged either way.

  • Per-file changes (against d83534c = 02bbf62 source):

    • Cargo.toml:3-5: add member.
    • crates/ghostlight-dungeon/Cargo.toml:27: replace with the library path dep.
    • crates/ghostlight-dungeon/src/main.rs:7: delete.
    • crates/ghostlight-dungeon/src/runtime.rs:3-22: split the use crate::{} block; world::{…} becomes use ghostlight::{…} (probe edit); :4: import VerifiedPrincipalEvidence from ghostlight, not app_session. :518,519,535,1084,1088,1092,1201,1325,1621,2492,2493,2581,2582,3081, 3501,3568,3658,3666,3675,3677,3777,3782,4267: crate::world:: → ghostlight::. :4313: ::fixture( → ::new(. :3081-3086: drop the three fixture_inference_* names. :3243,3310,3434: PreparedInference::prepare("ghostlight-controller-test", 4_102_444_800_000, request) (Q1-7 A; the two literals are the ones fixture_prepared_inference hard-codes at controllers.rs:381). :3255,3442,3447: InferenceOutput::new(vec![InferenceEvent::Text(..)], "sha256:<receipt>"); :3457: InferenceOutput::new(events, ..); :3318: InferenceFault::integrity_violation(..); :3479: InferenceFault::recovery_required(..). :3567-3644 (Q1-8 A): replace fixture_controller_opportunities + FictionalMinutes(..) with fixture().await, active_two_cell_world, snapshot(), and agency_graph(), then derive_cover(snapshot.world_id, snapshot.now, CLOCK_TICK_MINUTES, &snapshot.opportunities, &graph, budget); the cell count assertion becomes 2 (a genesis world has two controller-bearing subjects — :3089-3095 already says this is the most world.create can produce); the ordering and shared-tick assertions are unchanged. :504,2673 doc comments name world::consumer; reword to the crate.
    • crates/ghostlight-dungeon/src/eve.rs:3-6 and :182,335,338,354,386, 405,449,494: same rewrite.
    • crates/ghostlight-dungeon/src/mesh.rs:6-9: same.
    • crates/ghostlight-dungeon/src/idunn_health.rs:8: same.
    • crates/ghostlight-dungeon/src/app_session.rs:54-91: delete; :6: add use ghostlight::VerifiedPrincipalEvidence;; :232-235: struct literal → VerifiedPrincipalEvidence::new(session.account_subject_hash.clone(), session.access_expires_at).
    • crates/ghostlight/src/controllers.rs:221-237,372-382,409-432 (Q1-7 A): delete the fixtures; :388 prepare_invocation → pub fn PreparedInference::prepare (keep the body; the two in-library callers at the connector and SDK ports follow); :150 add pub fn new(events, receipt_digest); :192,206 new → pub fn retryable, integrity_violation → pub, add pub fn recovery_required(detail) (the RecoveryRequired disposition has a private new today; make it the named constructor); :277-370 drop #[cfg(test)] from TracingInferencePort and make it and new pub.
    • crates/ghostlight/src/sdk_inference.rs:1824-1828: the scanned file list becomes ["sdk_inference.rs", "controllers.rs"] (paths are now crate-root relative); :1860 sources[1] still indexes controllers.rs. The runtime.rs half moves to Dungeon (Adds).
    • deployment/idunn/recipe.toml:14-17: test-world-owner becomes two steps — test-world-library: ["cargo","test","--locked","-p","ghostlight"] (runs the 420 unit tests and the 10 doc-tests) and test-dungeon: the existing -p ghostlight-dungeon --bin ghostlight-dungeon line. :51-53: acceptance argv becomes -p ghostlight --lib controllers::tests::real_codex_connector_cognition_modes_commit_speech (the world:: prefix is gone; --exact still matches).
    • package.json:9 (dungeon:run): unchanged; :7-8 use --workspace and pick the crate up.
    • notes/local-live-smoke.md:64: unchanged; the ignored smoke stays in runtime.rs:3697.
    • Docs that name the old path (owner in brackets): README.md:165-166 [Hands] add the library line; docs/public-architecture/index.md:46 [Hands]; docs/architecture/ghostlight-session-zero.md:21-23 [Hands] rewrite "Source does not yet match" as the current rule; docs/architecture/ghostlight-world-ontology.md:939-941 [Hands] the build-budget sentence now names crates/ghostlight; notes/ghostlight-current-system-map.md:18,28,289 [Mind steward]; notes/fresh-workspace-handoff.md:284-286 [steward]; state/map.yaml:57,389-390 [steward].
  • Authority map:

    • Owner: the library crate ghostlight owns the world kernel, its ontology, reducer, journal, mailbox, controllers, elaboration, cover, clock, consumer admission, inference ports (connector, SDK sidecar, and the tracing decorator), and the markdown evidence source. Its public surface is the list under Adds and nothing else.
    • Inputs: VerifiedPrincipalEvidence values Dungeon constructs from AppSessionOwner::account_for_cookie; CreateWorldIntent, PrincipalCommandIntent, DecisionInvocation; consumer patch bytes via admit_document; store paths via WorldMailbox::open and open_controller_work; connector and SDK bindings via open_inference; a consumer-implemented InferencePort/ControllerWorkStore when the consumer supplies its own (Dungeon's tests do; production uses the library's).
    • Outputs: immutable WorldSnapshot, OperatorEvent, AgencyGraph, typed receipts (SubmitReceipt, CommitReceipt, CreationReceipt through MailboxError/KernelError), controller runs, cover partitions, encoded consumer receipts.
    • Derived state: Dungeon's Eve projection, mesh documents, Idunn health (which reads STATE_SCHEMA and state_schema_compatibility_tag and derives, never restates), HTTP responses.
    • Forbidden writers: any path in Dungeon naming WorldKernel, WorldState, reduce, apply_effect, CommandEnvelope, AuthenticatedCaller, the journal, an issue(), or ScopeDigest::fixture — enforced by visibility with no feature that can lift it, proven by the doc-tests under --all-features. Any second minter of VerifiedPrincipalEvidence — enforced by the Dungeon count test. Any fixture-shaped DecisionOpportunity — there is no constructor for one outside the reducer.
    • Shared paths: Dungeon production and Dungeon tests reach the kernel through the same pub items; a Dungeon test port constructs port values through the same constructors the library's own ports use (Q1-7 A). Library-internal tests keep pub(crate)/super:: access as today. Consumers other than Dungeon: none yet; Epiphany consumes only ghostlight-persona-projection, which is unchanged (Q1-5).
    • Deletion line: mod world;, the nine unused re-exports, the #[cfg(test)] re-export block, the fixture_* functions (A) and Dungeon's private principal struct are gone before the first pub is written.
  • Verification (Windows workstation; proves Windows only):

    • builds: cargo check -p ghostlight (no new warnings; the 646 dead-code warnings of an under-opened boundary must be 0), cargo check -p ghostlight-dungeon --bin ghostlight-dungeon, cargo check -p ghostlight-dungeon --tests, cargo build --locked -p ghostlight-dungeon --bin ghostlight-dungeon (proves Cargo.lock). Expected target-dir delta: one extra lib crate in debug; the scratch probe's check-only target was 2.3 GiB from empty, so expect well under 1 GiB incremental on the repo's 7.5 GiB.
    • tests: cargo test -p ghostlight = 420 unit + 10 doc (the 420 names equal the base world:: list with the prefix stripped); cargo test -p ghostlight-dungeon --bin ghostlight-dungeon = 49 (same names as base); cargo test -p ghostlight-dungeon --test build_provenance = 1; cargo test -p ghostlight-persona-projection = 13. Ignored counts unchanged (1 + 1). The reopen test from Cut 0 pins invariant 2 (world.cc written at base reopens with equal revision and digests). The doc-tests pin invariant 1. The VerifiedPrincipalEvidence::new count pins the single minter. The split credential-name test pins "no credential name in the port's source" on both sides. Tests 1–4 keep their names and assertions (test 4's cell count is 2).
    • negative: cargo test -p ghostlight --all-features --doc = 10 passed (same as without; the command is the standing check that no later feature reopens the seal); rg -n '\[features\]' crates/ghostlight/Cargo.toml = 0; rg -n 'cfg\(any\(test' crates/ghostlight/src = 0 (no feature-gated test path exists); rg -n 'fixture_' crates/ghostlight-dungeon/src = 0 outside the Heimdall ::fixture( sites (app_session.rs:560, runtime.rs:2552, 2611,4126 — use rg -n 'fixture_[a-z]' … which they do not match); rg -n 'crate::world::' crates = 0; rg -n '^mod world;' crates = 0; rg -n 'app_session|heimdall|eve::|idunn_health|axum|tower_http|jsonwebtoken|\bDungeon\b|Session Zero' crates/ghostlight/src = 0 (collisions checked: Dungeons in vault.rs is excluded by \b; do not add player); rg -n 'pub\(super\)' crates/ghostlight/src/lib.rs = 0; rg -n 'VerifiedPrincipalEvidence::new\(' crates/ghostlight-dungeon/src = 1 outside #[cfg(test)]; cargo tree -p ghostlight names no axum, tower-http, jsonwebtoken, cultnet-rs, cultmesh-rs, reqwest, aes-gcm, hmac, rand, base64, libc; git diff -M --stat 02bbf62..HEAD -- crates shows 13 renames.
    • operator: Idunn recipe run on Yggdrasil (Linux proof): the test-world-library, test-dungeon, build-daemon steps and the acceptance step resolve; /srv/…/ghostlight-dungeon --state-root reopens the live world.cc; health reports consumer-v4. One GHOSTLIGHT_SMOKE_TRACE road run after the cut shows the membrane trace still writes (test 5).
  • Operator questions:

    • Q1-7 Port value constructors for consumer-implemented ports. Tests 1, 2, 3 and 5 implement InferencePort outside the library and must return PreparedInference, InferenceOutput, InferenceFault; today they do it through #[cfg(test)] fixture_* wrappers that exist only because those types have private constructors. A: make the constructors the port contract — PreparedInference::prepare(caller_ runtime_id, expires_at_unix_ms, request) (rename of the existing prepare_invocation, "the one place a PreparedInference is built", which every in-library port already calls), InferenceOutput::new(events, receipt_digest), InferenceFault::{retryable, recovery_required, integrity_violation}, InferenceRequest::provider_model; delete the five fixture_* items; make TracingInferencePort an ungated pub decorator (it wraps any port and constructs nothing sealed; "production never constructs it" becomes a Dungeon rule, which is where it was enforced anyway — only the ignored smoke names it). B: keep every fixture #[cfg(test)] in the library and delete tests 1–3 and the trace field of LiveController (runtime.rs:2538,3726) from Dungeon; the permit-pool, quarantine, and interrupted-arm invariants of the tick driver lose their proofs and the road test loses its membrane trace. Recommended: A. A public trait whose methods must return a type nobody outside the crate can construct is not a port; it is a trait only the library can implement. Invariant 1 seals the kernel, not the inference transport, and none of these constructors mints an ID, mutates state, reduces, or writes the journal. A is a net deletion (about 85 fixture lines out, about 25 constructor lines in, of which prepare is a rename).
    • Q1-8 The cover for drive_one_tick_runs_every_cell_before_the_clock_and_all_share_one_tick. A: derive it from a real world through production ingress — the module's own fixture()/active_two_cell_world, then snapshot() and agency_graph() into derive_cover; delete fixture_controller_opportunities and the FictionalMinutes(u64) construction; the test asserts two cells instead of three, and the ordering and shared-tick assertions are unchanged. B: delete the test. C: keep fixture_controller_opportunities #[cfg(test)] in the library and move the test there — not available, drive_one_tick is Dungeon. Recommended: A. It removes the last caller of the sealed constructors from outside world/, uses only what tests 1–3 already use, and the test's own doc (:3558-3565) is about ordering, not about three.

Cut 2. Build and deploy references in gamecult-ops

  • Repo/branch: gamecult-ops, current default branch; one commit after Cut 1 is pushed.
  • First: git -C F:\Projects\gamecult-ops status clean; bash scripts/test-ghostlight-yggdrasil-wiring.sh passes at base.
  • Deletes first: none (Q1-6: retirement belongs to the deployment gate).
  • Keeps and moves: none.
  • Adds: none.
  • Per-file changes:
    • runbooks/ghostlight-dungeon-yggdrasil.md:249-251: insert cargo test --locked -p ghostlight before the Dungeon test line; :257 "one explicit Rust binary" stays true.
    • scripts/deploy-ghostlight-yggdrasil.sh:964-970: add the library test invocation in the same container run; :1308: acceptance becomes -p ghostlight --lib controllers::tests::real_codex_connector_cognition_modes_commit_speech.
    • scripts/test-ghostlight-yggdrasil-wiring.sh:276-278: add require_text "${deploy}" 'cargo test --locked -p ghostlight'.
    • idunn/yggdrasil/bindings/ghostlight.toml.in: unchanged; it names the recipe path, and the recipe carries the commands.
    • The scripts remain, per the Ghostlight handoff (:389-391), helpers not to invoke: they fail at the root/Idunn Git boundary and hold duplicate deployment authority. Editing them keeps their command list from lying about the crate graph until the deployment gate retires them; it is not an endorsement to run them.
  • Authority map:
    • Owner: deployment/idunn/recipe.toml in Ghostlight owns the build and test commands (binding :11 recipe_path). The runbook describes; the scripts are a second, dormant list pinned to the recipe's strings by the wiring test until the deployment gate removes them.
    • Inputs: the admitted ref and the recipe.
    • Outputs: the same one binary plus web/.
    • Derived state: runbook text, wiring-test assertions.
    • Forbidden writers: no second list of cargo commands that drifts from the recipe; the wiring test's require_text lines are that check.
    • Shared paths: n/a.
    • Deletion line: none in this cut.
  • Verification:
    • builds: none (shell and Markdown).
    • tests: bash scripts/test-ghostlight-yggdrasil-wiring.sh passes with the new require_text.
    • negative: rg -n 'ghostlight-dungeon --bin ghostlight-dungeon' F:\Projects\gamecult-ops --glob '!docs/**' lists only lines that also sit beside a -p ghostlight line.
    • operator: the next Idunn deploy of a post-Cut-1 commit runs the library step.
  • Operator questions: none; Q2-1 was Q1-6 and is ruled.

Subtraction ledger (estimate)

Cut Removed Added Deps / formats / targets
0 — — scratch captures only
1 mod world; (1), 9 unused re-exports (~3 lines), cfg(test) re-export block (12), Dungeon principal struct (38), Dungeon's direct persona-projection dep (1), fixture_controller_opportunities (26, Q1-8 A), inference fixtures (~60, Q1-7 A); ~140 lines lib manifest (~28), workspace member (1), Dungeon dep (1), lock entry (~20), principal type in lib (~30), pub use boundary (~25), doc-tests (~30), runtime credential test (~25), minter count test (~15), port constructors (~25, Q1-7 A), recipe step (~6); ~205 lines. Visibility edits: 94 pub(super)→pub(crate), ~150 pub(crate)→pub (items, fields, methods), 199 crate::world::→crate::, 32 crate::world::→ghostlight::; test 4 rewritten in place (~20 lines) +1 lib crate, 0 features, 0 binaries, 0 new packages in Cargo.lock; 13 files renamed (~52k lines)
2 — ~5 lines 0

Net for L0: about +65 source lines, one crate, no feature. The growth is boundary declaration (an implicit pub(crate) becomes an explicit pub list plus its compile-fail proofs); the liability it retires is the convention "Dungeon does not touch kernel internals", which becomes a compile error with no feature that can lift it, and five fixture wrappers that existed only to work around private port constructors.

Actual, reconciled 2026-09-16 against 6ddabca (Ghostlight) and 647e57e (gamecult-ops):

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

The miss is explained, not hidden. The single-minter scanner grew about 110 lines chasing Soul's evasions and remains evadable (S4). The boundary needed roughly 90 more public items than this map enumerated, each forced by a compile error or a private_interfaces warning. The integration tests buy ruling Q1-9, which postdates the estimate. Features and binaries added: none, as estimated.

Findings not assigned to a cut

  • deployment/idunn/recipe.toml:142-146,169-171 declare the world slot and provided capability as ghostlight.world_state.foundation.v0 / foundation-v0, and :150-154 the controller-work slot as ghostlight.controller_work.v3; source is consumer.v4 (mod.rs:84-85, tag consumer-v4) and controller_work.v15 (controllers.rs:75-76). Pre-existing drift; L0 changes no schema string, so it stays.
  • tests/build_provenance.rs is never run by the recipe or the scripts: --bin ghostlight-dungeon selects the bin's unit tests only. A cargo test --locked -p ghostlight-dungeon --test build_provenance step would be a recipe change with its own reason.
  • notes/ghostlight-current-system-map.md:18-19 still names the state owner as ghostlight.world_state.foundation.v0; :28 says "the private world::WorldKernel". Both are stale before this cut; the steward owns the rewrite.
  • runtime.rs:2492 is a CommitReceipt literal in Dungeon tests; it is why CommitReceipt's four fields are in the boundary at all. Under Q1-8 A the same literal serves drive_one_tick's fake clock submitter.
  • PreparedInference.invocation (controllers.rs:131, CodexTransportInvocation from codex-connector) stays pub(crate). A consumer port that talks to a real transport would need it; no such consumer exists in L0, and opening it is a port-contract decision for the pass that brings one.
  • web/ references nothing in crates/; sidecar/claude-sdk/test/* is written by library tests under GHOSTLIGHT_WRITE_SIDECAR_FIXTURES and read otherwise — path depth unchanged, nothing to do.