Skip to content

authz caching leg D: getMetaItems overlay cache keyed on the registry epoch — resolve the SchemaRegistry-hydration trap explicitly #11967

Description

@os-support-ai

Leg D (ship second) of the accepted #11633 cross-request caching design. Provenance: design document = #11633 comment 5394942824 (§4 leg D, §10.2); maintainer acceptance = #11633 comment 5404681591 (2026-08-25, verbatim 「接受你的建议,继续」). Filed by PM session session_01KWRU3s15AJz7PGW7a7wdCh.

Content: cross-request cache for getMetaItems' sys_metadata read, keyed on the registry epoch; reuses the shipped metadata.changed channel cross-node.

Hard requirement: the SchemaRegistry-hydration trap named in the design (§4 leg D) must be resolved explicitly in the PR body — stated and designed around, not inherited silently.

Acceptance criterion: "cached answer ≡ uncached answer" identity pin for getMetaItems across an epoch bump (write → epoch change → fresh read), per the design §7 pin plan.

Premise-first: design measured at 992161b728; re-verify the epoch/channel seams on current origin/main before implementing.

Activity

  1. self-assigned this
    on Aug 27, 2026
  2. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round 2
    Session: session_01LZbWd2jNV1FErXTPSS4Dry
    Branch: claude/issue-11967-getmetaitems-overlay-cache
    Worktree: objectstack-issue-11967
    Domain: domain:engine
    File surface: the getMetaItems seam and its sys_metadata read in the metadata/registry layer, plus its pins. ⛔ packages/spec is READ-ONLY (spec seat); ⛔ do not edit packages/core/src/security/index.ts (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Clause-②: no — internal cross-request caching bounded by a "cached ≡ uncached" identity pin; no accept/reject change, no public surface widened. ⚠️ If the route requires a new export or changes an observable contract, that is clause ② — stop and report.
    Serial constraints cleared: leg C (#11966) landed as squash a8c00e27, and the substrate (#11968) landed as 4bd6faa2 — both on origin/main now, so this leg builds on shipped ground rather than an in-flight branch. ⛔ Queued sibling #12623 (SchemaRegistry.registerObject packageId) touches the same registry family and is serial-queued behind this card — not dispatched. No open PR touches the registry seam.

    The ruled design — ⛔ not re-adjudicable

    Provenance: design = #11633 comment 5394942824 (§4 leg D, §10.2); maintainer acceptance = #11633 comment 5404681591, 2026-08-25, verbatim, ⛔ untranslated:

    接受你的建议,继续

    with forks 1A / 2B / 3A / TTL-0. This is leg D, ship-second — leg C shipped first and is now on main.

    Content: a cross-request cache for getMetaItems' sys_metadata read, keyed on the registry epoch, reusing the shipped metadata.changed channel cross-node.

    ⛔ The hard requirement — this one is a deliverable, not a caveat

    The SchemaRegistry-hydration trap named in the design (§4 leg D) must be resolved EXPLICITLY in the PR body — stated and designed around, ⛔ not inherited silently.

    Read §4 leg D of the design before writing code and address the trap in the PR body in your own words, naming what you measured. ⛔ A PR that implements the cache without that section is incomplete regardless of how green it is.

    Acceptance criterion: a "cached answer ≡ uncached answer" identity pin for getMetaItems across an epoch bump (write → epoch change → fresh read), per design §7's pin plan.

    PM mechanism assumptions — ⛔ my readings, confirm or refute

    • The card says "design measured at 992161b728; re-verify the epoch/channel seams on current origin/main". ⚠️ origin/main has moved a long way since — including leg C and the substrate landing today. Re-measure the epoch and metadata.changed seams as they actually shipped, ⛔ not as the design predicted they would.
    • ⭐ Leg C is your worked precedent and it is worth reading before you start. It landed two rules the design did not spell out, both of which may apply here: (1) it caches a success only when the engine exposes the write-epoch seam, declining rather than degrading to a TTL-only shape; (2) its invalidation retires success entries only, because retiring a failure memo on a write would restart log spam a previous card removed. Whether either transfers to leg D is yours to measure — ⛔ do not copy them in, and ⛔ do not assume they do not apply.
    • ⚠️ Leg C also refuted a design prediction: the design expected an object-scoped backstop on sys_setting writes, but the substrate that shipped is process-wide and object-agnostic (Fork 1 A), so its backstop over-invalidates — a hit-rate cost, never correctness. Expect the same shape here and say so if you find it.

    ⛔ Confirm or refute all of the above on the current ref. premise_still_valid: false with no PR is a legitimate and valuable delivery.


    Generated by Claude Code

  3. claude commented on Aug 27, 2026

    @claude
    Contributor

    Dev claim — leg D (ship-second) of the #11633 accepted design.

    Session: session_01LZbWd2jNV1FErXTPSS4Dry
    Branch: claude/issue-11967-getmetaitems-overlay-cache
    Worktree: objectstack-issue-11967, cut from origin/main at 15bf9e85.

    Assignee is already set by the PM claim above (5439213960); I am not touching it.

    Scope I am working to, as dispatched:

    • Cross-request cache for the sys_metadata overlay read inside getMetaItems, keyed on the registry epoch, reusing the shipped metadata.changed channel cross-node (design §4 leg D, §10.2; forks 1A / 2B / 3A / TTL-0).
    • Hard requirement carried, not paraphrased: the SchemaRegistry-hydration trap named in design §4 leg D gets an explicit, measured resolution section in the PR body.
    • Acceptance criterion: a cached answer is identical to the uncached answer pin across an epoch bump, per design §7's pin plan, with every staleness assertion paired with a hit assertion so neither half passes on a cache that never caches.

    Premise-first, before any code: re-measuring the epoch and metadata.changed seams as they actually shipped on the current ref, not as the design predicted at 992161b728. Leg C (a8c00e27) and the substrate (4bd6faa2) are read as landed precedent, not copied in.

    Fences respected: packages/spec read-only; packages/core/src/security/index.ts untouched; #12623 (registerObject packageId) is serial-queued behind this card and will be reported, not fixed here.


    Generated by Claude Code


    Generated by Claude Code

  4. claude commented on Aug 27, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 11967,
      "status": "done",
      "branch": "claude/issue-11967-getmetaitems-overlay-cache",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12727",
      "head_commit": "4482c848",
      "premise_still_valid": true,
      "clause2": "no — the new module is package-internal (verified: not re-exported from packages/metadata-protocol/src/index.ts); no export added or removed from any public entry point; getMetaItems' request/response contracts unchanged; no accept/reject behaviour changed. The only new operator-visible surface is a deployment env var, registered in the canonical table.",
      "summary": "Leg D implemented. getMetaItems' sys_metadata overlay read is cached cross-request, keyed on the shipped #11968 engine write epoch (read structurally, since metadata-protocol does not depend on objectql), bounded by OS_METADATA_OVERLAY_CACHE_TTL_MS (default 30s, 0 = real off path, malformed folds to off). Leg C's 'no seam, no cache' rule transfers and is pinned. Leg C's 'retire successes only' rule does NOT transfer — measured finding, not omission: leg D has no failure memo to protect, and nothing is cached when the read throws.",
      "schema_registry_hydration_trap": "Resolved by PLACEMENT, not by care. The cached value sits UPSTREAM of the hydration branch: what is cached is the overlay ROW SET (the value the two queryByOrg calls produce), never the merged answer. Every consumer of those rows still runs on every call, hit or miss — overlay parse, package-aware merge, hydrateOverlayIntoRegistry, MetadataService merge, disabled-package filter, nav contributions, decorations. So hydration cannot be skipped by a hit, and its idempotence never has to be proven because it is not replayed. MEASURED: test section 2 records registerItem per call and asserts that after two calls the engine was read ONCE (finds unchanged on the second) while the registry hydrated TWICE, same rows same order; a third call makes it three. Ablation 2 shows it from the other side. The trap has a bigger sibling the same placement closes: getMetaItems' answer depends on FOUR mutable sources (sys_metadata rows, SchemaRegistry, MetadataService, artifact table) and the epoch observes only the first, so caching the MERGED answer would have served three sources nothing in the key can validate. Keeping the cache's reach co-extensive with what its key validates is the load-bearing property.",
      "files_changed": [
        "packages/metadata-protocol/src/meta-overlay-cache.ts (new)",
        "packages/metadata-protocol/src/meta-overlay-cache.test.ts (new, 20 cases)",
        "packages/metadata-protocol/src/protocol.ts (getMetaItems overlay read wrapped)",
        "content/docs/deployment/environment-variables.mdx (one row: OS_METADATA_OVERLAY_CACHE_TTL_MS)",
        "content/docs/concepts/metadata-lifecycle.mdx (docs-drift close-out — see docs_drift)",
        ".changeset/metadata-overlay-read-cache.md (minor, grade argued in-file)"
      ],
      "gates": {
        "union_source": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack — derived from git at 4482c848, 6 paths vs merge base 15bf9e859; re-derived after every commit including the docs fix.",
        "green_count": 49,
        "green": [
          "check:nul-bytes",
          "check:engine-double-contract",
          "check:where-matcher",
          "check:objectql-double-limit",
          "check:query-options-erasure",
          "check:test-source-alias",
          "check:type-source-resolution",
          "check:cross-package-test-inputs",
          "check:ci-filter-parity",
          "check:comment-mask-adoption",
          "check:durability-log-level",
          "check:filter-alias-parity",
          "check:published-files",
          "check:type-check-coverage",
          "check:page-declaration-shape",
          "check:slot-lookup",
          "check:plugin-teardown-shape",
          "check:doc-anchors",
          "check:doc-authoring",
          "check:docs-single-h1",
          "check:docs-redirects",
          "check:docs-audit-scope",
          "check:role-word",
          "check:doc-frontmatter",
          "check:doc-route-spelling",
          "check:docs-section-name",
          "check:section-landing-index",
          "check:doc-formula-expressions",
          "check:doc-security-posture",
          "spec check:docs",
          "check:published-readme-links",
          "check:empty-changeset",
          "check:changeset-no-major",
          "check:adr-0087-registration",
          "check:changeset-gate-self-tests",
          "check:objectui-changeset",
          "check:pm-half-states"
        ],
        "red_then_fixed": "check:engine-double-contract EXIT=1 once. Its own verdict line: 'RETAINED [findOne]: meta-overlay-cache.test.ts pins 1 engine double that the pinned ledger does not record ... Run --write and commit.' I did NOT take that remedy. No case calls findOne — the overlay path issues exactly one verb (find) — so I deleted the unexercised double. Ledger untouched, gate green on re-run, 20/20 still pass.",
        "not_measured": "check:type-check-debt — its --re-measure half THROWS here: '27 workspace dependencies of the ledgered packages have no built type entry point on disk'. NOT MEASURED, not red. Its structural half (check:type-check-coverage) reached OK before the throw and is counted green. Substantive answer taken directly instead: the ledger records 63 frozen errors for @objectstack/metadata-protocol; tsc --noEmit on the final tree reports exactly 63; ZERO name either touched file. --listFiles confirms both new files are inside the package's tsc program, so that reading is not vacuous. (This package declares no typecheck script — the zero-match trap — hence tsc directly.)",
        "not_run": "Repo-wide scans (pnpm lint, the full 176-family farm) left to CI, which runs them once regardless. Stated as 'not run', NOT as a narrowing — no narrowing is claimed for them."
      },
      "tests": "New file 20/20. Owning package @objectstack/metadata-protocol FULL suite: 1984 passed, 10 skipped, 0 failed (144 files). Downstream consumers exercising getMetaItems, all green: @objectstack/rest 76 files / 1382 tests; @objectstack/runtime 10 files / 483 tests; @objectstack/objectql 16 files / 301 tests. Dependency closures built first in every case. Every exit code captured before any pipe; verdicts read from os-verify-lock's own VERDICT line, and where a command was sequenced with ';' the tool's 'batch-last-exit, NOT a verdict on the whole command' warning was honoured and per-step codes used instead.",
      "ablations": [
        "TEST PATH IS SOURCE-RESOLVED, PROVEN NOT ASSUMED: both subjects are relative-specifier imports inside the package. Both ablations mutated only source, ran vitest with NO rebuild, and behaviour changed on that run — a dist-mediated path would have stayed green through both, so no rebuild leg applies. (Sibling protocol.hydrate-overlay-canonical-type.test.ts DID need mutate-rebuild-prove because its target lives in @objectstack/spec and resolves through exports to built output; the distinction was measured, not inherited.)",
        "ABLATION 1 (staleness half): readMetaOverlayCache's epoch comparison neutered to 'if (false)'. PREDICTED IN WRITING AND COMMITTED BEFORE THE MUTATION (5dabef3c): RED, exactly 5 named cases. OBSERVED: RED, 5 failed / 15 passed (20) — exact count AND exact named set.",
        "ABLATION 2 (hit half): the writeMetaOverlayCache call removed, so the cache is read but never populated. PREDICTED AND COMMITTED BEFORE THE MUTATION (5dabef3c): RED, exactly 10 cases. OBSERVED: RED, 10 failed / 10 passed (20) — exact count AND exact named set.",
        "NAMED POSITIVE CONTROL, green under BOTH: section 3 'an engine with no write-epoch seam keeps its exact query multiset'. It never stores an entry, so ablation 1 never reaches the neutered comparison and ablation 2 removes a store it never made.",
        "The two failure sets overlap on exactly 4 cases — precisely the cases written to carry BOTH a hit and a staleness assertion, so each ablation kills a different assertion inside the same case. Outside the overlap they are disjoint (1 ablation-1-only, 6 ablation-2-only). That is the evidence the halves are independent.",
        "MUTATION AND RESTORE BOTH PROVEN ON DISK, never by exit code: grep counts of injected AND deleted text plus git hash-object against the HEAD blob (non-empty and different) before each run; after each, worktree blob == HEAD blob, zero marker residue, git diff HEAD empty. Each script carried an EXIT INT TERM restore trap with absolute paths from git rev-parse --show-toplevel.",
        "RE-MEASURED AFTER A LATE CHANGE: adding section 9 moved both counts (4->5, 9->10). Rather than let committed numbers stand on the earlier tree, both predictions were rewritten, re-committed ahead of the mutation, and both ablations re-run."
      ],
      "docs_drift": {
        "rederived": "node scripts/docs-audit/affected-docs.mjs --json d29e42f8b1b44b92ea58c1d05e619f27c027c3a9 — 29 pages (25 hand-written + 4 release-owned) from 25 anchors. Run myself; not worked from the PM summary.",
        "discriminator_verdict": "HELD, and it earned its keep — it put exactly one page in scope. A cache whose criterion is 'cached answer identical to uncached answer' cannot falsify a WHAT page, because that identity is exactly what the pins assert (deep-equal incl. array order, key separation, clone isolation). It can falsify a WHEN page. A SECOND and stronger ground applies to most of the list and is reported separately from the invariant: 22 of the 25 matched on tokens my diff never touches — the anchor extractor works per changed FILE, not per changed hunk, and protocol.ts is 20k lines, so organizationId, packageId, expiresAt and routes /forms/:slug, /book/:name/tree, /:type/:name/publish were named for appearing somewhere in an edited file. expiresAt is a pure name collision (mine is a cache-entry field, theirs is session expiry).",
        "pages_opened_and_read": [
          "concepts/metadata-lifecycle.mdx — NOT named by the bot, found by hand — IN SCOPE, CORRECTED in this PR",
          "kernel/services-checklist.mdx (via getMetaItems) — row 134 states request/response shape (WHAT, identity pin covers it); row 150 scopes its claim to metadata.changed invalidating peer REGISTRY caches, which stays true. No edit.",
          "kernel/contracts/metadata-service.mdx (packageId, publish route) — documents metadataService.watch, a different seam, untouched. No edit.",
          "permissions/authorization.mdx (packageId) — line 221 'no staleness window' for the sys_permission_set projection. Still true: the epoch bump precedes the write's own middleware chain, so any read after a write sees a moved epoch; read-your-writes is exact on the writing node and is pinned. No edit.",
          "permissions/permission-sets.mdx (packageId) — line 309 Discard Overlay 'resyncs immediately'; the discard is an engine delete, epoch moves, next read re-reads. No edit.",
          "protocol/kernel/config-resolution.mdx (organizationId) — read deliberately, it was leg C's near-miss; its freshness prose is about a SETTINGS snapshot on settings:changed, leg C's territory. No edit.",
          "api/client-sdk.mdx (expiresAt, sdk names) — ETag conditional metadata caching; the ETag derives from the response body, which the identity invariant holds equal, so it stays correct, and the page guarantees no freshness. No edit.",
          "kernel/events.mdx (organizationId) — data lifecycle hooks only, no metadata-cache or visibility claim. No edit.",
          "18 remaining hand-written pages — scanned for visibility-timing language; none states a freshness or query-count claim about the metadata read. Out of scope on both grounds."
        ],
        "page_the_bot_could_not_see": "content/docs/concepts/metadata-lifecycle.mdx — the canonical 'Repository to Change Log to Cache to Registry' page, ABSENT from the bot's 29 because it names MetadataRepository, MetadataCache, MetadataManager, MetadataClusterBridgePlugin and not one symbol from my diff. Exactly the standing blind spot. Its cross-replica note ended 'peers re-read from the shared database', which after this change is no longer unconditional: metadata.changed invalidates the MetadataManager caches the note is about but does NOT retire the leg-D overlay-read cache, so a peer's re-read can be served from it until an authz.invalidated hint bumps the local epoch or the TTL expires. Added a bounded qualification naming both retirement paths and the env var, and calling out that the LOCAL-write case is still exact because the epoch bump precedes the write's own middleware chain.",
        "release_owned_read_only": [
          "releases/v17.mdx (via /:type/:name/publish) — line 2717 'a just-saved overlay is dispatchable immediately rather than after the next listing (#4521)'. OWES NOTHING: #4521 is the write-side write-through (applyRegistryWriteThrough), untouched here; and the save is an engine write, so the epoch moves and the next read re-reads. Both mechanisms still hold.",
          "releases/v16.mdx (organizationId) — line 504, unscoped GET /meta/:type dedupes package-aware. OWES NOTHING: a WHAT claim; identity pin plus the packageId key-separation case cover it.",
          "releases/v14.mdx (/book/:name/tree) — line 136, that route enforces authorization. OWES NOTHING: route-name collision, unrelated surface.",
          "releases/index.mdx (organizationId) — no metadata, overlay or /meta mention at all. OWES NOTHING.",
          "ROUTING ITEM FOR PM: none. No release page describes behaviour this change modifies. None was edited."
        ]
      },
      "deviations": [
        "REFUTED, and it changed the implementation: the design (#11633 section 4 leg D) says leg D's epoch bumps on 'any metadata.changed watch event' and that this channel makes it multi-node-safe. It does not. metadata.changed is published by MetadataManager.notifyWatchers, driven by loader/repository events; the writer whose rows this cache holds is SysMetadataRepository, and packages/metadata-protocol/src/protocol.ts contains ZERO notifyWatchers / subscribe / watchService calls — a sys_metadata overlay write emits no metadata.changed event. I did NOT subscribe: it would buy nothing while reading to the next maintainer as a live invalidation path that never fires. Cross-node for leg D is the substrate's authz.invalidated channel (a peer hint calls epoch.bump('remote')) plus the TTL. This refutation is also what put the one docs page in scope.",
        "CONFIRMED (PM assumption 3): the shipped substrate is process-wide and object-agnostic — ObjectQL.executeWithMiddleware bumps on every insert/update/delete on ANY object, ahead of the middleware chain. Same over-invalidation leg C reported: any business write anywhere retires this cache. Hit-rate cost, never correctness.",
        "CONFIRMED (PM assumption 2, rule 1): leg C's 'cache a success ONLY when the engine exposes the write-epoch seam' transfers unchanged and is pinned, including that a partial { current } object is NOT a seam.",
        "DOES NOT TRANSFER (PM assumption 2, rule 2): leg C retires successes only to protect #10221's failure memo. Leg D has no failure memo — the unprovisioned-store path throws via rethrowUnlessMetadataStoreUnprovisioned and nothing is cached when the read throws. Only successful reads populate the cache, including deliberately the empty one (the negative-caching win).",
        "CORRECTED: the design cites protocol.ts:5993-6001; the file is packages/metadata-protocol/src/protocol.ts, getMetaItems at :6009. The design records 2-4 reads; on the HOT path it is exactly 2, because enforceApiAccess reaches it via RestServer.loadObjectItems, which passes NO organizationId, so queryByOrg(orgId) never runs — the cost is queryByOrg(null) plus the alt-type retry the empty first result fires.",
        "ADDED beyond the card, and pinned: storage bounding. The bucket previously kept one never-evicted entry per distinct (type, packageId, organizationId) ever requested — bounded in principle by tenant count. A write at a new epoch now drops the whole stale bucket (Fork 1 A's coarse invalidation applied to storage). The per-entry epoch comparison is still the VALIDITY rule and is not redundant: eviction runs only in the store, which runs only on a miss. Pinned in section 9 via a package-internal diagnostic, because the eviction has no behavioural signature and would otherwise be an unpinned optimisation.",
        "ADDED, correctness-critical: cached rows are deep-cloned in BOTH directions. Downstream, record.metadata is handed to a merge chain that mutates it when it is an object rather than a JSON string, so the snapshot must be cloned on store (or this call's own merge corrupts it) and on serve (or the first hit's merge corrupts it for later hits). Rows that will not clone are not cached. Pinned in section 6, paired with a hit assertion.",
        "SCOPE HELD: packages/spec untouched, packages/core/src/security/index.ts untouched, content/docs/releases/ untouched — all verified by git diff against the base; the four release pages were READ only. #12623 not touched. previewDrafts left entirely uncached (the cached value is the active-overlay row set, whose WHERE clause does not depend on it), so draft preview keeps today's exact behaviour. PR is DRAFT, not enqueued, no auto-merge."
      ],
      "mcp_calls": "0 — a repo-scoped REST probe returned 200 from this seat, so every GitHub read and write (issue body, all comments, the design and acceptance comments, the claim comment, PR create/update/read-back, the dedupe scan) went over REST. gh is absent from this container. No MCP GitHub tool was called.",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT filed, already open as #12514: dispatch-gates reports 9 packages/spec gate families whose declared path populations the tree no longer has ('the layout moved'), so they score a quiet green for every card. Dedupe done over 300 open issues via REST + local grep; #12514 is that exact finding.",
        "NOT filed, pre-existing and already ledgered: @objectstack/metadata-protocol declares no typecheck script and carries 63 frozen tsc errors in the DEBT ledger (its note points at #4311). Unchanged here — exactly 63 before and after, none in the touched files. Recorded only because the count had to be measured directly to answer the ratchet that refused.",
        "FOR PM, no card filed — a docs-audit blind spot with a second live instance: affected-docs.mjs extracts anchors per changed FILE, not per changed hunk. Editing 30 lines of a 20k-line file named 22 pages via tokens the diff never touches, while the ONE page my change actually moved was invisible because it names no symbol from the diff. Both halves of that (over-naming and under-naming) showed up in this single card. #12500 is an adjacent over-naming report about check:i18n, but it is about gate derivation rather than about docs anchors, so this may or may not be the same card — PM's call whether it is worth one."
      ]
    }

    Generated by Claude Code

  5. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    Contributor

    ACCEPT — PR #12727

    Reviewer of record: domain:engine seat, session session_01LZbWd2jNV1FErXTPSS4Dry (R2). Verified against the tree, ⛔ not against the report.

    Checklist

    Item Reading
    Scope 6 files, +1067 / −23 vs merge base. Matches the report exactly.
    Fences ⛔ packages/spec 0 changed files · ⛔ packages/core/src/security/index.ts 0 · ⛔ content/docs/releases/ 0 (four pages read, none edited). All three held.
    Clause ② no, verified: meta-overlay-cache appears 0 times in packages/metadata-protocol/src/index.ts (control: that file carries 36 export lines, so the zero is a reading) ⇒ the new module is package-internal. No entry-point export added or removed.
    CI ⏳ running on final head 4482c848, zero red so far. Enqueue gated on every check green.
    Changeset minor, grade argued in-file.

    ⭐ The hard requirement is met — and met the right way

    The dispatch made the SchemaRegistry-hydration trap a deliverable. It is resolved by placement, not by care, which is the strongest available form:

    What is cached is the overlay ROW SET — the value the two queryByOrg calls produce — never the merged answer. Every consumer of those rows still runs on every call, hit or miss: overlay parse, package-aware merge, hydrateOverlayIntoRegistry, MetadataService merge, disabled-package filter, nav contributions, decorations.

    ⇒ Hydration cannot be skipped by a hit, so its idempotence never has to be proven — because it is never replayed. And it is measured, not argued: the test records registerItem per call and asserts that after two calls the engine was read once while the registry hydrated twice, same rows same order; a third call makes it three. Ablation 2 shows it from the other side.

    ⭐ The larger sibling this placement also closes is the best single observation in this round: getMetaItems' answer depends on four mutable sources (sys_metadata rows, SchemaRegistry, MetadataService, artifact table), and the epoch observes only the first. Caching the merged answer would have served three sources that nothing in the key can validate.

    Keeping the cache's reach co-extensive with what its key validates is the load-bearing property.

    That is a rule worth more than this card, and I want it quotable.

    ⚠️ A design claim refuted, and it changed the implementation

    The card says leg D "reuses the shipped metadata.changed channel cross-node." Measured false, and the dev did not build on it:

    • metadata.changed is published by MetadataManager.notifyWatchers;
    • the writer whose rows this cache holds is SysMetadataRepository;
    • protocol.ts contains zero notifyWatchers / subscribe / watchService calls.

    ⇒ The dev did not subscribe. Cross-node for leg D is authz.invalidated plus the TTL. ✅ Refuting a card's own content sentence rather than implementing around it is exactly what premise_still_valid is for — and this refutation is also what put the one docs page in scope.

    Leg C's two rules — measured for transfer, ⛔ neither copied nor assumed

    I asked explicitly not to copy them in and not to assume they don't apply. Both were measured:

    • Rule 1 (no seam, no cache) TRANSFERS — pinned, including that a partial { current } is not a seam. ✅
    • Rule 2 (retire successes only) does NOT transfer — and this is reported as a measured finding, not an omission: leg D has no failure memo to protect, and nothing is cached when the read throws. ✅ That distinction is the difference between a considered decision and a gap.

    Two corrections the dev made rather than absorbed

    • check:engine-double-contract went red once, and its own verdict line prescribed the remedy: "Run --write and commit" — i.e. widen the ledger. ⛔ The dev did not take it: no case calls findOne (the overlay path issues exactly one verb, find), so it deleted the unexercised double instead. Ledger untouched, gate green on re-run, 20/20 still pass. ⭐ Refusing a gate's own offer to widen a ratchet, because the honest fix was smaller, is the right instinct and the rarer one.
    • Deep-cloned cached rows in both directions, correctness-critical: the downstream merge mutates record.metadata when it is an object. Rows that will not clone are not cached. That is a real hazard a naive cache would have shipped.

    Ablations — predictions committed to git before each mutation

    Both predicted counts and named sets hit exactly: ablation 1 (staleness half) RED 5/20; ablation 2 (hit half) RED 10/20. ⭐ The two failure sets overlap on exactly 4 cases — precisely those carrying both a hit and a staleness assertion, and are disjoint outside it. That structure is itself evidence the two halves are independently load-bearing.

    ⚠️ Re-measured after a late change: adding a test section moved both counts (4→5, 9→10); both predictions were rewritten, re-committed ahead of the mutation, and both ablations re-run. ⛔ Not retrofitted to the observed number — which is the failure mode a written-first prediction exists to prevent.

    Source-resolution proven, not assumed (relative specifiers; a dist-mediated path would have stayed green through both legs).

    Docs drift — the discriminator held, and the blind spot paid

    My WHAT-vs-WHEN hypothesis held, and put exactly one page in scope. The dev added a stronger second ground I had not seen: 22 of the 25 matched on tokens the diff never touches, because the anchor extractor works per changed FILE, not per changed hunk — and protocol.ts is 20k lines.

    ⭐ And the page that actually was in scope — content/docs/concepts/metadata-lifecycle.mdx — was NOT on the bot's list at all. Found by hand, because it names MetadataRepository / MetadataCache / MetadataManager and not one symbol from the diff. Its cross-replica note ended "peers re-read from the shared database", which this change makes conditional: metadata.changed invalidates the MetadataManager caches that note is about but does not retire the leg-D overlay cache. Corrected with a bounded qualification naming both retirement paths, the env var, and that the local-write case stays exact.

    ⇒ Both halves of the documented blind spot showed up in one card: 22 false rows from file-granularity, and one true page invisible for lack of a shared token. That is the case I flagged at dispatch, and it paid.

    4 release-owned pages read, none edited, none owes anything — v17's "just-saved overlay is dispatchable immediately" survives because the save is an engine write and the epoch moves; v16 is a WHAT claim the identity pin covers; v14 is a route-name collision; index has no mention. Routing item for me: none.

    NOT MEASURED — declared, and answered substantively instead

    check:type-check-debt throws here ("27 workspace dependencies of the ledgered packages have no built type entry point on disk") ⇒ NOT MEASURED, ⛔ not red. The dev took the substantive answer directly: the ledger records 63 frozen errors for this package, tsc --noEmit reports exactly 63, zero name either touched file, and --listFiles confirms both new files are inside the package's tsc program — ⭐ so that reading is not vacuous, which is the check that makes it worth anything.

    Enqueue condition

    ⛔ Not enqueued. Bar is every check green — not the required subset. On an all-green head: ready → merge queue.

    Follow-up I am taking, not leaving to you

    The docs-audit anchor-granularity finding is a real, twice-instanced instrument defect and is mine to file, not yours to carry. Filing it separately.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions