Archive — history, not state. Kept for its reasoning and its evidence; its plan is closed.

Roadmap — from the green base to what the docs describe

docs/ says how the system should work. This says what is between here and there, in dependency order, and why each step unblocks the next. plans/worklist.md is the row-level ledger. Rulings arrive in conversation and land here or in plans/edicts.md; see plans/method.md. References to review-queue Dn below point at plans/archive/review-queue-2026-07-25.md, kept for reasoning only.

What 9466791a was actually about, stated properly because the framing matters: the target-final artifact chain and the image runtime. Content-addressed objects with derivation keys separated from content hashes (mcode@3, executable manifest, target realization, cart@2, bundle@2), and code that is mapped rather than parsed. Splitting engine_lite is a consequence, not the point.


THE PREMISE ARC — ruled by John 2026-07-27, the successor sequence

“No earnest compiler rework until the basic premise is done” — engine_lite shrunk because actor construction is uniform and pulled into modules; actors execute bytecode from mapped-in stone pools; and all the deletions that make the transition real. Until then the mcode/mach rework stands on shaky ground. (The in-flight slot-limit agent still delivers its diagnostic + rework PROPOSAL — the proposal waits here for its ruling; hpack keeps waiting; model-fit items that ARE the premise — the import op — land inside it.)

The sequence, in dependency order (each row = roughly one agent landing; the spine is serial because every row touches the start path):

  1. pit.executable@1 dies — the consumer conversion (cement’s ~15 reads + EXECRECS, cart.cm, cart.ce, bundle.ce, development_shop, shop_realize, the start door, six tests); target payloads move into the realization. DONE — see the landing record in plans/next-arc.md. (Row 1 also read “[queued next]” and row 830 read “future work” long after it landed; corrected 2026-07-29.)
  2. D1 — realization@1 + $image — the realization as a distinct stored artifact; $image built (open/pin/bind/entry, boot holds it, C mechanics / Pit policy); the start path follows realization rows through $image for pool-backed executables.
  3. D2 — the import lane — the import op emitted (model-fit compiler work, in scope), IMPORT rows bound through the realization’s actor-owned result arrays, the realize lane groups units into pools (minimal link plan first: one pool per executable).
  4. D3 — the executable-as-array — 4a (the endowment-module resolution lane) and 4b (THE ARRAY) are DONE: the plan format is one ordered ENTRIES array plus the NAMES each entry binds, three kinds and no more (UNIT / NATIVE / VALUE). The separate bindings table and its three fulfilment CLASSES are gone, provides folded into NAMES, and neither the C walker nor cart.cm holds a list of engine-owned names — see the 4b record in plans/next-arc.md. What REMAINS of item 4: the start walk still lives in C (moving it to Pit is phase E’s boot/clerk split), and engine_lite still holds its residual floor.
  5. The fleet runs from pools — the dev mount lowers mcode windows through the fragment+linker lane instead of pmac+materialize; the seeded fleet executes from mapped stone. 5a is IN and the switch is OFF (PIT_POOL_PAYLOAD=1): the lowering, the linking, the C resident entrance and the start walk’s pool arm are all built, one pool per unit; tests/image_lanes.ce is the new gate and it caught a real defect (the closure-frame union arm — five reads took the wrong arm, so an image function’s lexical link read the realization-state array and the whole fleet failed to start). 5b is what remains: with the lever on, a started actor dies not a function in the C start walk while the same pools run correctly when entered from Pit — see the landing record in plans/next-arc.md. One pool per EXECUTABLE (the minimal link plan proper) waits behind 5b, because it needs the realization’s images/instances rows and a plan section naming (image, entry).
  6. The coupled deletionsNOT one landing; 5-6. See plans/stone-6-7-map.md, which supersedes this row. Two corrections that matter: (a) there is no C single-unit pool assemblerpmp1 is written only by Pit — and mach_materialize_cpool cannot die until one exists, because it dies with pit_load_mach_code, which also serves the pmcd lane that is every window of root.cart. That prerequisite (6.0) is the true head of the chain and was unrecorded. (b) pmac is not a “transitional reader” but the live payload form — 113 windows in boot.cart, 125 call sites, and a permanent cross-target producer. ct_lock reaching zero also needs pit_rt_ct_alloc for PitCode/PitFunction to go, which this row omits and the memory-model section states correctly — the two passages disagreed.
  7. E — minimal C bootalso 5-6 landings; see plans/stone-6-7-map.md. start_plan.c is 803 lines (this row said 870, next-arc.md has said 743 and 757 — 5a grew it). The honest target is ~150 lines for the cart-mount + header + enter path and ~240-260 counting runtime creation and the boot wota, excluding host.c. The crossing constraint recorded in next-arc.md is half wrong: move engine_claims derivation into build_engine_record and C never reads ENTRIES, after which 4b’s section-kind recipe applies directly. The boot actor starts the clerk and is the root; shop_actor’s exclusion from the pool lane resolves by root.cart shrinking from 23 windows to 2, NOT by changing M1’s rule.

NOT gating the premise (follow or interleave freely): bundle@2, the cart@2/pcr2 producer, os decomposition + the internal/ guard, sync-file deletion, std::sysinfo, F’s residue (budgets actually read; the pit_profile.h chain; provider-set discovery), per-object git fetch, the debugger ACTOR (its floor is in flight; the actor itself is wanted tooling and can land any time after $inspect merges).

Status — 2026-07-25

Phase A is COMPLETE. M1 has landed. Code executes from a read-only mapping, and a record’s shape is a row in that mapping — the same C type, not a copy.

phasestate
A — the image layerDONE. A1a/A1b/A1c, A2, A3a/A3b all landed
B — retire the ABI crossing (M1)root cart done (d39c19da). The seeded fleet in boot.qop is still mach, so a stamp bump is not yet free for the dev loop — see M5 below
C — the artifact chainobject store built; the vocabulary (mcode@3, manifest, realization, mach_pool_emit, bundle@2) is not
C2 — origin map + the debugger halfunbuilt. $pgo’s real form waits here too
D — the executable-as-arraythe import opcodes are inMACH_LOAD_MODULE_RESULT (144) and MACH_LOAD_BOUND_MODULE (145), fresh slots at the tail, handlers only. No emitter produces them, so no artifact contains them and PIT_MACH_ABI still reads mach-v2+fuse1: the stamp is owed when the emitter starts emitting, not when the handler learns to read. The array itself is next
E — minimal C boot + the boot/clerk splitafter D. Both rewrite how an actor comes into existence — sequence, do not parallelise
F — the remaining target leversindependent

Also landed this session, outside the phases: $shop dissolved into $clerk/$inspect/ $runtime with a token-forgery hole closed and proved closed; $mount deleted from the design; check-arms built and wired into check-all at 14/14; the immediate-ASCII lane made unreachable (steps 0-3 of 6).

Green at every commit: make check, make check-arms (14 arms), make check-cli, make lint, ./pit fuzz. Two gates are RED at baseline and are not ours: make check-web (a shop_actor stack overflow at depth 1024 under node) and make budget (stale recorded baselines). make check does NOT run meson testcheck-arms does.

What to do next, in order

  1. code_cacheDONE (ec15c99f). Bounded structurally rather than evicted; see the memory-model section below for what a hit actually buys and why eviction is impossible.
  2. Finish the immediate-ASCII deletionDONE (24e6fbdd). One text representation now; PIT_TAG_EMPTY_TEXT is the renamed sentinel and no bit pattern moved.
  3. Phase Dthe two import opcodes done; the executable-as-array is what remains.

The resolution arc — landing order (ruled 2026-07-26, edicts “The resolution model”)

HARD STOP after landing 8 — John, 2026-07-26. When all eight land, do not proceed into phases C, C2, D, E, or F, and do not start the parallel-safe items (check-web, the profile levers, the debug-contracts arm). John wants to discuss the remaining phases and confirm his own understanding of them before any of that work begins. The arc ends with a conversation, not a dispatch.

Each lands green and alone; agents touching pit-shop/ are serialized. The two-pass rewrite of use('core/…') sites (first to runtime::…, then to final homes) is accepted — two cheap mechanical passes beat one circular landing.

  1. DocsDONE (1add2adb). Thirteen pages state the target.

  2. :: and the chainsDONE (2d0dabc8). 546 sites rewritten, the /-alias interpretation deleted, the lists live in the recipe and fold into recipe_fingerprint. A transitional programs: ["core/internal"] list carries the CLI until 3 deletes it.

    The two-phase crossing recipe, now proven and canonical for the landings below: the committed seed carries a compiled resolver/fleet that predates the change, so a rewrite the fleet must compile cannot land in one pass. The crossing: revert sources, add one marked transitional ring, reseed; delete the scaffold, re-apply the rewrite, reseed again. The scaffold exists for exactly one reseed and is absent from the final tree — a crossing tool, not a compatibility arm. Landings 3 (cemented CLI locators), 6 (every symbol renames), and 7 should expect to need it. The stale untracked boot/boot.cart wedge pairs with it: move the cart aside, boot from boot.qop, re-cement.

  3. shop_toolsDONE (726b573e). 33 tools moved, bin: ["shop_tools"] live, programs list deleted, the script discriminator built (it lived in three places; now one predicate). The CLI’s starting package is still the terminal actor’s own (core), not the cwd’s — deliberate: orthogonal to the bin path, and changing start attribution has its own blast radius. resolve_locator’s new package_name_for_root is the primitive a cwd-package CLI needs when that is wanted.

  4. std_endowmentsDONE (a086077f). Five providers moved, rows deleted, the use-block live and narrated, endowments_whole declared. The symbol follows the KEY, not the path — so no crossing fired, and the manifest delta was file-column-only. One meson gate read a file path (PIT_HAVE_HOOK) and would have silently compiled the hook out; fixed. Pre-existing red found: tests/shop_resolve.ce workspace-member step fails at baseline, only under pit test all — chipped for a separate session.

  5. pitlib made realDONE. Six wrapper modules deleted, native/ gone, blob.c out of runtime.c, internal/time.c and random.cm in. pitlib is walked whole by MEMBER_MODULE_PACKAGES with keys taken package-relative, so its five natives rows and the hardcoded blob static-lead row are all discovery now. The manifest delta is file-column-only across all 12 targets by multiset (the one exception is blob, which changes LINE KIND from static|lead to extension| while keeping key and symbol). init became PIT_USE_BOOT(<fn>) in the C file, scanned beside PIT_USE_PROBE — the name PIT_USE_INIT was already taken by the hand-written-entry macro. No crossing fired: nothing but a stale realization cache + a stale dylib stood in the way, and make nuke cleared both. Two consequences worth knowing: a member package’s flat key needs a third symbol probe in add_native_provider (the wrapper’s old job, and landing 6 deletes it), and blob is now claims-trimmable like any other module rather than an always-static row.

  6. core dies, in two halves (fleet home RULED by John 2026-07-26): 6a — the moves. DONE (1efcc05f). Fleet in pit-shop, support in shoplib (five fleet-only modules in pit-shop instead, with importer evidence), engine its own package, internal/runtime.cm deleted as a subset of config.cm. No crossing: the seed producer reads the engine by file path and the old daemon boots from compiled content in boot.qop. Census: runtime:: remains only for os/sysinfo (landing 7) + four flat-key stragglers 6b retires. OPEN for John with 6b: moving terminal.ce moved the CLI’s starting package to pit-shop, so pit-shop’s alias table now partly encodes the CLI’s reach — decide whether that reach belongs in the recipe’s shop-scope aliases instead. 6b — the symbol flag day. DONE (0b2c1edb). The authority is shoplib::symbols (cake cannot import pit-shop, so it moved rather than being shared); manifest field 4 is the FULL symbol as data and no consumer builds one; every compiled-in module is pit_<package>_<path>_use; dependenciesaliases, name/members dead; the workspace root is the runtime package (.pit/runtime, config.runtime_path, --runtime) and $core_path was DELETED rather than renamed — it only restated the ungated shoplib/shop_paths module. Three lists became one walk plus one build list (LINKED_C_PACKAGES, kept build-side deliberately — linkage is cake’s question, not the clerk’s), the eight pit-* protocol directories dropped their prefix (John’s ruling: the aliases were the natives-table disease in miniature), the 14 k==v aliases died, and bin gained cake. John confirmed: root package runtime, actor keyed std/actor. One correction proven against the failure: bare single-segment imports (use('fetch')) genuinely needed the k==v aliases — fixed by qualifying the import, not restoring them. The crossing fired on exactly one thingruntime::internal/os has no source file, so only the OLD static_target_hit (hardcoded to package core) could answer it. Two scaffolds bridged one reseed: a "dependencies": {"runtime": "core"} row in the nine packages that write runtime:: (the old reader reads dependencies, the new one reads aliases, so the new fleet never saw it), and 52 hand-added static|lead|<old-symbol>|<new-symbol> rows in boot/manifest/darwin.manifest. Both gone after the second reseed; make manifest dropped the rows. Two live bugs the rename exposed: engine_lite.cm loaded the couple module by the hardcoded key internal/couple, and a stale key silently disarmed $couple’s death pact — a loader cart then never exited. And cart.cm’s claims closure stripped a core/ prefix, which had been making the CART3 spec disagree with pitlib’s keys all along.

  7. Every location honestthe separable half DONE (d30abaca): std/actor.c real (John’s catch closed), wgpu a package, the audio_test arm proven FALSE and deleted (the file needed none of the backends the arm claimed), lane/lease as PIT_USE_LANE/PIT_USE_LEASE macros in 22 C files with the manifest fact-identical by multiset (which caught tvos’s uikit_window.c having silently lost its main lane). natives is down to its last two rows: os and sysinfo.

    The rest is blocked by findings, not effort, and merges into the post-stop agenda:

    • Two resolver rules make platform/posix unusable as a package name (multi-segment = canonical remote locator; the fallback parse takes the first segment — the exact text-guessing the package-identity invariant forbids). Reachable design: flat platform-* packages + locator-valued module arms; a 15-dir restructure + crossing.
    • Discovery cannot replace the catalogs until the subsets are ruled: darwin, linux and ios each take a different subset of platform/posix’s files — the catalog rows are a per-target capability policy in disguise. Walking posix whole would newly grant iOS $posix_spawn. Needs the package splits (apple, …) and per-target endowment lists; the recipe’s lists are shop-wide today.
    • The six console os providers are runtime C with hand-coded symbols because meson injects -DPIT_USE_NAME only for extension rows — moving them to extension rows is the prerequisite for any arm, and deletes the six branches as a consequence.
    • Windows compiles platform/posix/os.c (#ifdef _WIN32) — the directory name is already dishonest for one target.
    • pit-qbe/vendorsource/ + the LINKED_C_PACKAGES derivation: mapped, not started — is_native_file/is_support_source in pit-shop/package.cm:41-47 are the seam; the vendored tree carries main.c/tools//test/ that must go for “the folder is the unit” to hold; deriving the list needs the closure fed to cake, which cannot import pit-shop.
    • internal/ now holds 5 legitimate runtime fixtures + sysinfo.c, whose 7 cross-package importers are exactly what landing 8’s guard would refuse.
  8. The internal/ guard: RULED (b) — held until os/sysinfo move. John, 2026-07-26: “we have the mechanism to turn the guard on, so we can just do it when it’s ready.” The :: boundary makes the refusal statable and endowment_package_refusal is the exact template (~20 lines, fires on the candidate’s package); the turn-on rides with the platform restructure post-stop. No carve-outs, no exemptions — the rule arrives whole.

THE ARC IS CLOSED AND CLOSED OUT (05d91a9f): the file providers are package-private, watch_client is pit-shop’s, libgit is its own package, the docs are the diffable contract (22 pages, 14 contradictions fixed), and make budget is a REAL GATE again — re-baselined after the compiler campaign, with the one anomaly (record_lookup alloc_count +89.8%) flagged rather than baked in. Five gates green: check, check-arms 14/14, check-cli, lint, budget.

THE ARC IS CLOSED. Seven of eight landed; the eighth is a ruled deferral, not an omission. The post-stop conversation has begun — its agenda: the os decomposition ($environment, $clock, entropy, $dlfcn-as-actor, sync-file deletion, $platform), the flat platform-* restructure with nested-package-root resolution, the endowment-package splits by take-set (posix/apple/macos/ios) with per-target recipe lists, pit-qbe/vendorsource/ + LINKED_C_PACKAGES derivation, then the guard — and after all that, phases C, C2, D, E, F.

Docs landed (1add2adb). Of the ambiguities the docs pass surfaced, five are answered from the record: core does not survive as a package (ruled; $core_path dies with it or is renamed in landing 6); a bare internal/… path never continues down the chain — always the caller’s own, or a miss (anything else lets the chain defeat the privacy rule); pit testshop_tools::test with the ordinary .ce probe is the intended bin shape; the refusal of <pkg>::internal/x is a resolution-time rule on requests, so a cemented canonical identity in an artifact is a name, not a violation; the committed build manifests are rendered outputs of the one authority, not hand-maintained mapping tables, and stay for cold bootstrap.

Two need John: (1) the endowment lane/lease facts have no home in the three-field manifest — likely answer is a macro in the C file, the PIT_USE_PROBE precedent, but it is a design call; (2) endowments_whole — the earlier ruling wrote it as a second list, the docs pass demoted it to a per-entry mark to keep the four-list count; either works, one must be picked.

Proposed, pending John — the script-fallback discriminator: an argument that is locator-shaped (identifiers, /, ::, dots, dashes — no whitespace, parens, operators, or quotes) is resolution-only, and a miss reports the full chain; only non-locator-shaped text falls through to the script path; -e forces script always. Decidable from the text alone; pit sp gets “not found”, never a parse error.

The queue above is empty. What is actually next, in dependency order:

  1. The executable-as-array (phase D proper). The opcodes and the doc page are in; this is the work that deletes start_plan.c and dissolves both surviving engine-value tables.
  2. mcode@3 and the artifact vocabulary (phase C) — independent of D, and mach_pool_emit is blocked on it.
  3. The natives table (edict §5) — not blocked on anything. 24 of its 33 rows across both manifests are pure restatement of a path; the other 9 carry two facts that belong in package membership. Core is ruled: blob, fit, time, random, json, math.

Not next, and now known why: the key intern table. It is blocked behind constants living in the mapped stone pool — see the memory-model section.

Phase A — the image layer — COMPLETE

Kept for the reasoning, which the later phases still lean on. Every increment below landed; see the Status block above.

A1. Record shapes. John’s formulation, which is simpler than 9466791a’s and is what we build: a shape is just another stone object in the image. The pool holds a list of text, a list of shapes, a list of bytecode, and numeric constants — all stone. A record holds a pointer to a shape if it has one; the stone shape points at stone keys, so building a shaped record is a pointer store and reading one is an index. There is no “dynamic shape” kind — losing a shape is losing the pointer. Add or delete a key and the record stops being shaped and becomes an ordinary keyed record.

That deletes a distinction rather than porting one. 9466791a has two kinds, IMAGE and DYNAMIC, and its own comment marks DYNAMIC as “legacy decoded-Mach/native descriptors … transition-only until those execution lanes are gone.” Under John’s model those lanes are exactly what the image layer removes, so the kind tag has nothing left to discriminate and the kind field itself can go.

One thing genuinely blocks that and must move first. DYNAMIC also carries a mutable atomic_uint observation_token for development PGO — a lazily assigned process-local token. An immutable image shape cannot hold it, which is the real reason the two-kind split exists at all. So PGO’s per-shape token moves out of the shape and into the profile side, keyed by shape identity, where a mutable development-only observation belongs anyway. Do that before collapsing the kinds; it is small, and it is the load-bearing prerequisite.

Sequencing, since 94 call sites index the struct directly:

  • A1a — accessors over the existing layout, convert all 94 sites, struct untouched. Mechanical, zero semantic change.
  • A1bmove the PGO observation token off the shape DONE (f0022268), by deleting it. The field was named reserved in this tree, not observation_token. A shape’s own address is its identity — shapes are runtime-owned and immortal, and the PIC path already compared raw shape pointers. That removed the field, a global counter, and an O(n) walk of shape_cache under a lock; the walk was itself a dependency on the runtime shape table A1c deletes. PitRecordShape now has no mutable field at all, which is the precondition A1c needed.
  • A1c — shapes become stone objects in the pool; records point at them; the kind field and the runtime shape table both go. See review-queue D5 for why the table could not go earlier.
  • Watch record_capacity and the nan32 arm, where a record’s capacity is written from its shape count, and the _Static_assert(sizeof(PitRecord) == 24).

A2. Image function kind. A PIT_FUNC_KIND_IMAGE arm on PitFunction, plus the GC visit arms. Genuinely dormant: no value of that kind can be constructed until A3.

A3. The VM cursor. MachExecCode, a by-value tagged union, replaces PitCodeRegister * in the dispatch loop — 126 call sites where code->cpool[b] becomes an accessor.

  • A3a — register-only union, convert every site. No encoding change, no ABI bump. Run make budget: this is the hot loop, and a dispatch regression hides here.
  • A3b — add the image arm, Pit_NewImageFunction, and restore the three execution tests deferred from increment 1.

After A3b a pool executes from a read-only mapping — the thing that lets code run from ROM — with PIT_MACH_ABI still reading mach-v2+fuse1 and boot untouched.

This also unblocks the one thing increment 3 withheld: provider_entry’s construction step currently refuses, with the original call quoted at the site. Restoring it is deleting the refusal.


Phase B — retire the ABI crossing (M1)

Measured and proven; see worklist §M. The cart’s engine section carries mcode rather than a finalized pool, and C serializes it at mount in 7.5 ms. The same dev cart then boots on a v2 or v3 binary, and an ABI bump stops being an event.

Do this before the two import opcodes, not after. Every hour spent crossing the ABI without it is an hour spent re-deriving why four agents could not.

Standing constraint: the engine stays one translation unit. C can do 1 mcode → 1 blob; N mcode → 1 pool is Pit (mach_pool_emit.cm). A multi-unit engine could not be rebuilt by C, and porting that emitter into the audit surface is what minimal-C forbids. So the engine_lite split must remove code, never use() it.


Phase C — the artifact chain (the object store is already built)

Correcting my own framing: the object store exists and works. pit-shop/shop_store.cm already implements the nix-shaped model the docs describe — objects/blake2/<hash> where the name is the blake2 of the bytes, and catalog/<bare(key)> mapping names onto hashes, with the comment stating the split outright: “the catalog key is a compiler derivation identity, not the content hash.” Both directories are live in .pit/ today. Derivation keys naming computations, content hashes naming bytes: built.

What is missing is not the store, it is what goes in it. The artifact vocabulary:

  • pit.mcode.unit@3 plus the derivation catalog entries for it — ~893 lines of compiler.cm. Today’s unit is @2 with a separate cache wrapper.
  • executable manifest and target realization as distinct stored artifacts, replacing the pit.executable@1 mixed record that fuses logical identity with one way of running it.
  • mach_emit.lower_result and its companions — ~250 lines the pool emitter reads.
  • mach_pool_emit.cm — N units → one pool. Blocked on mcode@3: it rejects @2 at the door. Before touching it, make its failure findable — the operands must be numbers panic that defeated four agents is compiler-emitted, one shared block per function, and the optimizer inlines up to 20 calls, so the frame names whichever function the arithmetic was inlined into. Compile with inlining off, or write a Pit-level test with one hand-written @3 record.
  • bundle@2 — a store snapshot rather than a bag of executables.

cart_v2.cm is already a complete Pit codec, reader and writer, so carts are written in Pit today. Only cold-boot reading is C, and that C is deleted in phase E.

Cart formats are a Pit-only concern

The only genuine C dependency on a cart is where the engine and the boot entry are. So if every cart version keeps the fixed header stable — magic, profile/ABI stamp, engine offset, boot offset — then the same runtime boots v2, v3, and v4 alike, and a new cart format needs no C change at all. New boot code is loaded from the cart like everything else; the format’s evolution stays entirely in Pit.

That makes the fixed header the versioning boundary, not just the boot ABI, and it is a better reason for phase E than “less C” on its own.

Phase C2 — debug and profile translation

I omitted this and it belongs in the spine. Both halves scale the same way, and that scaling is the whole design problem.

What exists today. mach_compile_mcode_bin already returns a pit.mach.debug sidecar alongside the blob — {pc -> line, col} per function, plus the filename (pit-shop/shop_build.cm:656). So the 1 mcode → 1 mach case is solved: bytecode maps to a source position directly, because there is exactly one unit’s worth of provenance to record.

What does not exist. The moment several units share one pool, pc -> line is meaningless — a pc identifies a location in a pool, and which unit it came from is no longer implied. That needs the DWARF-shaped artifact: a sidecar that takes a pc anywhere in a pool and yields the mcode unit hash plus its function and site ID, from which source follows because mcode already carries its spans (it is JSON with positions in it). The docs call this the pool origin map (docs/spec/artifact-formats.md), and the implementation does not exist here — pit-shop/mach_origin.cm is 1,259 lines in 9466791a and absent on this branch.

Why it is not optional. Three things read it:

  • $runtime — a stopped frame becomes a source line only through this. Without it, merging units into one pool silently costs you the debugger, which is exactly the trade-off the link plan is supposed to let you choose.
  • PGO — instrumented images associate counters with sites in the final image, and the map normalizes those observations back to stable mcode function and site IDs so a profile survives recompilation. pit-compiler/pgo.cm already speaks in site IDs; it has nothing to resolve them against once units merge.
  • hot reload — matching functions across a rebuild uses the {mcode unit hash, function ID} pair, which is what the map keys on.

$runtime is not on this branch, and its absence makes a doc claim false

CORRECTION — I asserted the raw C capability was complete. It is not. Nine of those ten source/pit_internal.h:617-628 entries are declarations with no definition anywhere in the repo: pit_debugger_stack_depth, backtrace_fns, build_backtrace, closure_variables, local_variables, set_closure_variable, fn_info, fn_bytecode, val_address. A full-repo grep finds only the header lines. Only pit_debug_info has a body (source/runtime.c:10068), and it fills in a name plus "<native C>" for C functions — nothing for bytecode functions. internal/debug/debug.c, the supposed consumer, is seven stubs returning 0/{}/[] with zero callers.

What actually exists for introspection, and it is a different and better-shaped set: $vm — already a real, correctly gated endowment — gives memory() (VM-wide and per-actor), value_footprint(), and dumpmem(), a full object-by-object walk of another actor’s heap. use('actor') gives snapshot() (every actor: state, queue length, 60+ memory/GC/turn fields), heap_profile(id), vm_stats(), op_counts(), set_trace(), profile_set(), shutdown(). Pit_GetStack gives own-actor frames with file and line but no locals.

So the split is: memory and heap-walking are done and work. The debugger half does not exist — no named locals, no closures, no mailbox contents, no live bytecode, no breakpoints, no suspend/resume, no hot reload. The call/return trace_hook is wired throughout source/mach.c but nothing can install one. Any plan sized from my earlier sentence is wrong: consolidating what exists is days, making docs/architecture/debugging.md true is a new subsystem.

What is missing is the consolidation into one gated endowment. That is 9466791a’s internal/runtime_raw.c (685 lines) plus a five-line runtime_adapter.cm wrapping the raw provider into the public value — neither on this branch. Today the surface is reachable as an ordinary C module (internal/debug/debug.c), which means nothing gates it: no claim, no policy decision, nothing to deny.

So docs/architecture/debugging.md currently asserts something untrue —

“A development build has it; a shipped artifact does not, so its absence can be checked rather than trusted.”

An importable module hands full actor introspection to whatever imports it. The structural-absence property only exists once it is an endowment, because that is what makes it deniable and makes omitting it a build fact rather than a hope. $runtime is therefore not a convenience on this list — it is what makes an existing documented security claim true.

It pairs with the origin map and the two want doing together. The C functions give you frames, locals and bytecode; the origin map turns a bytecode position into {mcode unit, function, site} and thence a source line. Neither is a debug session alone. Seen from this side, the ordering constraint below is sharper: merge units into one pool without the map and $runtime still returns frames, they just stop resolving to source.

Do not assume the newer surface wins. 9466791a deleted parts of this while adding runtime_raw.cPit_GetStack, pit_trace_hook, pit_capture_disrupt_stack all go in that commit. Some of what the green base has is more complete. Diff the two surfaces deliberately before porting.

The dependency this creates: the origin map is a prerequisite for merged pools being usable, not just constructible. So it lands with mach_pool_emit, not after it — otherwise the first merged pool is a debugging dead end.

Phase D — the executable, and the engine that runs it

The design is settled (see the conversation record and docs/ — it is not yet written into docs/ as a page, which it should be).

An executable is an ordered array of entries plus the names they bind. Each entry produces a value into a slot, three ways and no more:

{value: <any>}          a constant — 4, "debug", or a runtime-injected actor token
{unit: image, entry}    run this bytecode
{native: key}           load this compiled-in provider

An endowment is a module result bound to a $name instead of a def name. Same instantiation, same result array, same slots. So:

  • delete the three engine-value name tablespartly done, and the premise was wrong. They were not three lists of the same names. plan_engine_values (C) existed for one purpose: to compare its ids against the ids cement.cm ENGINE_VALUES baked into the plan’s binding rows. Nothing else read an id — the walker already resolved a BIND_ENGINE row by name out of the engine’s record. So both id halves are deleted (the C table and its cross-check, ENGINE_VALUES’ numbers, and word 3 of the binding row, now reserved and zero).

    The other two are not duplicates and do not merge. cement.cm keeps a membership set — is this claimed name the engine’s? — which is a build-time question asked before any engine has run, and it is load-bearing: an engine value is legitimately null ($shop in an actor with no shop, $overling in the root), so a name the engine owns must not fall through to a raw-provider lookup that then refuses. engine_lite.cm’s engine_value() is the run-time half: what the name is. One fact, two sides of the build/run line, and a shared module cannot bridge them because the engine core is loaded as one raw blob and can use() nothing.

    Both sides dissolve under the array model rather than merging under this one. When an endowment is an ordinary entry in the executable’s array, “is this name the engine’s?” stops being a question anyone asks.

  • $delay, $clock, $couple, $contact, log, send become ordinary modules over raw primitives, exactly as std/file sits over $posix_file

  • start_plan.c (803 lines) dies — the walk moves to Pit, where “the runner is dumb” is expressible

  • engine_lite drops ~1,870 lines of standard library, which no actor pays for unless something claimed it. Unused means absent, and the claim is the liveness analysis — no stripping pass needed

Slot resolution is what makes shared modules work: a module’s bytecode names only things inside its own image, relatively. Cross-image references are binding rows the realization fills in. That is why this phase needs A3.


Phase E — minimal C boot

  • Cold boot shrinks to: map the cart, read a fixed header (magic, profile/ABI stamp, engine offset, boot offset), memcmp the stamp, load the engine, create actor 0, enter. ~100-150 lines.
  • cart_v2_boot.c never gets written here and start_plan.c goes with Phase D. That is 1,717 lines of C that the plan simply declines.
  • The fixed header becomes the entire C↔pit boot ABI.
  • A boot actor starts the clerk, asks it for the entry, starts that, and is the root. The clerk mounts — the cart, its own .pit store, and whatever else its mount modules reach. There is no $mount endowment; the gating is underneath, in what those modules claim. Same code in a dev build and a game build; only the cart’s entry and the clerk’s modules differ.

Phase F — the remaining target levers

The audit found one structural spine: source/pit_profile.h is an eight-arm per-console #if/#elif chain where each arm sets name, word width, object ABI, rep, region sizes and image cap indivisibly. Endian is now a lever; three axes are still missing:

AxisWhy
memory budgetevery arm exists partly to set three numbers; no way to say “2 MiB dynamic” without being the N64
thread-primitive providerdistinct from the scheduler axis: PIT_SINGLE_THREAD says “one drain”, not “which mutex links”. Six unrelated targets share one #if for four documented reasons
out-of-process daemon powerwhether the host may fork a daemon, symlink a socket, honour PIT_INPROCESS. Currently !defined(_WIN32)

Plus: profile name as a build-supplied string rather than a chain output, and the provider set discovered from linked providers rather than declared by hand in internal/policy.ce.

Target-varying constants use the existing module-arm mechanism. package.json already swaps modules per target (file_providerfile_darwin/file_playdate/…). A constant like the seed path is a module with per-target arms returning it — not a branch, not a startup fact, not a C edit. That is the generic answer and it retires review-queue D10.


Delete the natives table — a package’s files name themselves

John’s ruling, and it is larger than the row-deletion it started as. edicts.md §5 has the rule; this is the work.

The root package.json carries a natives array of 26 {"file": …, "key": …} rows, with more in pitlib, std and cart. It should not exist. A module’s name is <package>/<file> and an endowment’s name is its filename in a package the shop draws endowments from — so a row saying {"file": "internal/vm.c", "key": "endowment/vm"} is a hand-maintained restatement of something the filesystem already says. $vm’s name is a table entry today.

CORRECTION — I called this blocked on “what core’s public surface is”. It is not, and John said so. Once a package’s files name themselves, moving files around is trivial and does not touch the underlying system. The arithmetic, computed rather than estimated:

root tablepitlib
rows285
pure restatement (key == file − ".c")195
carries one fact90

So 24 of 33 rows say nothing a directory listing does not. pitlib’s table dies outright under one rule: native/<path>.c provides <path>.

The 9 remaining rows carry exactly two facts between them, and neither is a decision about core:

  • 5 endowment rowsinternal/vm.cendowment/vm. The fact is this file provides an endowment, and its home is the package: put it where the shop draws endowments from and the $name is the filename (edict §5).
  • 4 per-target arms — the fact is this file does not exist on that target, and its home is likewise the package: a target has $memfs because memfs.c is in that platform’s package and nowhere else. Selection needs no arms.

RULED: core is blob, fit, time, random, json, and the math files

John, 2026-07-26. What that implies, because the repo has almost arrived there already:

  • pitlib/ is that set — and as of landing 5 it literally is: blob.c, fit.c, json.c, math/{radians,degrees,cycles}.c, time.cm over internal/time.c, random.cm. native/ is gone; the outlier std/random.cm moved in.
  • The adapter is still visible in the C. pitlib’s json init symbol is pit_core_json_init — the symbol says core while the package says pitlib. That mismatch is the table’s whole reason for existing, leaked into a function name. Landing 5 deliberately did NOT touch it; landing 6 is the symbol flag day.
  • internal/ is not core and never was. nota, wota, qop, deflate, crypto, kim, eddsa, mach, shop_paths, libgit2, qopfs, sysinfo, sysfacts, bootgate, wildstar belong to the shop, the compiler and the platform. The root table made 28 unrelated files look like one namespace, which is what made “what is core?” seem like the hard question. It is not: every one of those files needs a package, and that is an easier question asked file by file.

Free and immediate: internal/debug/debug.c and internal/debug/runtime.cdone. 79 lines of stubs exposed as the importable modules debug and debug/runtime, zero callers, deleted with their two natives rows. The built static-extension table now has no debug entry, so the surface is structurally absent rather than merely unused.

That exposed one piece of dead C: Pit_SetMemoryLimit (source/runtime.c:1919, declared pit_internal.h:143) had exactly one caller, the deleted stub. Reported rather than removed, per edict 2. If a memory limit wants setting later it belongs on $runtime with the other fact-setters, not as a bare C entry point. Pit_SetMaxStackSize beside it is unaffected — three live callers.

CLOSED: the development shop no longer launders the endowments it holds

Done, and the fix went the whole way. development_shop.ce:1224-1245 is now a comment where the seven handlers were. The tools hold the endowments themselves — pit ps, stop, trace, heap, profile and the shell each claim what they use, through internal/actor_find for the $inspect half. A build that denies $inspect now denies pit ps, which is where the refusal honestly belongs. The shop keeps $inspect for actor_token() alone: naming the actors it started itself, which is shop work.

The reasoning, kept because it generalizes: a relay cannot be a boundary. Forwarding a power on request is the same as granting it, minus the record that you did.

What it was, for the record — seven handlers, each one line of shop code standing in front of an endowment the caller could hold directly:

messagerelays
resolve_actor$inspect.ref({id}) — turns an arbitrary id string into a live send target
actors$inspect.snapshot()
runtime_stats$inspect.count()
trace_actor, system_trace$hook.set_trace / set_system_trace / get_system_trace
heap_profile, profile_set, profile_clear$vm.*

resolve_actor was the sharp one: hold a shop reference, send {type:"actors"} to enumerate every id, then resolve_actor on any of them — a live send target, with no $inspect held at any point. That reopened by message the token-forgery hole this arc had just closed in use('actor'). docs/architecture/security.md’s account — that $inspect is how you name an actor you were not handed — is now true again.

My brief caused half of it. I told the agent “actors stays where it is” meaning do not add a second enumerator to $runtime, which was right, but it left the relay in place — and the relay was the actual hole. The agent flagged it rather than acting on it, which was correct.

CLOSED for now: the boot.qop storage question

Measured: boot.qop is 457 MB of pack across 278 versions; boot/content/objects/** — literally the same bytes, unpacked — is 9.3 MB across 487 versions. 49× the cost for 75% more versions, because the content store only adds changed objects and they delta, while every reseed rewrites the archive as one opaque blob.

John’s ruling: we are not acting on this, and the boot chain will NOT mount the store directly. Keep committing what is needed to boot. Revisit near the end of the rebuild, after many more phases, when a different and better storage-and-boot shape may be apparent. Do not re-raise it before then.

Decomposing os — the six surfaces, and what is ruled

platform/posix/os.c exports 28 functions and is reached through a per-target arm that is false on six consoles, each of which then re-adds its own os through a copy-pasted if (tc.system == …) branch in cake/plan.cm. That chain of six is the design failure written down: one module every platform had to provide, with nobody able to say what it should contain.

The groups, measured from pit_os_funcs[]:

groupfunctionsdisposition
dynamic linkingdylib_open, dylib_preload, dylib_symbol, dylib_has_symbol, native_module_load$dlfcn on posix; Windows its own. OPEN: probably an actor
sync file I/Othe 10 bootstrap_*OPEN: may not be needed at all
process/envgetenv, exepath, exit_code, boot_refuse, open_url$environment for vars; the rest wants $platform
entropyrandomper-platform endowment, $BCryptGenRandom and friends
clocknowper-platform endowment under a uniform $clock
miscprint, stack, enable_benchmark_native_loadOPEN: possibly delete

Three of them are not platform surfaces at all. load_internal, internal_exists and internals are lookups into the linked-in static extension table — the runtime’s own module registry wearing an OS costume. That is why os had to exist on consoles with no OS to speak of. They go to the shop.

RULED: $clock is a uniform shape over a per-platform endowment

Getting the time is not uniform enough to be one C surface — on an Amiga it is asynchronous. So $clock is an endowment with a uniform shape: give it a callback, it returns eventually with the time. How that is implemented varies per platform, and $clock itself uses the platform-level endowment underneath. Eventually the shop blocks actors from every other time-related surface, leaving $clock the only door. The raw now stays available to the time module.

RULED: $environment for environment variables

A clear endowment on its own. What is left of the process/env group — exit_code, boot_refuse, exepath, open_url — is a grab bag, and the shape it wants is a consistent $platform available everywhere, answering questions about the machine you are on: core count and the like, the equivalent of reading /proc on linux or plan9.

OPEN: dynamic linking probably wants to be an actor

The idea: if a source is a C file and the module is not in the static binary, then a dylib must be opened, and that dylib should be in the content hash. Making this an actor splits it out altogether so the logic is not present at all on consoles, rather than present and unused. John: “I’m not 100% sure how this would work.”

OPEN: is the sync file surface needed at all?

The reasoning for deleting it: bootstrapping is now the boot actor’s job. Pit starts by opening the boot cart and starting the well-known boot actor location; after that, everything can be async.

Counting against, and this is a migration cost rather than a refutation: the 10 bootstrap_* functions have ~40 callers, led by pit-shop/shop_store.cm (22), internal/bundle.ce (15) and pit-shop/shop_source.cm (13) — the object store, bundles, and source resolution. Nothing there is structurally sync; the shop is an actor and can await. So the question is not whether it is possible but whether the rewrite is worth doing now.

One caller is different in kind and should be checked before anything else: internal/engine_lite.cm:51 holds os_bootstrap_stat_fn. The engine core is loaded as one raw blob and can use() nothing, so whatever it needs cannot come from a module.

OPEN: does print survive?

Playdate has no print — it writes out through a log wire, and on platforms that support it we can print more robustly by writing to sockets. The shape if it does survive: endowments governed by C headers, $stdio providing $stdio.print. But the real question is whether it is needed at all now.

Traps that have each cost someone a session

  • make manifest depends on allFIXED (dbb85c88). manifest, seed and seed-ship now depend on pit-present, which builds only when there is no binary at all. Verified against the failure: with a native module deleted, make manifest went from exit 2 to exit 0.

  • The ABI crossing is NOT mechanically fixable, and my earlier description of it was wrong. The blocker is not seed: all — it is that source/mach.c:2960 reads const char *abi = PIT_MACH_ABI;, a compile-time constant, so a blob carries whichever runtime emitted it. Fixing the dependency order makes things worse: measured with the stamp bumped, the original make seed exits 2 with an honest mismatch, while the order-fixed version exits 0 — the producer ran on the old runtime — and the next ./pit ps dies. A quiet success leaving an unbootable tree.

    So seed now carries SEED_ABI_ASSERT, failing at the point of cause and naming PIT_CROSSING_ACCEPT_ABI, and running before the seed stamp is written so a failed crossing cannot record success. An ABI move is a crossing, not something a reseed performs.

  • Removing a name the cart is cemented against needs a two-phase landing — and this one is inherent, not a bug awaiting M1. My earlier guess that M1 would dissolve it was wrong: a cemented cart is by definition a compiled snapshot of a closure, so removing a name it embeds always needs either a generation carrying both or a boot path that does not use the snapshot. M1 changes what the cart carries, not that fact.

    Note also that since b54b4d90 boot/boot.cart is untracked, so “the committed cart” now means boot/root.cart + boot.qop.

  • build_ps1_fleet/ is a TRACKED build directoryFIXED (37e861e6). 8 files, including a generated core_static_extensions.c that still registered the debug modules after they were deleted, so anyone grepping for a deleted native found it there and concluded the deletion was incomplete. Untracked, and .gitignore now carries the general patterns build_*/ and build-*/ rather than an enumeration — the enumeration is what missed this dir in the first place.

Known open, not on the critical path

  • make check-web is RED at baselineshop_actor stack overflow at depth 1024 under node. Confirmed pre-existing by stashing and re-running. make check-all cannot be green until this is fixed, and it has nothing to do with this arc.

  • PIT_MACH_DEBUG_CONTRACTS is compiled by NO gate arm. It is gated on !NDEBUG && !__OPTIMIZE__ (source/mach.c:5230) and every check-arms arm configures -Dbuildtype=release, i.e. -O3. So mach_contract_disrupt, mach_fact_disrupt and their ~13 call sites are invisible to the entire suite. This is the same shape as the pit_text_kim.c escape — a compile-time-selected body that the default gate never compiles — and it is exactly the class check-arms was built to close. Wants a debug arm. (A3a covered it by hand: mach.c at -O0 both arms clean, plus a full -O3 runtime with -DPIT_MACH_DEBUG_CONTRACTS=1 running the real suites — vm 1086/0, language 474/0, fuzz 718/0, no contract fired.)

  • make check-arms exists now (13 arms, ~10s warm) and three arms are RED at baseline. Not wired into check-all until they are green:

    • numrep-fixed32 and fixed32+kim8+compactbuild failure, and a real guard drift. meson.build:657 decides whether to build mach_image_lifetime_test with if numrep != 'nan32', but the C file guards itself on PIT_OBJECT_ABI_NAN32, which pit_profile.h:305 also defines for fixed32. So fixed32 builds the target and trips its own #error. The proof it is drift and not design: meson.build:220 writes the same condition correctly as numrep != 'nan32' and numrep != 'fixed32'. Fix by making the meson condition ask about the ABI rather than re-listing rep names.
    • force-gctext_body and gc_policy fail, stably. text_body_test.c:178 is the argument-evaluation-order hazard CLAUDE.md documents: Pit_ConcatStringGrow(ctx, t_ref.val, Pit_NewString(ctx, "xyzzy123")). Under FORCE_GC_AT_MALLOC the inner allocation always collects, so a register copy of t_ref.val read first goes stale. Check the runtime’s own Pit_ConcatStringGrow call sites for the same shape, not just the test. The gc_policy failures (soft cap 8192 vs 16384, strike streak 0 vs 1) look like assertions that only hold at normal collection cadence — probably the test’s contract, not a bug.
  • make check-profiles is red at baseline too, for the same fixed32 build failure — its REPS includes fixed32 and it compiles the same dir. It is not in check-all, so nobody was seeing it.

  • profile= in the capacity harness prints only PIT_PROFILE_NAME. utf32 and kim8 builds both report profile=host-nan64-v1; the string that actually keys artifacts folds every axis. Anyone reading profile= as the cache key would conclude two different arms share one.

  • OPEN, needs John: the SHAPE/SHAPE_KEY row layouts disagree with the docs. Not a defect — a design call, and it shapes how a record reaches its shape in A1c.

    docs/spec/artifact-formats.mdsource/pit_internal.h:332
    SHAPEu32 first_keyu32 kind (holds "IMG1")
    u64 reservedi64 key_rows_relative
    SHAPE_KEYu32 text_value; u32 sloti64 text_relative

    The kind fielddeleted. It held "IMG1" and was the IMAGE/DYNAMIC distinction under another name. Word 0 is now uint32_t reserved, validated as zero, following the idiom PitMachPoolFunctionRow and PitMachPoolValueRow already use, with a negative test the old kind check never had. The other three fields keep their exact wire offsets and the row stays 24 bytes. Safe to require zero because there is no pmp1 producer in the tree — every pool is a hand-built C fixture.

    RULED: displacement. A shape row keeps key_rows_relative and a shape-key row keeps text_relative, because a record holds a bare pointer into the mapped SHAPES section and a displacement is followable from the field’s own address while an index needs the pool base carried alongside. That also puts shapes in the same world as STONE, which already uses slot-relative displacements — consistent with A1c’s “a shape is just another stone object.” docs/spec/artifact-formats.md documents these as indices and is wrong; the doc is corrected as part of A1c.

  • Six defects in the ported pool readerfixed (a6891ce0), along with two holes found beside them: a STONE reference did not have to name an object start (a well-formed text forged inside another object’s packed body was addressable), and one message covered two distinct failures. The reader was never dormantimage_provider.c:337,676 call Pit_MachPoolOpen; the earlier “unreachable today” note here was wrong.

  • Still open in the reader, reported and not changed: STONE’s declared alignment is never required to be ≥ PIT_OBJECT_ALIGN (harmless on x86/ARM64, not on strict-alignment targets); entry.flags/fn.flags/fn.abi_flags/value.flags are decoded and never validated while import.flags must be zero; mach_pool_shape_key_hash takes a key_row it discards.

  • arch_for_named_target is now unreferenced in-tree.

  • The psp/dc/n64 nan64 cross-check lane stamps a nan32 profile name; now a two-line fix.

  • docs/ should gain the executable-array design and the engine-single-unit constraintdone, and the first half was already mostly written. docs/shop/endowments.md has the runner’s two forms and docs/architecture/actors-and-memory.md has the runner walking the array into result slots; what was missing was the spec-level statement of the shape, now docs/spec/executables.md “A realization is an ordered array” — the three entry kinds, the endowment equivalence, and why slot resolution is what lets images be shared. The engine constraint is docs/architecture/boot.md “The engine is one unit”, stated as what it implies: a split must remove code, never use() it.


Answered — work lifted from the retired review queue

John ruled on these directly; they are work items now, not open questions.

Endowment packages, taken on demand or whole — the settled shape

This supersedes the “assumed endowments” sketch below, which named the right need with the wrong mechanism. John’s ruling, now written into docs/shop/endowments.md:

A target’s endowments are chosen by naming packages, not files. windows_endowments, macos_endowments + posix_endowments, lang-endowments for the portable ones. A recipe lists them; to know what a build can grant you read those directories.

The dev-versus-shipped question turns out to be one mechanism, not two, and the hinge is whether the shop can still compile. A development machine answers “can this be granted?” with “can this be built?” — a claim for $iocp builds a provider on demand. A shipped runtime with no compiler cannot, so anything it might ever grant must already be linked. Hence a second disposition on the same list:

endowments:       ["lang-endowments", "platform-web"]   on demand — the default
endowments_whole: ["platform-web", "debug-endowments"]  every top-level file, claimed or not

That is also the whole answer to $hook/$pgo with nothing claiming them: a development recipe takes their package whole. No optional claims, no per-name list.

Two consequences to hold onto:

  • Whole-linking changes what absence proves. Policy rather than absence is then the enforcement. Legitimate for a playground; it must not be described as small-by-construction.
  • Capability levers must stay out of the profile string. $hook/$pgo/$inspect and the native-build power change no artifact byte; stamping them would fork the realization cache and stop dev-built carts mounting on shipped runtimes. Now explicit in docs/architecture/target-profiles.md.

Which layer branches: package below, module arm above

Ruled. A package boundary where the platforms share nothing — raw endowments, where an arm would only restate which package a file is in. A module arm where they share an interfacestd/file, sdl/window, whose whole purpose is the shared part, and whose manifest should stay the one place every implementation is listed.

So the modules arms survive; the natives table and the per-target arms inside it do not.

A build can assume an endowment, and there are no optional claims

The question was how a component offers introspection as one service among many when the build may not provide it. The answer is not an optional claim — it is that a build can include a provider nothing claims. A development runtime says “I can provide $pgo and $hook” and links them; a sealed cart links exactly what its closure claims.

That keeps two rules intact that would otherwise fight: nothing unclaimed is built stays true of a shipped artifact, and a claim stays a hard requirement rather than a maybe. The build decides what is available; the claim decides what an actor gets. A component may keep requiring $inspect and simply fail on a runtime built without it, which is the honest refusal rather than a silently degraded shop.

Needs: a way to name assumed endowments in a build, and the recipe or cake plan is where it belongs, beside the endowments: permission list.

$pgo and $hook cost nothing when absent

The counters and the call/return hook should exist only when their endowment does. Today the hook is compiled into every build (seven sites in source/mach.c plus GC) and nothing can install one, and the PGO site table is likewise unconditional. Both should compile out with the endowment, which is what makes the cost table in docs/architecture/debugging.md true rather than aspirational.

This also settles pool-reader defect 6: the declared-but-unused PGO row struct is part of the same cleanup.

make check-arms — test every axis

make check compiles one point in a multi-axis profile space, and that has already let a real breakage through. A configure-and-build sweep across text, numrep, gc, and the value word is wanted, and John asked for a full test of the axes rather than only a build. Cheap, and it retires the class of bug that got through.

Fix all six pool-reader defects

Recorded when the reader was landed as a faithful port. All six are agreed sane fixes: the misleading duplicate-key message, the off-by-one arity bound against 8-bit register fields, disruption_pc == 0 doubling as the no-handler sentinel, the quadratic validation paid at open, the zero-length section satisfying the presence check, and the unused PGO row struct.

Another target-name pass

The shop-side derivations deleted earlier were the right move, and recent work has made that class of hardcoded target handling more obsolete still. Worth a second sweep — the earlier audit’s category (a) rows plus whatever the endowment-by-filename work has since made dead.

Full green everywhere, once the rework settles

check-web and make budget are red at baseline. Not chased now. The bar that matters first is that the VM runs and logs come out — vm_suite must pass — and a full green suite across every arm comes after the rework rather than during it.

The macOS .app sniff may dissolve on its own

Kept behind a sys_os predicate because a CLI test depends on app_mode and nothing sets PIT_APP. Likely disappears with the platform rework, when it becomes an ordinary startup fact published by the platform package. Recheck rather than fix.

The memory model, and the four places the runtime diverges from it

John’s target, stated 2026-07-25. There are three memory regions and no others:

  1. The runtime stone pool — a fixed array, seeded before actor 0 exists, never grown. Indices 0-127 are the ASCII characters; the rest are the constant keys C itself needs (PIT_KEY_*). s[i] becomes: read the codepoint, and if it is under 128 return table[cp]. No hash, no probe, no lock, no allocation.
  2. Mach image pools — stone, many of them, brought along by whatever modules reference them. Just constants for bytecode. Text is deduped within an image, never across.
  3. Actor heaps — everything else.

Nothing is interned across actors. Text from two modules’ pools may be different objects for the same content, and that is expected. Comparison is by content — and cheaply, because stone text caches its hash in its header, so two copies from different pools carry the same hash. Hash first, then length, then content. Care: antestone text uses that field for the letter count, so the fast path must be gated on stone rather than assumed.

The divergences, all verified

  • rt->ct_* — the key intern table. BLOCKED, and the blocker is structural. Process-wide, growing, dedup-by-content, under ct_lock, and it returns another actor’s object on a hit. pit_key_new / pit_key_new_len reach it at runtime, so obj[computed_text] = v in two actors yields one pointer — observable by identity or by probe-hit timing. This is the timing channel the text-interning deletion was for.

    Two agents hit the same wall from opposite directions, independently, and they are right. mach.c marks every cacheable loaded unit shared (mark_code_tree_shared), and both collectors then skip it — pit_gc_copy.c:884 and pit_gc_compact.c:866 each open with if (code->shared) return;. So a shared code register’s cpool[] and name, filled by mach_materialize_cpool through pit_key_new, are not actor-GC-scanned and must be process-immortal. The intern table is not incidental to that; it is what gives a cross-actor-shared code object’s constants an owner. pit_new_cfunc_internal (runtime.c:2343) has the identical shape — a process-wide cfunc cache holding a pit_key_new result in a ct-arena PitFunction.

    So moving keys to actor heaps before cpool constants live in the mapped stone pool is a use-after-move, read by other actors, silently. The ordering this forces: the image layer must not merely exist, it must be what carries constants — then a shared cpool is read-only by construction and needs no owner at all. Delete the intern table after that, never before.

  • rt->code_cache — bounded, and answered. The open question was what it is still for with mcode in the cart. Measured: a hit saves mcode lowering, and nothing else matters — 85-90% of all miss time is mach_lower_mcode_window on the root cart’s 18 windows, ~11.5 ms each, and every actor that starts loads the engine core. Deleting it costs 2.6x on every warm CLI command (15.1 -> 38.9 ms) and +51% on cold boot (185 -> 280 ms), and raises residency, because the cache is the runtime’s only owner of a loaded code tree — a load that does not enter it is never freed at all. So it stays. What was unbounded was one call site: mach_load, the compile-then-run intrinsic. Its blobs are new every time — 201 adds, 4.1 MB, 0 hits, over a session of both suites and the fuzzer — and they no longer enter. Entries over three fuzz runs: 97 -> 302 -> 502 before, 83 -> 86 after. The bound is structural, not a policy: only a blob read off a durable medium (a boot image, a start plan’s CODE window) is cached, so the set is the modules the node has started and every entry is code it may run again. The linear walk is fine at that size (63 probes average, 2.9 us a hit) and needs no index. Eviction is still impossible and that is a real finding. Freeing an entry frees the PitCodeRegister while code->code_obj — one ct-arena OBJ_CODE shared by every actor — still points at it, and actor heaps still hold that value. There is no refcount, no back-reference, and no actor-death hook, so nothing can know when a code tree is unreachable. LRU or a size cap would be a use-after-free. Bounding what enters is the only bound available until code has an owner. Two consequences to carry forward: (1) shared = 1 makes the GC skip a cached tree’s cpool, which is only sound because pit_key_new returns ct-arena text — the intern-table deletion above and this cache are one decision, and moving keys to actor heaps without moving cpool constants into the image pool is a use-after-move in shared code; (2) code loaded outside the cache leaks permanently (nothing calls pit_free_code_register except Pit_FreeCodeCache), which wants per-actor ownership of dynamically loaded code — its own work item.

  • pit_rt_ct_alloc / ct_lock — the constant arena. Its tenants are the intern table’s texts and the shapes below, both of which are leaving. When the plan is complete ct_lock should have no callers left, which makes it a good completeness check: if something still takes it, something still grows that should not.

    Who still takes it, and why, as of the immediate-ASCII deletion: rt_intern_text_to_value in both text bodies (blocked above); pit_rt_ct_alloc for shared PitCode/PitFunction, immortal by design; pit_record_shape_new for rows that outlive every actor heap; scheduler.c and internal/vm.c for read-only accounting snapshots. Two closed with the deletion: pit_key_text_object no longer interns on any rep, and the generation-time probe in mach_fuse_probe_generated stopped feeding the table.

  • pit_record_shape_new at runtime (mach.c:2693) — NOT per-record. It resolves a portable shape-spec cpool entry into a descriptor once per loaded code object, which is the pre-image path; a mapped image carries real SHAPES rows needing no materialization. It dies with the last parsed mach blob rather than as its own fix. (Its other caller, qbe_helpers.c:757, is native — ignored per edict 7.)

Shapes follow the same rule as text: inferred from code, deduped in a mach image. {x: 5, y: 3} gets a shape from the pool; rec.z = 3 drops the shape and becomes an ordinary hashed record. Nothing makes a shape on the fly. That may be revisited later.

Five other runtime-wide locks exist and are unaudited against this model: cfunc_lock, image_lock, opaque_lock, lock, and code_lock above.

Pool size: the expansion is honest except on the failure path

Measured 2026-07-25 against the actual artifacts, accounting for 100.00% of every byte across 95 pools (4452 KB) and decoding 928,093 instruction words with zero unknown opcodes.

Corrections to earlier claims in this file, both mine. The expansion is 2.72×, not 4× — the earlier source figure counted a different file set (1676 KB across the 95 modules, not 1568 KB). And pmac is not the sectioned pmp1 format: it is mach v2, a recursive function-record tree with a per-function inline cpool. pmp1’s ENTRIES/FUNCTIONS/STONE sections have no referent in boot/content.

Every suspected culprit was dead: no duplicate pools (95 pools ↔ 95 distinct locators, 1:1), no debug info in the pool (the writer forces lines_present = 0), no unused constants (13 of 47,011, 110 bytes), no padding, no alignment slack. Inter-pool dedup already works. Type-guard tests are 97,386 instructions (10.5%) — the price of a checked dynamic language, not waste. streamline removes 23.1% of the IR it is given.

The one real finding: every guard failure is an inlined 11-instruction log.panic

pit-compiler/mcode.cm:487 emit_log_panic expands to context ; loadk "log" ; load ; loadk "panic" ; loadk <msg> ; newarray ; push ; stone ; call ; callargs ; disrupt.

  • 21,921 DISRUPT sites; 21,920 (100.0%) are this block.
  • 241,724 words = 944 KB = 21.2% of every pool.
  • 13 distinct messages, led by 'cannot read property of function' (8,747 sites).
  • 78% of all 3,429 functions contain at least one.
  • Plus 245 KB of guard-vocab cpool strings, re-stored per function.
  • internal/down.ce is 3 lines of source → 67 instructions, 22 of them two panic blocks.

Combined saving ≈ 1098 KB, 24.7% of all pools; expansion 2.72× → 2.05×.

John’s ruling on the fix — compiler arc, not now

A shared tail, in bytecode, not a new opcode. The block ends in DISRUPT and never returns, so it is a one-way branch with nothing to jump back to. A MACH_TRAP <msg_id> opcode would put the context lookup and the log.panic call inside the VM — C doing what bytecode does, against edict 2. Rejected for that reason.

Likely shape: loadk <msg> ; jmp <shared_tail>2 words instead of 11, one tail per function, message varying at the site. Check first whether jump targets are function-local; if so a per-pool tail is not available and per-function sharing only wins where a function repeats a message.

But log-stripping is the load-bearing feature and comes first. John wants a compiler option to strip ALL logs from compiled source. With logs stripped the eleven instructions become bare DISRUPT — one word, no new opcode, no shared tail. So the shared tail is an optional nicety for the dev loop, where 944 KB matters far less than in a cart.

Settle when that arc starts: is the message part of the log or part of the disruption? If disruption { } can observe “cannot read property of function”, stripping the log must not take the message with it — the message would need to live on the disrupt rather than as a log.panic argument. That is the difference between stripping being safe and stripping silently degrading error reporting.

Shapes are not being used

MACH_NEWRECORD_SHAPED fires 25 times in 928,093 instructions. Dense-record lowering is effectively dead; every record literal takes the generic NEWRECORD + per-field guarded STORE path. So none of A1a/A1b/A1c is on the hot path yet — that work made the representation correct, which was its purpose. Making the compiler emit shaped records is a separate arc, after the memory model is safe and coherent. John: “make it safe and coherent first; then figure out in a separate arc how to make it fast.”

Source: plans/archive/roadmap.md