Skip to content

fix(metadata-protocol)!: a name a disabled package holds through a stored row is judged over the served set, never by save order (ADR-0150 D3) - #33130

Merged
objectstack-fleet[bot] merged 119 commits into
mainfrom
claude/project-thread-saq29l
Nov 13, 2026
Merged

objectstack-fleet[bot] merged 119 commits into
mainfrom
claude/project-thread-saq29l

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Part of #32354

Clause-②: yes (narrowing) — on both transports, a bare read (plain and /published) of a name held by the stored rows of two or more enabled packages beside a disabled one is refused 409 RESOURCE_CONFLICT naming only the enabled holders, where it answered by save order; on RestServer, ?package=PAIR of a disabled customization pair whose base is enabled answers 404 where it served the base's item. Three bare reads widen from 404 or 409 to 200 with the one enabled holder's row (table below). ?package=DISABLED (case 3) is not a move: 18.4.0 already answers 404.
Metadata-protocol: none — no key, value or metadata type an author writes changes; what moves is which stored row a by-name read answers while a package is disabled.
Skills-docs: none — no skill or doc teaches the by-name answer for a name a disabled package holds through a stored row; content/docs/deployment/marketplace-handbook.mdx already says a disabled module is hidden.

Supersedes #32811 (same work: that PR's head branch belongs to an earlier card thread, which this thread cannot push; this branch resumes from its head cd1143c19 and merges main once).

维护者速读

  • 改了什么: 不指名包读取一个元数据名时,如果某个包已停用、而这个名字由多个包的存储行持有,现在按「在用的持有者」判定:只剩一个在用包就答它的行,两个以上就 409 且只点名在用包,不再按保存顺序答,也不再点名停用包。另外 ?package=停用的定制包 现在答 404,不再透出它基础包的内容。
  • 为什么改: 专题 ADR-0150 D3:停用一个包,它在每个读门上都应消失,结果不随保存顺序变化。本轮同时落实了契约复审 6856157850 的 S1–S4。
  • 风险与回滚: 第一方 0 处(没有第一方应用在启动时停用包)。收窄两处(三包存储行同名且其一停用 → 409;停用定制包的地址 → 404);进程内的分层读取(安全目录回显、打包权限集锁)保持原答案、不会抛错。回滚即 revert 本 PR。
  • 先看: packages/metadata-protocol/src/protocol-meta-item-face.ts 的 servedRowHolderOf 与 servedAddressJudgment;protocol-meta-layered-reads.ts 的两处调用(只改址、不拒绝);protocol-exact-qualifier.ts 新增的一行。
  • 两分钟验证: pnpm --filter @objectstack/rest exec vitest run src/meta-disabled-package-doors.test.ts,预期 27 例全过(main 上其中 9 例红)。
  • 需要你做的: 无,席位落地。

What changes

Head 5c37471ba5, on main 2c98485db5 (merged once). The diff against main is this card's 7 files, +619 / −11. #32808 (#32501) has landed, so nothing of it rides here any more.

The fix, three hunks in @objectstack/metadata-protocol:

  1. Row holders judged over the served set (protocol-meta-item-face.ts: servedRowHolderOf, servedAddressJudgment, servedAddressOf). A read naming no package, of a name a disabled package holds, takes the registry's served holder as before (objectql + metadata-protocol + runtime: by-name doors judge a name's holders over the served list, so a disabled package neither refuses nor is named (ADR-0150 D3; #32101 F4 class) #32183), else the stored rows' one served holder: the registry's served holders together with the packages the active rows are bound to, less every disabled package. One → read at its address; two or more → the by-name read's own refusal (ambiguous-in-environment) naming only those (seat ruling 6817756360 Q2); none → the read as before. Asked only while some installed package is disabled, of a type the registry reads by package, when no enabled package ships the name in code and no package-less row or environment item is stored. The item's active rows are read once (readItemActiveRows), and two kinds of row hold nothing: a row the reads decline (declinesStoredRow, review S4) and a row bound to a customization pair of any holder of the name (pairsOf, review N2 widened by 5c37471b): a pair row is bound over every holder it is a pair of (metadata: overlaySlotBinding's other readers (bundleMembersHeldBy, getEnvironmentScopedItem) bind a pair row without reading overrides.package (ADR-0150 D4) #32408), never a holder of its own.
  2. Who refuses (review S1). servedAddressJudgment answers the refusal, never throws it. The plain read (readMetaItemFace) throws it through servedAddressOf. getMetaItemLayered only re-addresses to a served holder and otherwise leaves the read as it was, so its in-process resolution readers (the security catalog echo, the packaged-permission-set lock gate) keep the answer they had, as the packagelessLayeredReadRefusal docblock requires; that method returns the refusal to the /layers door's step 1c. The docblock gains one paragraph saying so.
  3. A disabled package's own address (protocol-exact-qualifier.ts disabledExactAddressHolder, one line): an address naming a disabled package is its withheld item whether it holds the name in the registry, only as a stored row, or not at all (a customization pair: review S2).

Review N1: the plain read resolves the member selector once (resolveBundleMemberSelector), as the layered read does.

Moved answers. FROM = main 2c98485db5 (18.4.0 plus later merges) with this change's four source files put back; TO = this head. D disabled, E (and F) enabled. Both transports, both registration orders, the save orders as listed, unless a row names one transport.

# state door FROM main TO this head arm
C1a rows of E and D only, no shipper, D's row saved first bare plain read, /layers, /published 404 200, E's row widening
C1b the same, E's row saved first bare /layers 409 naming E and D 200, E's layers widening
C1b′ the same bare plain read, /published 200, E's row the same none
C1c D ships the name in code, E holds it only as a row bare plain read, /layers, /published 404 200, E's row widening
N2 D ships it, D's pair holds D's declared override row, E holds a row (RestServer) bare plain read, /layers, /published 404 200, E's row widening (C1c's)
N2′ the same without E's row (RestServer) the same 404 404 none
Q2 rows of D, E, F, no shipper bare plain read 404 (D saved first) † 409 RESOURCE_CONFLICT naming E and F, details.candidates narrowing
Q2 the same bare /published as the plain read † 409 naming E and F narrowing
Q2′ the same bare /layers 409 naming D, E and F † 409 naming E and F same status, D unnamed
S2 PAIR disabled, its base enabled and shipping the name (RestServer) ?package=PAIR, with PAIR's declared override row 200, the override (stamped with the base) 404 narrowing
S2′ the same, no override row (RestServer) ?package=PAIR 200, the base's item 404 narrowing
S4 D ships a flow name, E and F hold undeclared (declined) rows (RestServer) bare plain read, /layers, /published 404 404 none
C3 E ships the name in code, D holds it only as a row ?package=D 404 404 none (second guard; review S3)
C2 the environment's package-less row beside E and D both shipping bare /layers code layer E's E's none (CONTROL)

† A pin stops at its first failing assertion, so on the FROM leg the Q2 pins stop at the plain read. The plain read's FROM is the card's development-round reading (D saved first: 404; an enabled row saved first: 200 with it); /published asks the plain read first and answers its answer; /layers is the development-round reading.

Every case re-enabled: the answers go back (the C1 pins re-enable D and see /layers refuse naming both).

Merge

Tests

Pins (packages/rest/src/meta-disabled-package-doors.test.ts, the two [#32354] describes; packages/runtime/src/domains/meta-disabled-holder-served-stack.test.ts, the [#32354] describe, real ObjectQL engine with an SQL store, reboot included). New in this round: Q2's bare /published on both transports (S3), the in-process layered read and packagelessLayeredReadRefusal in the Q2 state (S1), the shipped flow name with declined rows (S4), the disabled pair address with and without its override row (S2), and the disabled base's pair override row alone and beside an enabled row (N2).

Legs, each through scripts/ablation-replace.mjs --hold with a string-literal marker (MAIN: the four source files put back at origin/main), each rebuilding @objectstack/metadata-protocol (both suites read its dist/) and proving the marker reached dist/ with scripts/ablation-dist-preflight.mjs (MAIN: servedRowHolderOf absent), each restored with git checkout HEAD -- and git diff HEAD empty; the final rebuild carries none of the four markers (--absent, exit 0 each). Cells: cases failed / cases in the file.

leg REST (27) runtime (16) red cases
head 0 0 —
MAIN 9 8 C1 (4 + 4 with reboot), C1c (2, runtime), Q2 (2 + 2), S2 (2), N2 beside E (1); C3, S4, N2 alone and C2 green, as declared
S1 off (the layered read throws the refusal again) 2 0 Q2 (2): promise rejected "PackageScopedNameRefusedError: … held by 2 packages …" instead of resolving
S4 off (declined rows counted) 2 0 S4 (2): The flow "shipped_flow" is held by 2 packages … package "app.healthy_demo"; package "app.third_demo"
N2 off (a disabled base's pair override counted for the pair) 1 0 N2 beside E: The dashboard "pair_dash" is held by 2 packages … package "com.qa.other"; package "com.qa.pair-base.custom"
H2 off (the new disabledExactAddressHolder line) 2 0 S2 (2): 200 with the base's item / the override
PAIR off (pair rows counted as holders of their own, on 5c37471ba5) 1 0 N2 beside E; and packages/objectql package-scoped-exact-qualifier.test.ts 2/230 red (the two [#32408] pins: the bare read served the pair row with every holder disabled)

The previous round's legs on cd1143c19 (ROWS, FACE, LREF, LADDR, F1, F1+H2 off; the #32811 body) cover the C1/Q2 hunks, which this round does not change.

Verification

All on head 5c37471ba5 unless stated, under scripts/pm/os-verify-lock.sh where it builds or tests.

  • packages/objectql package-scoped-exact-qualifier.test.ts (the [#32408] pins that went red in Test Core (4/8) on 032a1edad8): 230/230.
  • pnpm --filter @objectstack/metadata-protocol test: Test Files 337 passed, 7 skipped (344); Tests 22799 passed, 36 skipped (22835); lock VERDICT command-exit 0.
  • pnpm --filter @objectstack/metadata-protocol typecheck, and typecheck of rest and runtime: exit 0 each.
  • The pin files: rest meta-disabled-package-doors.test.ts 27/27; runtime meta-disabled-holder-served-stack.test.ts + meta-legacy-pair-served.test.ts 35/35.
  • pnpm lint (eslint over the whole tree, --no-inline-config): exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands: 72 families derived for this diff, all run on 5c37471ba5 (and before on 032a1edad8, after a full pnpm -r build, exit 0), 72 exit 0; --ran reconciliation exit 0, 0 NOT-MEASURED. Among them check-adr-0087-registration --base origin/main, check-changeset-no-major, check-empty-changeset, check:type-check-debt and check:dual-build-cjs-loads (both under the lock).
  • Dogfood battery (owed: the title carries !, Clause-②: carries (narrowing)), pnpm --filter @objectstack/dogfood test on 5c37471ba5 after the full build: Test Files 460 passed, 3 skipped (463); Tests 3874 passed, 42 skipped (3916); lock VERDICT command-exit 0. (On 032a1edad8: the same counts.)

Acceptance notes

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec untouched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request. (Supersedes fix(metadata-protocol)!: a name a disabled package holds through a stored row is judged over the served set, never by save order (ADR-0150 D3) #32811, see above.)
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…, not per deployment (ADR-0150 child B, part 1 of the webhook family)

WIP: the switch, the platform-requested auth.* names, the bundle-addition
rule, the package-scoped /meta read and the send's holder-filtered bundle.

Claude-Session: https://claude.ai/code/session_01ADHkmTMDJkEC5D92VHoU5v
Co-authored-by: Claude <noreply@anthropic.com>
…latform-requested auth names and the bundle-addition rule

Claude-Session: https://claude.ai/code/session_01ADHkmTMDJkEC5D92VHoU5v
Co-authored-by: Claude <noreply@anthropic.com>
…nd a pair's member as the package's own

A read naming a dependent whose only member of a bundled name is an addition
(a locale its base does not ship) was refused not-held-by-qualifier once
email_template joined the package-scoped readers, and a pair's added member
on its base's address was replaced by the registry's item. The qualifier now
asks the holders as the caller it names, and keeps an item of a pair of the
admitted package.

The co-owner dogfood case asserts the ADR-0146 D2 refusal a co-owner meets
first at install-local.

Claude-Session: https://claude.ai/code/session_01ADHkmTMDJkEC5D92VHoU5v
Co-authored-by: Claude <noreply@anthropic.com>
…he email template's narrowing; changeset

Claude-Session: https://claude.ai/code/session_01ADHkmTMDJkEC5D92VHoU5v
Co-authored-by: Claude <noreply@anthropic.com>
…s base's address for a bundled type only

Claude-Session: https://claude.ai/code/session_01ADHkmTMDJkEC5D92VHoU5v
Co-authored-by: Claude <noreply@anthropic.com>
…rowing; pin the qualifier's holder ask for the nine

Claude-Session: https://claude.ai/code/session_01ADHkmTMDJkEC5D92VHoU5v
Co-authored-by: Claude <noreply@anthropic.com>
…mplate send resolves its name from the caller's package (ADR-0150 D3)

The flow notify node records the package whose own flow body is armed
beside its template reference; the email and inbox channels hand it to
sendTemplate/renderTemplate as callerPackageId; the registry loader
resolves the name from it and, for a name two packages hold, reads the
resolved holder's own address. A send that names no package keeps the
bare-name meaning.

Claude-Session: https://claude.ai/code/session_01WGufzgX7CHbaN1f2ZtiAiS
Co-authored-by: Claude <noreply@anthropic.com>
…is refused on both exact reads and listed by the upgrade report

Two packages of one App share the App's customization pair, so a locale
row there with no overrides declaration cannot say whose template it
extends: each holder's exact read refuses it naming every holder, the
list scoped to a holder leaves it out, and os migrate stored-references
lists it under pairMembers. One holder, the App's own address and a row
that declares its base keep being served. Pins for F1 (loader rungs,
service ladder, notify node, channels, dogfood) and F2; changeset.

Claude-Session: https://claude.ai/code/session_01WGufzgX7CHbaN1f2ZtiAiS
Co-authored-by: Claude <noreply@anthropic.com>
…otype.at, which the package's lib target does not declare

Claude-Session: https://claude.ai/code/session_01WGufzgX7CHbaN1f2ZtiAiS
Co-authored-by: Claude <noreply@anthropic.com>
…) for email_template, beside api

Two packages of two namespaces may now each hold one email template name, so
the second package's item no longer overwrites the first's in MetadataManager's
in-memory store: a read naming a package answers that package's own, and a read
naming none of a two-holder name is the D3 refusal (#31527 B-shape item 1).
Pins that email_template has no metadata redactor, so a save never takes the
carry-forward's code-layer read for it (item 4).

Claude-Session: https://claude.ai/code/session_01WGufzgX7CHbaN1f2ZtiAiS
Co-authored-by: Claude <noreply@anthropic.com>
…-3giljf

# Conflicts:
#	packages/objectql/src/package-scoped-e8-readers.test.ts
#	packages/objectql/src/package-scoped-read.ts
…-3giljf

# Conflicts:
#	packages/cli/src/utils/stored-package-references.ts
#	packages/metadata/src/package-slotted-store.ts
…es hold is no holder's own

A package-less row (the environment's item, or a sys_metadata row bound to
no package) of a name two or more packages hold is the customization of one
holder and cannot say which (ADR-0150 D4). Each holder's exact read now
refuses it 409 ambiguous-in-closure naming every holder, the list scoped to a
holder (which a send made from it reads) leaves it out, and
os migrate stored-references lists it under packageLessRows. One holder,
the _env address and the platform-requested auth mails are unchanged. The
rule sits in the exact qualifier, so it covers every type read by package
(epic ruling 6761802819 on #31699).

Claude-Session: https://claude.ai/code/session_01WGufzgX7CHbaN1f2ZtiAiS
Co-authored-by: Claude <noreply@anthropic.com>
…-3giljf

# Conflicts:
#	packages/objectql/src/package-scoped-e8-readers.test.ts
#	packages/objectql/src/package-scoped-read.ts
…et (#32183)

A disabled package's item is not a holder. `packageScopedHoldersOf`
withholds a holder whose package is disabled (`isPackageDisabled`, the
predicate `listItems` hides by), keeps the caller's own holding, and reads
every holder under `includeDisabledPackages: true`, which
`resolvePackageScopedName` forwards.

Durable judgments read every holder: the four stored-reference backfills,
the CLI stored-reference report and the picklist load audit, so a binding
never moves with a reversible disable.

The security catalog never re-targets a stored column that names a
disabled holder, and a name only disabled packages hold is not-found
there and in `holdsPlatformPermissionSet`, never the metadata service's
by-name answer. The REST action rung answers a thrown package-scoped
refusal as the declared 409, not `degraded`.

Claude-Session: https://claude.ai/code/session_01WGufzgH2rGjWC9emeku9yp
Co-authored-by: Claude <noreply@anthropic.com>
Pins, each with its BASE reading at main de141e3:

- objectql: the source half (served holders, the caller's own holding,
  every holder on request, re-enable, a disabled base never promoting a
  bundle addition), the library readers including the read MCP
  `agent_prompt` makes, and action addressing.
- Both `/meta` transports over the live `PACKAGE_SCOPED_READ_TYPES`: two
  holders with one disabled, only a disabled holder, a parity pin per type
  (the bare read and the list agree through disable and enable), and a
  shareable namespace's co-owners.
- The REST action rung and the security catalog (no re-target of a
  disabled holder's column, no metadata fallback, the platform verdict).
- Every durable judgment keeps every holder: the four backfills and the
  CLI stored-reference report.

The picklist verdict (`picklistReferenceVerdict`: the load audit, the
install door and a field's options) judges every holder, so a reversible
disable of a dependency never refuses the next boot or empties a live
field's options; the by-name `picklist` read judges the served holders.

Claude-Session: https://claude.ai/code/session_01WGufzgH2rGjWC9emeku9yp
Co-authored-by: Claude <noreply@anthropic.com>
Clause-② no (narrowing): one in-process library answer narrows for a
name only disabled packages hold; every door move is listed FROM -> TO.

Claude-Session: https://claude.ai/code/session_01WGufzgH2rGjWC9emeku9yp
Co-authored-by: Claude <noreply@anthropic.com>
…eads no migration placeholder (#32183)

The two label lines read `FROM → TO`, which the ADR-0087 gate takes as a
migration prescription beside `not-required (no-migration-prescription)`.
Each moved answer still reads as before → after; nothing is rewritten by a
consumer, so the disposition stands.

Claude-Session: https://claude.ai/code/session_01WGufzgH2rGjWC9emeku9yp
Co-authored-by: Claude <noreply@anthropic.com>
…heir withheld item (#32183)

With holders judged over the served set, the by-name read of a name only
disabled packages hold resolved to nothing, and its envelope carried no
package. The layered view's step 1b and `/published` ask that envelope
whether a disable withheld the item, so both doors took the name as not
withheld: `/layers` served the disabled package's code layer at the bare
address and at `?package=` of any non-holder, and `/published` served its
snapshot.

The item face now answers such a name as that package's withheld item
(item undefined, the envelope naming the disabled holder), the shape a
disabled holder's read had, on both faces. Every door keeps the missing
name's 404.

Claude-Session: https://claude.ai/code/session_01WGufzgH2rGjWC9emeku9yp
Co-authored-by: Claude <noreply@anthropic.com>
…ished 404s (#32183)

The item face now carries part of the fix, so `@objectstack/metadata-protocol`
joins the changeset. The note says the layered view and `/published` keep
the missing name's 404 for a name only disabled packages hold, and lists the
bare read of a name two or more disabled packages hold (409 → 404).

Claude-Session: https://claude.ai/code/session_01WGufzgH2rGjWC9emeku9yp
Co-authored-by: Claude <noreply@anthropic.com>
…n the action addressing pins (#32183)

The file's import graph reaches `standalone-stack.ts`'s dynamic import of
`@objectstack/metadata`; check:test-source-alias asks an edited test file to
load it at module top, so no clocked case pays that transform.

Claude-Session: https://claude.ai/code/session_01WGufzgH2rGjWC9emeku9yp
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzgH2rGjWC9emeku9yp
Co-authored-by: Claude <noreply@anthropic.com>
…eview round on #32214

- notify node: a literal payload `template` string records the armed package
  too, as the channels read it (F-A).
- the pair-row owner check is one self-contained function,
  pairRowOnAddress, which the exact read and the scoped list both ask: the
  declared base first (F-B), the App's own pair, two owners refused, a sole
  owner that is not one of the address's bases not served (S2).
- SchemaRegistry.getPackageScopedItem skips a pair row that declares
  another holder as its base, so the App's address answers its own item.
- stored-references hints, its module doc and the changeset say truthfully
  that no door binds an added member to one base today (#28777) (F-C).
- changeset: Metadata-protocol narrowed; slotted set lists api, agent, skill
  and email_template; the package-less rule names PACKAGE_SCOPED_READ_TYPES;
  the pair row's sole-owner and App-slot rules.
- email-service docs page: the new callerPackageId member.

Claude-Session: https://claude.ai/code/session_01WGufzgX7CHbaN1f2ZtiAiS
Co-authored-by: Claude <noreply@anthropic.com>
…-3giljf

# Conflicts:
#	packages/cli/src/commands/migrate/stored-references.integration.test.ts
#	packages/metadata/src/package-slotted-store.ts
#	packages/objectql/src/bare-name-one-holder.ts
#	packages/objectql/src/registry-bare-name-one-holder-ai-family.test.ts
#	packages/objectql/src/registry.ts
#	packages/qa/dogfood/test/bare-name-one-holder-flow-family.dogfood.test.ts
#	packages/services/service-automation/src/engine.ts
…holds take the served holder's stack

Contract review M1 on #32327. A read naming no package, of a name an
enabled and a disabled package both hold, now reads at the served
holder's address: the plain read (the face), the layered read's code
layer, overlay row and lock, and the layered row-ambiguity refusal,
which no longer refuses a read the served holder addresses. Before, the
layered code layer was the first composite in registration order (the
disabled holder's when it registered first), and a legacy row bound to
the disabled holder made the bare read answer 404.

Pins on both transports, both registration orders: the plain read and
/layers for every package-scoped type, and the plain read, /layers and
/published with legacy rows bound to the disabled holder (REST harness
and a booted kernel).

Claude-Session: https://claude.ai/code/session_01WGufzgH2rGjWC9emeku9yp
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

21 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 5c005e0e42e207b073843b255248877b84442342.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 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 — 10 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 5c005e0e42e207b073843b255248877b84442342 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8f1dd13aa96d90372594edd40741761f20995f92 — the merge of head 5c37471ba53aa5155b3e7fbd5cc3bc63fd547627 into base 5c005e0e42e207b073843b255248877b84442342, 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 8f1dd13aa96d90372594edd40741761f20995f92 && git checkout 8f1dd13aa96d90372594edd40741761f20995f92
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5c005e0e42e207b073843b255248877b84442342 5c37471ba53aa5155b3e7fbd5cc3bc63fd547627 && git checkout -B drift-repro 5c005e0e42e207b073843b255248877b84442342 && git merge --no-ff 5c37471ba53aa5155b3e7fbd5cc3bc63fd547627

node scripts/docs-audit/affected-docs.mjs --json 5c005e0e42e207b073843b255248877b84442342

⚠️ 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 5c005e0e42e207b073843b255248877b84442342 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 13, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

review#1 · os-tesla · session_01L9qibzosF5fZk98UHos9cv

Contract-review-claim: session_01L9qibzosF5fZk98UHos9cv · head 032a1ed · 2026-11-13T16:17Z

…ts own in the served row holders (#32408)

Test Core (4/8) on 032a1ed: two objectql pins of #32408 went red. A row
bound to a holder's pair (the App's pair or a legacy per-package pair) is
bound over every holder it is a pair of, so a disable never moves it; the
row holder set counted the pair as an enabled holder of its own and
re-addressed the bare read to it, serving a disabled holder's row. The
pair-row exclusion now covers every pair of a holder (pairsOf), which also
subsumes review N2's declared-override case.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4d1FGsHCVfNAwXgN9sE
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

review#1 · os-tesla · session_01L9qibzosF5fZk98UHos9cv

Contract-review-claim: session_01L9qibzosF5fZk98UHos9cv · head 5c37471 · 2026-11-13T16:37Z

Delta on the moved head, after knock 6858982833. The claim 6858560808 on 032a1eda lapsed with the head; no record was posted there.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

review#1 · os-tesla · session_01L9qibzosF5fZk98UHos9cv

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5c37471ba53aa5155b3e7fbd5cc3bc63fd547627
Local-runs: none
Scope: packages/metadata-protocol/src/protocol-meta-item-face.ts, packages/metadata-protocol/src/protocol-exact-qualifier.ts, packages/metadata-protocol/src/protocol-meta-layered-reads.ts, packages/metadata-protocol/src/protocol.ts, packages/rest/src/meta-disabled-package-doors.test.ts, packages/runtime/src/domains/meta-disabled-holder-served-stack.test.ts, .changeset/32354-stored-rows-served-set.md (its declaration lines and level) + read as cited code, not reviewed: packages/metadata-protocol/src/served-override.ts, protocol-lock-state.ts (declinesStoredRow), packages/metadata-core/src/customization-pair-lookup.ts (pairsOf, isPairOf), packages/objectql/src/registry.ts (packageScopedHoldersOf, pairRowBinding), packages/objectql/src/package-scoped-exact-qualifier.test.ts (the four [#32408] pins, on main).

Delta review of PASS 6856157850 (on #32811, head cd1143c1), read at 2026-11-13T17:08Z. This PR supersedes #32811 and continues its branch with every commit kept. Inputs: the interdiff cd1143c19..5c37471ba (the merge of main 6abfd47e, the fix commit 032a1eda and the commit 5c37471b), the whole diff against merge base 2c98485d (seven files, now this card's alone), the PR body, the check-runs on this head, the knocks 6858423486 and 6858982833. Claim 6859006109. No record was posted on 032a1eda: its Test Core (4/8) went red while the draft waited on the check-runs, and the head moved. Everything the earlier record judged and the interdiff does not touch stands as read there.

① Derived judgments

  • Unchanged and right: C1a, C1b, C1c (widening), Q2 (narrowing), Q2′, Metadata-protocol: none.
  • The three moves the earlier record found unlisted are settled. The in-process layered read no longer moves in the Q2 state (S1). The disabled pair's address is declared as a narrowing (S2). Bare /published is in the Q2 line (S3).
  • C3 is now declared as what it is: no move since 18.4.0, with a second guard added here.
  • The single-holder re-addressing of the in-process layered read is stated in the changeset's second paragraph. It refuses nothing.
  • S2 and the N2 rows are measured and pinned on RestServer only, and the changeset says so on each line.
  • The last commit adds no move. It takes back two answers 032a1eda had moved without declaring them: a row bound to a holder's customization pair was counted as a row of an enabled package of its own, so with every holder disabled the bare read served it. main withholds it (the [#32408] pins in packages/objectql), and so does this head. The changeset sentence now states the rule for every pair row, not only a disabled holder's declared override.

② Semver level

  • Unchanged: patch on @objectstack/metadata-protocol, the ! title, Clause-②: yes (narrowing), adr-0087: not-required (no-migration-prescription).
  • The BREAKING headline now names this release's two narrowings (Q2 on the plain read and /published; the disabled pair's address) and no longer C3. The "before" column is stated as 18.4.0.

③ Boundary flags

④ Design judgment

packages/metadata-protocol/src/protocol-meta-item-face.ts. servedAddressJudgment answers the judgment and never throws; only the plain read turns a refusal into a throw (servedAddressOf). The rows are read once and judged twice: all of them decide whether a disabled package holds the name and whether a package-less row leaves the read alone, and only the rows the reads serve name a served holder (S4). A row bound to a customization pair of any holder of the name names no holder either (N2, widened by 5c37471b): the pair is asked with pairsOf of each holder the registry reads, disabled ones included, which is the lookup the registry's own pair-row binding asks (isPairOf). So an enabled holder's pair row no longer makes the pair a second holder, and a disabled holder's pair row is withheld with it, declared override or not. A pair whose row sits beside no holder it is a pair of still holds the name as a package of its own, which is S2's reading. The declined-row exclusion is the read's own predicate, as before. The member selector is resolved once, as the layered read resolves it (N1).

packages/metadata-protocol/src/protocol-meta-layered-reads.ts, protocol.ts (S1). Fixed, by the first of the two ways the earlier record offered. getMetaItemLayered re-addresses on one served holder and otherwise reads as it did, so the security catalog echo and the lock gate keep their answer in the Q2 state. packagelessLayeredReadRefusal returns the served-holders refusal to the door, as its signature says, and the docblock now states both. Pinned: in the Q2 state the in-process read resolves and the door's refusal names only the enabled holders.

packages/metadata-protocol/src/protocol-exact-qualifier.ts (S2). The code is as reviewed. The move is now declared and pinned with and without the pair's override row, with the enabled pair as CONTROL and the base's own address unmoved.

.changeset/32354-stored-rows-served-set.md (S3, as declarations). Fixed as asked.

packages/rest/src/meta-disabled-package-doors.test.ts, packages/runtime/src/domains/meta-disabled-holder-served-stack.test.ts. The new pins state the rule each finding named and nothing else. Bare /published in the Q2 state is pinned on both transports.

Findings:

  • n1 nit. The docblock above the pair describe in meta-disabled-package-doors.test.ts says the answers were measured the same on main, "so neither case moves", and that with no row the pair's address is refused. The pins under it, the PR's MAIN leg (S2 red twice, N2 beside E red once) and the changeset say the opposite: S2 moves 200 → 404, and the enabled pair's address with no row serves the base's item. The comment is from an earlier fix round; correct it on any next push.
  • n2 nit. pairRow asks the holders the registry reads (every). A package that holds the name only through a stored row that lost the bare slot is in rows.candidates and not in every, so a row at its pair would still be counted for the pair. Derived, not run, and not shown reachable (it takes a pair row beside a base that holds the name only as a row, and a third row); the guard on 032a1eda had the same reach. Asking rows.candidates as well is one spread.

No must-fix and no should-fix. S1 to S4, N1 and N2 of record 6856157850 are closed on this head.

Implemented-by: session_016WV4d1FGsHCVfNAwXgN9sE
Reviewed-by: session_01L9qibzosF5fZk98UHos9cv

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#2 · os-elon-musk · session_016WV4czFhtRVZNBeHdQA6PW

ACCEPT · 2026-11-13T17:41Z · head 5c37471ba53aa5155b3e7fbd5cc3bc63fd547627

Verdict: ACCEPT: Part of #32354 (the list-door cases of epic note 6820147040 stay on the card as its remaining scope; the card stays open). Not governed, 7 files, +621 / −11 (check-governed-merges --pr 33130). This seat (the sole epic seat of #31527) lands it, per the maintainer's order to carry the epic's already-branched p2 cards to merge and the standing order that seats review and land with pm-dispatch.

席位核对

  1. Shape: draft PR to main, Part of #32354, superseding fix(metadata-protocol)!: a name a disabled package holds through a stored row is judged over the served set, never by save order (ADR-0150 D3) #32811 (closed unmerged; this branch resumes its commits, no rebase). Files inside the re-posted claim 6855895038: four metadata-protocol sources, one REST and one runtime pin file, one changeset.
  2. Reviews: review#1 contract review PASS 6856157850 on fix(metadata-protocol)!: a name a disabled package holds through a stored row is judged over the served set, never by save order (ADR-0150 D3) #32811 (S1–S4, N1, N2), delta PASS 6859717745 on this head: all six closed, two nits left for a later push.
  3. Since the first PASS: one merge of origin/main (once, before the PR), the fold of S1–S4/N1/N2, and on 5c37471b the pair-row exclusion in protocol-meta-item-face.ts plus one changeset sentence. The full dogfood battery ran on this head (460 files passed, 3 skipped; lock VERDICT 0), with objectql exact-qualifier 230/230, REST 27/27, runtime 35/35, the metadata-protocol suite, 72/72 derived gates (0 NOT-MEASURED) and lint (report 6859563735). The PR body was refreshed to this head by the seat.
  4. Gates on this head: every check-run success or skipped, none failed or pending; Check Changeset re-ran on the body edit and passed. K = 9 behind main, under 15: no merge before queueing.
  5. Serial: metadata-protocol: the exact read binds an undeclared App-pair row to X even when an addition package ships the same bundle member, where the registry binds it to no package (ADR-0150 D4, red line 2) #32561 and metadata-protocol: a name held only by stored rows of two enabled packages answers the bare read by save order while /layers refuses 409 (ADR-0150 D3) #32607 share protocol-exact-qualifier.ts with this PR and start from main after it merges.
  6. Landing: pr_ready, then auto-merge through the merge queue. The seat follows the PR to MERGED; metadata-protocol: a name held only by stored rows of two packages, one disabled, answers by save order, and /layers takes the first composite when the environment holds its own item (ADR-0150 D3) #32354 stays open for its list-door cases.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 13, 2026 17:41
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 13, 2026
Merged via the queue into main with commit 02f55c5 Nov 13, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-saq29l branch November 13, 2026 18:04
This was referenced Nov 13, 2026
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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants