Skip to content

fix(metadata-protocol): the layered read's code layer is null for a name no package ships, before and after hydration (#21059) - #21168

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21059-layered-code-null-unshipped
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21059-layered-code-null-unshipped

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21059
Clause-②: no

What this changes

The layered door (GET /api/v1/meta/:type/:name/layers) reports three layers: the code layer, the stored layer and the effective layer. The spec's layer 1 says the code layer is null "when no artifact ships this item (it exists only as an overlay)". The method's own #5707 / #5840 rule says a layer is asserted only from a read that happened.

For a name no package ships, the code layer was null only until a hydration had put the stored row into the registry's bare slot. After that, the code layer was the stored body, and the lock and provenance flags were derived from it. Below the artifact lookup, the code layer's fallback reads the bare slot. The fallback is there for items registered at runtime with no package, and hydrated rows land in the same slot.

getMetaItemLayered's code-layer fallback now ignores a registry answer that carries the tenant mark. The hydrator already writes that mark on every stored row it registers (ADR-0010 tenant provenance, read through isTenantAuthored from @objectstack/metadata-core). No new mark is added. The same rule covers the alias retry.

  • Unshipped name: the code layer is null before and after hydration.
  • Stored body with package-provenance stamps under an unshipped name: this follows triage's ruling, which the maintainer can overturn. A stamp is not an artifact read, so the code layer is null. The lock-state resolver gets no code layer for the item, and its flags are the same before and after hydration.
  • Shipped name: unchanged. The artifact lookup answers before the fallback is reached.
  • Item registered at runtime with no package: unchanged. It carries no tenant mark and keeps its code layer.

The effective-layer binding from #21002 and isShippedFlowName are not edited. The stored rows are not changed.

Measured: the layered door, showcase composition on a database file

The first boot reads the door with the rows stored and none of them hydrated. The second boot is cold, on the same file, after the boot hydration. Every reading answered 200.

Name class Before the fix: before hydration Before the fix: after hydration After the fix: before hydration After the fix: after hydration
Unshipped, stored row code layer null; no provenance flag code layer = stored body; provenance flag org code layer null; no provenance flag code layer null; no provenance flag
Unshipped, stored body with package-provenance stamps code layer null; provenance flag package, package flag = showcase code layer = stored body; provenance flag org, package flag = showcase code layer null; provenance flag package, package flag = showcase code layer null; provenance flag package, package flag = showcase
Shipped, with a stored row (control) code layer = loader body; package code layer = loader body; package same same
Registered at runtime, no package (control) code layer = registered body code layer = registered body same same
Unshipped, on the deprecated layers flag of the by-name door not read code layer = stored body not read code layer null

For every unshipped and runtime reading, lock is none, the item is editable and deletable, and it is not resettable. The shipped name is resettable.

The table was taken before the fix on a build of origin/main e952cff578, where the dist carried no fix marker. It was taken after the fix at 4a5639294d. It was taken again at the merged head 317eca662e, where all 9 readings were identical to the post-fix run.

Tests

  • packages/metadata-protocol/src/protocol.layered-code-unshipped-name.test.ts (new, 7 cases). It covers the boot hydration and the read-side hydration, the package-scoped read, the plural spelling, the stamped corner (with and without a package binding), a non-flow type (view), and both controls.
  • packages/qa/dogfood/test/flow-unshipped-name-layered-code.dogfood.test.ts (new, 6 cases). It boots the showcase twice on one database file. It covers the layered door and the deprecated layers flag, the stamped corner's flags across hydration, and both controls.
  • @objectstack/metadata-protocol full suite: 198 files passed and 3 skipped (201); 2962 tests passed and 19 skipped. Typecheck exit 0, and the new pin is in the program (--listFiles count 1). Dogfood tsc --noEmit exit 0, with the new pin in its program.
  • Consumers of the layered read, which import this package (a downstream direction), all green: objectql 3 files, 29 tests; rest 11 files, 173 tests; runtime 1 file, 10 tests; plugin-security 5 files, 134 tests. Dogfood, 9 files, 53 tests, at the merged head: the new pin, the metadata: the layered read of a shipped flow name reports a stored row as the effective layer, so after #20946 it disagrees with the by-name read and the list (and the published-snapshot read serves that layer) #21002 layered and published-door pins, permission projection, two doors, multi-package artifact, and the two object-extension meta reads.

Ablation. The fix was committed first. scripts/ablation-replace.mjs replaced the tenant-mark test with an always-false call; the anchor went from 1 hit to 0 and the replacement from 0 hits to 1.

  • Unit pin, which resolves source: 5 failed (every subject case) and 2 passed (both controls). The restore left the blob equal to HEAD and git diff HEAD empty.
  • Dogfood pin, which resolves dist/: the package was rebuilt, and ablation-dist-preflight --absent found the guard absent from all 24 built files. 3 failed (the subject cases) and 3 passed (the hydration check and both controls). After the restore leg, the blob equals HEAD. The package was rebuilt, the preflight found the guard present, and the tree was clean.
  • Two attempts were not measurements. A first unit attempt used a replacement that was a substring of the anchor, so the tool refused it and restored before any test ran. A first dogfood attempt used a mutation the declaration build rejects. Its JS reached dist/, so dist/ was rebuilt from the restored source before the next run.

Gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at 317eca662e derived 74 commands, and all 74 were run with their exit codes recorded before any pipe. --ran reports 74 derived, 74 run, 0 NOT-MEASURED and 0 UNRUN. Two first-pass results were reruns, not failures. check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: 8 packages outside this diff had no dist/); it exited 0 after they were built. check:query-options-erasure crashed reading a temporary measurement file that was removed while it ran; it exited 0 on the rerun. pnpm lint (the whole repository, eslint . --no-inline-config) exited 0 with no findings at 317eca662e.

Acceptance notes

  • The stamped corner's flags now come from the stored layer, on both sides of the hydration. The lock source line, code layer first and then the stored layer, is unchanged and outside this card's surface. Whether a stored body's own provenance stamps should decide the flags for a name no package ships is left to triage.
  • The rule applies to every type, not only flow. The boot object limb writes the same tenant mark, so an object no package owns also has a null code layer after hydration. The unit pin covers flow and view. Objects are not pinned separately; the objectql and rest layered-read suites above are green.
  • A sibling with the same lookup shape is not edited. readCodeLayerForCarryForward uses the artifact lookup, then the registry's bare slot. It is called only when no stored row exists for the save's scope. This is a source reading, not measured.
  • scripts/engine-double-contract.pinned.json gains one generated row (--write) for the new unit pin's findOne double. That double is copied from the pinned one in protocol.flow-layered-shipped-name.test.ts.

Generated by Claude Code

claude added 3 commits October 1, 2026 10:19
…d stored row, so a name no package ships answers a null code layer on both sides of a hydration (#21059)

getMetaItemLayered's code-layer fallback read the registry's plain slot
for items registered at runtime with no package. A stored row the
hydrator registers lands in the same slot, so once a hydration had run
the code layer was the stored body and the lock flags were derived from
it, while the same read before the hydration answered a null code layer.

The fallback now discards a registry answer that carries the tenant
marker the hydrator already writes on every row it registers (ADR-0010
tenant provenance, read through isTenantAuthored). No new marker. A
stored body with package-provenance stamps under an unshipped name is
the same row: no code layer, per triage's ruling. A runtime-registered
item with no package keeps its code layer.

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
…ble in the engine-double-contract ledger (#21059)

Generated by `node scripts/check-engine-double-contract.mjs --write`: one
findOne row for protocol.layered-code-unshipped-name.test.ts, whose double
is the pinned one from protocol.flow-layered-shipped-name.test.ts.

Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Oct 1, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 1, 2026
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 5 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via getMetaItemLayered (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/contracts/metadata-service.mdx (via getPublished (sdk, the bare tail of client method meta.getPublished, bound to GET /api/v1/meta/:type/:name/published; the bare tail of client method meta.getPublished, bound to GET /meta/:type/:name/published))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-3.mdx (via /:type/:name/published (route, bridged from symbol getMetaItemLayered — its route source's handler names it))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json bafb8c949813c47b5a8486133acb1796f9d6eeb3 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c769412419a43133262f2209552b6c359a0a3b9a — the merge of head 317eca662e99e79423c5982fe9791e99fec7572f into base bafb8c949813c47b5a8486133acb1796f9d6eeb3, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin c769412419a43133262f2209552b6c359a0a3b9a && git checkout c769412419a43133262f2209552b6c359a0a3b9a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bafb8c949813c47b5a8486133acb1796f9d6eeb3 317eca662e99e79423c5982fe9791e99fec7572f && git checkout -B drift-repro bafb8c949813c47b5a8486133acb1796f9d6eeb3 && git merge --no-ff 317eca662e99e79423c5982fe9791e99fec7572f

node scripts/docs-audit/affected-docs.mjs --json bafb8c949813c47b5a8486133acb1796f9d6eeb3

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs bafb8c949813c47b5a8486133acb1796f9d6eeb3 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 317eca662e99e79423c5982fe9791e99fec7572f
Local-runs: none

This is the record of record for PR #21168 at 317eca662e, card #21059 (the layered read of a flow name no package ships reports a hydrated stored row as the code layer, against the spec's "null when no artifact ships this item"), under triage's first grade 5925776178 (the code-layer fallback does not answer a hydrated stored row as code; the hydrator's tenant marker is the discriminator, no new marker; a stored body carrying package-provenance stamps under an unshipped name is not a code layer, so the lock-state resolver reads its code layer as null, overturnable by the maintainer; pins: before and after hydration code: null, a shipped name the control), the seat's claim 5929237752 (the fallback and its alias retry only; the effective-layer binding and isShippedFlowName read only; a runtime-registered item keeps its code layer; the pins; one dogfood cold-boot pin; a patch changeset; disclosure) and the os-dev-report 5930527947 (done, one open question on the stamped corner's flags).

Inputs:

Check-runs on 317eca662e, read after convergence and collapsed latest-per-name (a background poll of the commit's check-runs, no local run; every run reports this head): 35 runs, 32 success, 3 skipped (Build Docs and Console Pin Gate path-filtered, Packed-tarball smoke (opt-in) opt-in), 0 failure. All seven required contexts are success: Lint & Repo Gates (which carries check:engine-double-contract over ① (e), check:cross-package-test-inputs over the dogfood pin's escaping path, check:adr-anchors and the rest of the check:* family), TypeScript Type Check (and its four sub-jobs, source gates, consumer gates, debt ledger, workspace), Test Core (and all six shards), Dogfood Regression Gate (and all three shards, which run the new cold-boot pin), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard. Check Changeset, Check PR Size, Spec property liveness, Dogfood Verify CLI, the claim, single-writer and part-of guards (The card this PR closes must claim this branch, Part-of PR must not also close its card, No other open PR may claim the same issue, No other open PR may claim the same single-writer path), Check Documentation Links and the labelers are success. No run is red on this head, so there is no red to compare with origin/main.

Mergeability: git merge-tree --write-tree of origin/main (f0cc16e8d5, fetched fresh for this review, seven commits past the merge-base 2488b98b48, none touching the five paths of this diff or any source file this review read; one touches a different dogfood test file) and the head, run from a throwaway bare clone sharing the object store with no merge driver registered (AGENTS.md's probe): clean, tree f164df5b88, no conflicted path. No path of this diff is routed to the os-regen driver.

Disclosure is kept at the card's level: doors, roles, codes and statuses. The tenant mark, the provenance flag, the package flag and the body's package-provenance stamps are named abstractly; the three layers by the method's own layer names; no request-body, header or field spelling appears and no seeding step is written.

① Derived judgments

(a) The discriminator — RIGHT. The mark is the hydrator's own, on every stored row it registers under a name the fallback can reach, on nothing the loader or a package-less runtime registration produces, and the alias retry is covered.

  • The diff to protocol.ts is two hunks: the import of isTenantAuthored (:86-91) and the fallback (:9278-9314). runtimeOnly (:9306-9307) discards a registry answer isTenantAuthored marks; it wraps the getItem operand of the first lookup (:9308-9309) and the alias retry (:9311). lookupArtifactItem (:15115) stays the left operand, so a code artifact answers before the discard is ever consulted. No writer is added: isTenantAuthored is the existing reader (metadata-core code-artifact-provenance.ts:43-45).
  • Every path that puts a stored row into the registry's bare slot writes the mark. The three non-object hydrations share hydrateOverlayIntoRegistry (:15941): the boot limb (:23510), the read-side hydration (:8313) and the write-through (:16228); it registers mergeArtifactProtection(stateTenantAuthorship(data), envelope) (:15994), where stateTenantAuthorship (:1703-1706) restates the mark on a copy of every object body (a non-object body never reaches it, :15969), and mergeArtifactProtection (:1614-1627) overwrites that mark only when an artifact envelope exists (:1616) — never for a flow (:15993), and for another type only under a shipped name, where the left operand answers first. The boot object limb (:23483) and applyObjectRegistryMutation (:15709) write the mark literally. So an unshipped name's hydrated row is marked on every side of every hydration, the state the dogfood pin proves on the real registry.
  • Never on a loader entry: the loader registrations (engine.ts:6959, :6971, :7014; metadata/plugin.ts:2875; the scanner) pass a package id, and applyProtection (spec protection.zod.ts) stamps the package value when the key is absent. Never on a runtime registration with no package: with no package coords and no protection block applyProtection leaves the key absent, which is the runtime control in both pins. The one body that could carry the tenant value into a loader entry is a source body declaring it itself — no producer in the tree does, and such a body is already outside isCodeArtifactBody, so lookupArtifactItem, isArtifactBacked, the write refusal and the ADR-0048 precedence already judge it tenant-authored; the layered read now agrees with them rather than disagreeing.
  • False null, read for: a package-less object registered through MetadataFacade carries the mark on its contributor copy (metadata-facade.ts:195) by that facade's own design — its docblock calls such an item runtime-authored by definition and marks it so no reader classifies it a code artifact — so a null code layer for it is the facade's own classification, not a false one; and where the facade is the metadata service, its unmarked generic-map copy answers the service read (:9255-9260) before the fallback is reached. None found.

(b) Scope — RIGHT, and the type reach is read, not assumed.

  • Only the fallback moves. The effective-layer binding (:9455-9458, PR fix(metadata-protocol): the layered read of a shipped flow name reports the loader's body as the effective layer, as the by-name read and the list do (#21002) #21043), isShippedFlowName (:14722), the overlay read (:9337-9409), isArtifactBacked (:9467) and the lock source line (:9469) are byte-identical to the merge-base.
  • The rule is not type-scoped, and that is right for every type the door serves, because the fallback is reached only when the service read misses and lookupArtifactItem misses — no loader holds the name and no code artifact is registered. For object, getItem is getObject (registry.ts:3907-3909): the owner layer of a code package answers getArtifactItem's object branch first (:3967-3985, ADR-0029 D9.6), so a shipped object never reaches the fallback whatever overlays sit on it; a tenant-created object (owner under the sentinel, marked by the boot limb or the write door) now reads code: null, which is layer 1's text for an item that exists only as a stored row, and its flags come from the stored row instead of the merged registry body, which carried the sentinel as a package flag. For a discriminated bundle type getArtifactItem scans the composite members first (:3996-4003), so a loader member answers before a bare bundle member is discarded. The metadata service's own map is loader-backed, not the registry (metadata-manager.ts getDiagnosed), so a tenant-created object does reach the fallback in a real composition, as the PR body says. The unit pin's view case covers the non-flow, non-object class; object is reached and not pinned (③ flag 3).

(c) The stamped corner's flags — RIGHT as landed; consistent with triage, with #5707 / #5840 and with the unchanged lock source; no affordance and no write path moves; A is within the seat's discretion for this PR.

  • Source: after the fix code is null on both sides of a hydration, so lockSource = code ?? overlay ?? {} (:9469) is the stored row, served raw (Object-extension overlay fields are missing from GET /meta/object/:name (present in the list route) — the overlay's fields can never be set through the UI #7556), and resolveLockState (:9470; spec metadata-protection.zod.ts:208) reads the row's own stamps: the provenance flag reads package, the package flag is set, lock is the row's, resettable is isArtifactBacked (:9467), which is false because the hydrated copy is marked and getArtifactItem's bare fallback declines it (registry.ts:4028-4034). Before the fix and before hydration: the same bytes, the same flags. Before the fix and after hydration: the lock source was the hydrated copy, whose mark the hydrator restated over the stamps, so the provenance flag read org with the package flag set. Relative to the pre-fix pre-hydration reading nothing moves; relative to the post-hydration reading only the provenance flag moves; editable and deletable derive from lock (none in all three readings) and resettable is false in all three. No write door reads the layered answer; each resolves its own gate (getEffectiveLock, isArtifactBacked, tenantAuthoredWriteRefusal), all untouched.
  • Triage decided the code layer (a stamp is not an artifact read) and said the resolver then reads the code layer as null; it said nothing about which layer feeds the resolver next. That is the lock source line's documented order (the spec: "artifact winning over overlay"), unchanged. Under getMetaItemLayered 的 overlay 读用裸 catch:sys_metadata 读失败时三层视图把「读不到」画成「没有 overlay」 #5707 / MetadataManager.get() 丢弃 loadDiagnosed 的 degraded 判定:loader 读不到与「这一项没声明」在 6 个消费点上不可分辨 #5840 the flags are derived from a read that happened (the stored row). Under automation: a flow created through the authoring door can assert package provenance, and the ADR-0126 guards and the activation ledger then treat it as package-shipped #20761 rule 1 the stamps are display, and these flags are display: the affordances come from lock. The one cost is the dev's: a stored-only item whose bytes claim a package's provenance is reported with it.
  • The dev's save-door claim, verified at the head: saveMetaItem strips the three derived keys on every authoring write (:16900, stripDerivedProvenance :1669-1676). For the card's type it is stronger: tenantAuthoredWriteRefusal runs first (:16892, :14860-14903) and refuses, 422, a flow sent with code-shipped stamps under a name the loader's set does not hold, unless the stamp names the save's own base or a row the store already binds to that package. The two server-stated rewrites keep the silent strip. So the corner's at-rest population is residue written before the strip, and direct store writes. ⛔ Prime Directive chore: version packages #10: a card for B needs that population measured, not inferred.
  • Discretion: A decides nothing the lock source line had not already decided on origin/main before hydration, moves no affordance and opens no write, so the seat may accept it on this PR. B restates tenant authorship on the stored layer where it feeds the resolver: that edits the lock source line (outside this claim), moves the by-name door's agreement, and changes a documented order on a served envelope, so when filed it is the maintainer's to rule, with the measurement. The maintainer's standing right to overturn triage's code-layer ruling is unaffected by either.

(d) The pins — RIGHT; they hold triage's pins and the stamped corner, and they red without the fix as reported.

  • Unit (protocol.layered-code-unshipped-name.test.ts, 7 cases): the unshipped name across the real boot hydration (loadMetaFromDb), the read-side hydration (getMetaItemsForExecution), the package-scoped and plural spellings, the stamped corner with and without a package binding read scoped and unscoped, a view, and the two controls. The registry double is PR fix(metadata-protocol): the layered read of a shipped flow name reports the loader's body as the effective layer, as the by-name read and the list do (#21002) #21043's: the two key shapes, getItem's bare-first precedence (registry.ts:3934-3935) and getArtifactItem's composite-first scan with the artifact test (:4010-4034); registerItem with a package stamps as applyProtection does; bare() proves each hydration happened. The engine double carries no write verb and routes findOne through assertEngineFindOnePredicate. flagsOf equality pins the whole envelope across hydration, the stamped corner included.
  • Dogfood cold boot (flow-unshipped-name-layered-code.dogfood.test.ts, 6 cases, two boots on one database file): the first boot reads the layered door with the rows at rest and the bare slot proven empty; the cold boot proves the hydration; then the layered door, the deprecated layers flag on the by-name door (whose closed query set admits it, per the fix(metadata-protocol): the layered read of a shipped flow name reports the loader's body as the effective layer, as the by-name read and the list do (#21002) #21043 record), the stamped corner's flags across hydration, the shipped control (the loader's body under the package's flags) and the runtime control (registered after the boot, no stored layer). It runs in the dogfood isolated project (vitest.config.ts:259-261); the escaping path is spelled fileURLToPath(new URL(…, import.meta.url)), a form check:cross-package-test-inputs recognises.
  • Red without the fix, from source: with the discard inert the hydrated bare entry is answered as code, so every assertion of a null code layer after a hydration reds and nothing else does. Unit: cases 1-5 red, the two controls green — the reported 5 / 2. Dogfood: cases 2, 3 and 4 red; case 1 (the hydration proof) and the two controls green — the reported 3 / 3. Both counts read as predicted; the unit leg from source, the dogfood leg from a rebuilt dist with the preflight proof, the fix committed first (d95251a8b0), as the body states.
  • Triage's pins: code: null before and after hydration (unit 1-3, dogfood 2-3); the shipped control (unit 6, dogfood 5); the runtime control (unit 7, dogfood 6); the stamped corner (unit 4, dogfood 4).

(e) scripts/engine-double-contract.pinned.json, one generated row — RIGHT, compelled and exact, as in the #21043 record's ① (d). The new unit pin's engine double has a findOne behind the producer-side predicate, so the gate's RETAINED invariant requires the ledger to learn the file through --write. The row { file: protocol.layered-code-unshipped-name.test.ts, verb: findOne, pinned: 1 } sits at its localeCompare position between the protocol.flow-… rows and protocol.legacy-overlay-delete.test.ts; the whole ledger is sorted at the head (zero unsorted pairs); the entry count moves 835 to 836; the diff is the five lines. Lint & Repo Gates, which carries check:engine-double-contract, is the byte-exact arbiter (the check-run paragraph). Disclosed as Deviation 1.

(f) The changeset .changeset/21059-layered-code-null-unshipped.md, @objectstack/metadata-protocol patch, Clause-②: no — RIGHT; every sentence is delivered at the head. The layered read's layer-1 contract and the two answers it used to give; the code layer now skipping a stored copy by the mark the startup load already writes; null before and after the load with the flags from the stored layer both times, the stamped corner included; the deprecated layers flag answering the same; packaged items and runtime-registered items unchanged; the stored rows not changed (no write verb in the diff). The package is released (17.5.0, not private). Disclosure in the changeset holds (③ flag 5).

Surface inventory: no route, schema, query set, status code or exported signature changes; one method's code-layer fallback, for exactly the hydrated-stored-row population, through the three doors that read it (the layered door, the deprecated flag, the dispatcher's layered answer); the published doors untouched; one patch changeset; one generated ledger row; no governed path.

② Semver level

The PR body's line 2 reads Clause-②: no, as the claim did, and the changeset is patch. RIGHT.

  • Clause ② asks whether the card widens an accept set or expands a public surface: it does neither. The import is internal and runtimeOnly is a closure; no member, key, route, parameter or status is added; the layered response schema is untouched.
  • Not a (narrowing) either: no accepted input is refused. A stored row under an unshipped name is still accepted at rest, still hydrated, still reported as the stored layer and still the effective layer; what moves is one diagnostic read's layer-1 value for one state (hydrated), to the value the spec and the method's docblock already state and the same read already answered before the hydration. A bug fix in a released package takes patch; no ADR-0087 disposition is owed. Check Changeset is green on this head.

③ Boundary flags

  1. Deviations, all five answered. (1) The ledger row — compelled and exact, ① (e). (2) The temporary probe — not in the diff (five files), never committed. (3) turbo's agent-guidance block in AGENTS.md — not in the diff; its producer is tooling: turbo 2.11.5 writes a managed block into AGENTS.md in every agent worktree; opt out with "agentGuidance": false in turbo.json #21146, fixed on main by fde553c509 (PR chore(turbo): opt out of the agent-guidance block in the root turbo.json #21151, the root turbo.json opt-out, landed 10:36Z), which is not an ancestor of this head (the head's merge parent 2488b98b48 is two commits earlier), so the dev's worktree really was exposed and restoring to HEAD was the right act; the queue's rebuild onto main carries the opt-out, and nothing is owed here. (4) The trailers — the two authored commits carry the model-free pair AGENTS.md specifies; the merge commit is git's default message with no trailers, which the pre-push hook does not refuse and a squash landing replaces. (5) The worktree's removal — procedural.
  2. Out-of-scope finding 2, readCodeLayerForCarryForward — REAL at the head, reachable, class b, file it. The sibling (:7799-7815; the lookup :7807, the alias retry :7808-7811) keeps the two-step lookup and the alias retry with no discard, and its docblock (:7783-7784) promises the order getMetaItemLayered resolves its code layer — a sentence this PR makes false without touching the sibling. Reach, from source: storedBodyForCarryForward (:7712, the call :7735) calls it only when repo.get finds no row at the save's own state and exact scope (sys-metadata-repository.ts:461-471: the package key is exact, a bound or a package-less row, with no fallback between them), and only for a type with a redactor (metadata-redaction.ts: datasource, flow). A tenant-marked bare entry does exist then: a package-bound save of a name whose only row is package-less (or the reverse), or an org-scoped save of a name with an environment-wide row, since boot hydration fills the bare slot from the environment-wide row. In those cases the hydrated copy is, by accident of hydration state, the body the read served (the read's stored lookup falls back bound-to-global and org-to-env), and before a hydration the sibling answers nothing, so no credential is carried and the placeholder is persisted literally. The stale-after-delete corner is closed by the delete door's tier 3 (restoreArtifactRegistryView :16343, removeOverlayEntry at :16402). ⛔ Not fixed by copying this PR's discard — that would turn the accidental right answer into no carry-forward; the fix is a scope-aware stored read ahead of the code layer. Carrier: a card, under Prime Directive chore: version packages #10, naming the falsified docblock as its contract text and the two scope cases as its reach; source reading only, not measured. Not a FAIL: outside the claim's surface and disclosed in the report and the PR body.
  3. Named by this review, not by the dev — a residual of the same class, not a defect of this diff. hydrateOverlayIntoRegistry marks the row it registers, but the expansion it registers beside it (:15995) does not carry the mark: hydrateExpandedViewItems (:16123-16131, the registration :16130) registers a stored view container's expanded items from the raw body through expandRuntimeViewContainer (:16031), which carries only the container's package id and the artifact envelope. For a container stored through the runtime door under an object no package ships, the layered read of an expanded name still answers the expanded item as code after a hydration (the fallback keeps an unmarked bare entry) and null before. Same contract text (layer 1), narrower reach (runtime-door view containers of unshipped objects, read by their expanded names); the loader's own expansions (engine.ts:6971) are package-stamped and unaffected. Class b; carrier: the card in flag 2 or its own. An object pin — the type the PR body names as reached and leaves unpinned — belongs with it.
  4. Fixes #21059 — RIGHT. The card's What (the layered door for an unshipped flow name after hydration), its Reach (the deprecated flag and the dispatcher's layered answer share the method; the Studio diff tab reads it; the published door was already unaffected) and both of its Open items (the discriminator is the hydrator's mark; the stamped corner per triage's ruling, pinned) are delivered through the one method the card named. The claim comment names this branch; The card this PR closes must claim this branch and Part-of PR must not also close its card are green. The residuals in flags 2 and 3 are not the card's.
  5. Disclosure held across the PR body, the changeset, the report, both file headers and every test title: doors by path, roles, codes and statuses; the tenant mark, the provenance flag, the package flag and the stamps named abstractly; no request-body, header or field spelling in prose; no seeding recipe (the body says the rows are "stored" and "at rest"). The two pins' code is the one place the seeding and the stamps are spelled, as a pin must — the reading the fix(metadata-protocol): the layered read of a shipped flow name reports the loader's body as the effective layer, as the by-name read and the list do (#21002) #21043 record gave for its own pins. No model identifier in the commits, the body, the changeset or the pins; no tracker number in runtime prose (the diff's runtime text is a comment and a closure).
  6. The check-run picture above: every required context converged green and no run is red, so nothing here is red on origin/main either.

Implemented-by: claude/issue-21059-layered-code-null-unshipped
Reviewed-by: session_01VvcEokUG1tvVxkceYfR5XB

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 1, 2026 12:11
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 1, 2026 12:11
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 1, 2026
Merged via the queue into main with commit d34aa58 Oct 1, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21059-layered-code-null-unshipped branch October 1, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants