Skip to content

refactor(metadata-protocol): the two overlay-write closes ask one fix-can-be-taken predicate - #29874

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-a7zbyk
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-a7zbyk

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Fixes #29397

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:两个覆盖层写入关闭判定(namedBaseCloseSubject、packagelessCloseSubject)里逐步重复的「修复能否走通」四步(配对 id、ensure 门、基包版本、D2 门放行声明体)收成一个共享判定 overlayPairFixOf + overlayPairFixAdmits,各自只保留本该不同的那部分。
  • 为什么改:两份副本逐步相同,改其中一份时另一份会悄悄漂移;合同评审 S6 要求合并。
  • 风险与回滚:纯搬移,拒绝文案、状态码、调用顺序都不变,两个关闭的既有测试原样通过;回滚即撤销本 PR。
  • 先看:packages/metadata-protocol/src/protocol-packaged-base-refusal.ts(新判定与具名关闭)、packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts(无包关闭,行读取仍夹在两半之间)、packages/metadata-protocol/src/protocol.named-base-overlay-write.test.ts(补的三条控制用例)。
  • 两分钟验证:pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.named-base-overlay-write.test.ts,预期 31/31 全绿,含三条新的「某一步失败则写入保持开放」控制用例。
  • 需要你做的:无,席位落地。

Why

The contract review of #29270 (record 6577747588, should-fix S6) found that the two overlay-write closes judge "can the fix the refusal names be taken" with the same four steps, written twice. A change to one copy could drift from the other with every pin still green. This PR keeps one copy.

What changes

  • One predicate, two halves (protocol-packaged-base-refusal.ts, the module that owns the override write gate and both refusal sentences):
    • overlayPairFixOf(host, base): the registry half. The base has a pair id, the ensure door would answer or create the pair (customizationPairEnsureRefusal), and the base records a manifest.version. It answers { base, pair, basedOn } or null.
    • overlayPairFixAdmits(host, fix, { type, name, body, member }): the gate half. The override write gate admits the declared body (overrides: { package: base, basedOn }) into the pair.
    • It has two halves because the package-less close reads the item's stored rows between them, and only for a name the registry has not already left open. Folding the row read in front would change the call order and which failure wins (a store-read failure before a registry null).
  • namedBaseCloseSubject keeps only its own steps: a named package, a body carrying overrides stays the D2 gate's, the named package among the shippers, and the declared body from the save (no row read).
  • packagelessCloseSubject keeps only its own steps: exactly one shipper, the item's row read, and classifyPackagelessRow.
  • Unchanged: closedOverlayWriteClauses, both refusal sentences, protocol.ts (no line touched; its two host methods and their eight call sites are as on main), and the return shape { base, pair }. The new functions are internal: src/index.ts names its exports, and neither is added there.
  • Changeset: @objectstack/metadata-protocol patch (see Acceptance notes for why it is not skip-changeset).

Measured first, on main at 42ff86f

Every step of the two predicates, common or intended-different (line numbers are main's):

Step namedBaseCloseSubject (protocol-packaged-base-refusal.ts) packagelessCloseSubject (protocol-packageless-overlay-migration.ts) Verdict
Which write :394-395 a non-empty package :729 no package intended-different
overrides in the body :399 left to the D2 gate (classified at :764, foreign_declaration) intended-different (both docblocks say so)
Shippers :400 the named one among them :731-733 exactly one intended-different (the card)
Pair id :401-403 :734-736 common
Registry present :404-405 :737-738 common
Ensure door :406 :739 common
Base version :407-408 reads registry.getPackage(base) :740-741 baseVersionOf re-reads engine.registry common. Same value: ObjectQL#registry is a getter over one never-reassigned field, and getPackage was already proved a function at the step above
Row read and classification none :743-768 intended-different (the card)
Member of the declared body :411-415 keyed on isPlainRecord(item) :754-756 keyed on item !== undefined spelled differently, same answer for every JSON body: for a non-record body both read the canonical member. Left in each close, not folded, so the fold stays move-only
Declared body and gate :416-423 :770-778 common

No step differs by accident, so the card's stop condition did not fire.

Tests

  • No existing pin was edited. protocol.named-base-overlay-write.test.ts gained fixtures (three packages and two shipped dashboards that no existing case addresses) and one new top-level describe. The package-less close's pins (packages/objectql/src/protocol-packageless-overlay-migration.test.ts, environment-overlay-email-locale-restart.test.ts, environment-overlay-five-types.test.ts) were not touched.

  • Step coverage before this PR:

    Step named close package-less close
    pair id none no_pair case
    ensure door a read-only base whose pair cannot be ensured an unensurable pair cases
    base version none none
    gate refuses the declared body none the email_template unshipped-locale reset
  • Three new pins, all through the named close, all green on main before the fold (commit 0dc436c on 42ff86f), and each shown able to fail: a write naming a read-only base whose id forms no pair, a base that records no version while its pair is installed, and an email_template at a locale the base does not ship. Each stays open, as on main. One basedOn pin covers both closes now, because the step lives once.

Acceptance notes

  • The version step is not separately observable today. Delete only the basedOn check and the no-version pin stays green: the gate then sees overrides with no basedOn, which OverrideDeclarationSchema rejects, so the gate judges the body as an undeclared same name in the pair and refuses it. The pin still pins the outcome: a fallback version (?? '0.0.0') reds it (Verification, ablation 4). The check is kept as it was.
  • Changeset, not skip-changeset. No published behaviour changes, but the shipped dist/ of @objectstack/metadata-protocol does (the build carries the new functions). AGENTS.md keeps skip-changeset for a diff that publishes nothing, so this is a patch. The order allowed skip-changeset if no behaviour changed; AGENTS.md wins.
  • The predicate guard registry present with a getPackage function is a type guard reached only by an engine without a registry. It is not one of the card's four steps and is not pinned. That is unchanged.

Verification

Unit tests at ba4aaf1 (the head, main merged in at 3a856f6, after #29797's 5f99582): @objectstack/metadata-protocol typecheck and full suite, and the package-less close's three objectql files against the rebuilt dist
pnpm --workspace-concurrency=2 --filter '@objectstack/metadata-protocol...' build
  VERDICT command-exit 0           (dist/index.js carries overlayPairFixOf)
pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 src/protocol.named-base-overlay-write.test.ts
  Test Files  1 passed (1)
       Tests  31 passed (31)      (25 of this PR, with #29797's six; the three new controls among them)
  VERDICT command-exit 0
pnpm --filter @objectstack/metadata-protocol typecheck && pnpm --filter @objectstack/metadata-protocol test
  Test Files  311 passed | 4 skipped (315)
       Tests  22320 passed | 26 skipped (22346)
  VERDICT command-exit 0
pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/protocol-packageless-overlay-migration.test.ts src/environment-overlay-email-locale-restart.test.ts src/environment-overlay-five-types.test.ts
  Test Files  3 passed (3)
       Tests  105 passed (105)
  VERDICT command-exit 0

Earlier heads: at e1c8a10 the suite read 22314/22314 and the objectql files 105/105; at 408cc35, 22308/22308 and 105/105.

New pins on main first: 25/25 at 0dc436c (the pins on 42ff86f, before the fold)
✓ CONTROLS: every step the fix takes, failing, leaves the write naming the base open (ADR-0146 D1, D11) > a read-only base whose id forms no pair (an id ending in `.custom`) keeps the write naming it: there is no pair to name
✓ … > a read-only base that records no version, its pair installed, keeps the write naming it: there is no `basedOn` to declare
✓ … > an email template at a locale the base does not ship keeps the write naming the base: the override write gate refuses its declared body
Test Files  1 passed (1)
     Tests  25 passed (25)
Ablation on main: four mutations of namedBaseCloseSubject through scripts/ablation-replace.mjs, each landed on disk and was restored to the HEAD blob

The file was protocol-packaged-base-refusal.ts. The blob was 410bf525 before, and it was 410bf525 after every restore, equal to HEAD.

  1. if (!pairId.ok) return null; replaced with a no-op: the no-pair-id pin goes red, 1 failed | 24 passed.
  2. The basedOn check deleted: 25 passed. The step is masked by the gate (see Acceptance notes).
  3. The gate's refuse return deleted: the unshipped-locale pin goes red, 1 failed | 24 passed.
  4. basedOn given the fallback ?? '0.0.0': the no-version pin goes red, 1 failed | 24 passed.
Derived gates at e1c8a10: 59 derived, 58 run green, 1 NOT MEASURED; pnpm lint exit 0. Re-run at ba4aaf1: the ratchet and text families, all exit 0

At ba4aaf1 (the merge brings no change to this PR's four files; git diff --stat origin/main HEAD still reads 4 files, +177 / -53), a declared narrowing to the ratchet and text families: check:type-check-coverage, check:type-check-debt, check:durability-log-level, check:doc-authoring, check:closed-ceremony-text, check:nul-bytes, check:issue-citations, check-empty-changeset.mjs --base origin/main, and eslint (--no-inline-config) on the three changed source files: every one exit 0. CI runs the full set on this head.

The full union, at e1c8a10:

node scripts/pm/dispatch-gates.mjs --commands (change set: the four files above, vs merge base on origin/main), each run with its exit code captured before any pipe, then --ran:

Run reconciliation — 59 derived, 58 run, 1 NOT-MEASURED, 0 UNRUN.
  NOT-MEASURED · DERIVED (1):
    - node scripts/check-plugin-teardown-shape.mjs --self-test   [exit 3 — PREREQUISITE NOT MET]
✓ dispatch-gates --ran: 59 derived famil(ies) accounted for — 58 run, 1 NOT-MEASURED
pnpm lint :: exit 0

NOT MEASURED, reason: that self-test's fixture is pinned to a commit outside this shallow clone (it asks for git fetch --unshallow). CI runs it on a full checkout. Every other derived family exited 0, including check:durability-log-level, check:doc-authoring, check:closed-ceremony-text, check:type-check-coverage and check:type-check-debt.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 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.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JQwxbZz2XXsYuaJz9sPuGU


Generated by Claude Code

…base close where it was untested

Claude-Session: https://claude.ai/code/session_01JQwxbZz2XXsYuaJz9sPuGU
Co-authored-by: Claude <noreply@anthropic.com>
…an-be-taken steps into one predicate

namedBaseCloseSubject and packagelessCloseSubject repeated the same four
steps (the pair id, the ensure door, the base's version, the override write
gate on the declared body). overlayPairFixOf / overlayPairFixAdmits now hold
them once in protocol-packaged-base-refusal.ts; each close keeps only its own
difference (which base, which body, the package-less close's row read
between the two halves). Move-only: no refusal and no call order changes.

Claude-Session: https://claude.ai/code/session_01JQwxbZz2XXsYuaJz9sPuGU
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/metadata-api.mdx (via basedOn (symbol, a field of interface OverlayPairFix))
  • content/docs/concepts/metadata-lifecycle.mdx (via basedOn (symbol, a field of interface OverlayPairFix))
  • content/docs/deployment/cli.mdx (via basedOn (symbol, a field of interface OverlayPairFix))
  • content/docs/deployment/install-modes.mdx (via basedOn (symbol, a field of interface OverlayPairFix))
What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 3a856f680d7df622d3226f0a76e5f3f576a5327b → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 3a856f680d7df622d3226f0a76e5f3f576a5327b

⚠️ 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 3a856f680d7df622d3226f0a76e5f3f576a5327b → 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

domain:server#2 · os-warren · session_01JQwxbXKRzgnVvjzxaYxLUf

维护者速读

席位核对

ACCEPT · domain:server#2 · head ba4aaf11cb445d87aee70203fe00788df34302c7 · 2026-11-03T11:09Z · seat review of record at TIER_DEFAULT (declaration face: Clause-②: no in the changeset, no spec path); not governed (check-governed-merges --pr 29874 0 of 4 paths)

  1. Shape: base main, Fixes #29397, Clause-②: no, Metadata-protocol: none; @objectstack/metadata-protocol patch changeset (the dist ships changed bytes; the order allowed skip-changeset only if nothing published changed, so the dev's choice is right). 4 files, +177/−53.
  2. Seat read: overlayPairFixOf holds the three registry steps (pair id, the ensure door, a base version) and overlayPairFixAdmits the override-write-gate step on the declared body; both closes call them in the old order, the package-less close keeping its stored-row read between the two halves, so call order and the winning refusal are unchanged. closedOverlayWriteClauses, every sentence and protocol.ts untouched; no existing pin edited.
  3. Gates on this head: check-runs pinned to ba4aaf11: 52 total, 37 success, 15 skipped, 0 failed. Dev at ba4aaf11 (after a metadata-protocol build): named-base overlay pins 31/31 (25 from this PR plus fix(metadata-protocol)!: a package commit's plan and revert address the committing package's own row; a deleted row's restore asks the package-less close #29797's 6); metadata-protocol typecheck and test 22,320 pass / 26 skipped; objectql package-less pins 105/105 against the rebuilt dist; declared narrowing gates and eslint on the 3 source files exit 0. Full union as read at e1c8a104: 59 derived, 58 exit 0, 1 NOT MEASURED (teardown self-test needs an unshallow clone); pnpm lint 0. Three new named-close controls (no pair id; no base version with the pair installed; an unshipped email locale), green on main first, each ablated red.
  4. Accepted deviations: two steps answer the same with different spellings (the base-version read; membership of the declared body); the member spelling stays per close so the fold is move-only. The version step alone is masked by the D2 gate (Acceptance notes).
  5. Serial: PR fix(metadata-protocol)!: a package commit's plan and revert address the committing package's own row; a deleted row's restore asks the package-less close #29797 (metadata-protocol: a pair commit's revert beside a pre-close base-bound row answers success while writing nothing, because the commit plan and the restore both read any package's active row #29414, seat 5) merged 10:32Z as 5f99582e; this head merges main 3a856f68 (which contains it) cleanly, no conflict in protocol-packaged-base-refusal.ts, and both closes' pins re-ran green (seat 5's ask). metadata-protocol: the #28905 overlay-write refusal sentence does not name os migrate packageless-overlays, the ceremony that now moves a base-bound row into its pair #29479 (the refusal sentence, same module) is next in line, and stays unopened under the maintainer's stand-down.
  6. Landing: not governed, under 3000 lines ⇒ the seat lands it (pr_ready + automerge_enable). At MERGED: landing record on metadata-protocol: fold the two overlay-write close predicates (namedBaseCloseSubject, packagelessCloseSubject) into one fix-can-be-taken predicate #29397, pm:dispatched removed, card thread released. metadata-protocol: the #28905 overlay-write refusal sentence does not name os migrate packageless-overlays, the ceremony that now moves a base-bound row into its pair #29479 is not opened (stand-down).

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 3, 2026 11:10
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit c55d300 Nov 3, 2026
53 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-a7zbyk branch November 3, 2026 11:31
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

metadata-protocol: fold the two overlay-write close predicates (namedBaseCloseSubject, packagelessCloseSubject) into one fix-can-be-taken predicate

1 participant