Skip to content

refactor(metadata-protocol): add the protocol host split machinery and move one pilot member through it (#25467 E1) - #29488

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-xniw22
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-xniw22

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Part of #25467

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:为 protocol.ts 的类体拆分搭好“宿主”机制:新增 protocol-host.ts(被搬出的方法体经它访问类成员)和工具 scripts/host-split.mjs(只允许三种封闭改写,并逐个 token 校验搬出的方法体与原方法体一致);只搬了一个试点方法 tenantAuthoredWriteRefusal,没有成段搬移。
  • 为什么改:[maintainer] refactor(metadata-protocol): split src/protocol.ts (33,065 lines, +10,169 in 30 days) along its ObjectStackProtocolImplementation seams, one move-only slice per PR #25467 裁决 E 的第二步 E1:先把搬移规则、校验工具、两个按路径 pin 和读缝普查做成能穿过宿主的形态,E2 才能成段搬移而不让守卫失明。
  • 风险与回滚:低。没有行为改动;试点方法签名和文档不变,对外 index.d.ts(ESM 与 CJS)与 main 构建出的逐字节相同。回滚即整体 revert。
  • 先看:packages/metadata-protocol/src/protocol-host.ts;packages/metadata-protocol/scripts/host-split.mjs 文件头(封闭改写集);protocol.ts 里类的 static {} 块。
  • 两分钟验证:pnpm --filter @objectstack/metadata-protocol exec vitest run scripts/host-split.test.mjs src/protocol.read-verb-canonical-fold.test.ts src/protocol.lookup-artifact-item-call-sites.test.ts,预期 3 个文件全绿。
  • 需要你做的:无需审批级别操作(约 1,530 行,低于 3,000 行线)。getMetaTypes() 显式返回类型已由席位裁定移出 E1(保持推断,.d.ts 逐字节不变),另立小步处理。

What changes

Step E1 of ruling E (6537939895, item E1), claim 6578609040. The host split machinery, one pilot member, no section moves.

The host. src/protocol-host.ts declares ProtocolHost: every class member a moved body reads, each typed AS that member (ObjectStackProtocolImplementation['x'], or (typeof ObjectStackProtocolImplementation)['x'] for a static). The class builds the host in a static {} block, the one place that can read its private members; a static block adds no member, so the published .d.ts does not move. A method is forwarded as a closure called at call time (a spy or test double on the instance is still the one a moved body reaches), a field as a getter (plus a setter when written), a private static as a closure to the static. protocolHost(this) builds one host per instance and caches it in a WeakMap.

The closed rewrite set and its check. packages/metadata-protocol/scripts/host-split.mjs (a tool, not a gate) has three commands: survey, move MEMBER MODULE and check BASE_REF MEMBER MODULE. Inside a moved body exactly three rewrites are allowed:

  1. this.x becomes host.x (an instance member);
  2. a private static ObjectStackProtocolImplementation.fn(…) becomes host.fn(…);
  3. this.constructor becomes host.

check proves AST identity modulo exactly that set by walking every token (operators and keywords included), and also proves the body text, comments included, equals the base body rewritten and re-indented; it checks the stub, the method's unchanged signature, the function's parameters and return type, and that the Pick names exactly the members read. Everything outside the set is a refusal: a bare this or this[…], super, the method's arguments, an identifier already named host, a private static read as a value or a private static field, this.constructor not followed by a static, an undeclared member, generators and destructured parameters.

scripts/host-split.test.mjs (35 tests, collected by the package's vitest run) pins each rewrite kind, each refusal, and fifteen negative controls on the moved function plus three on the stub: a changed operator (?? to ||, === to !==), const to let, a changed literal, a wrong rewrite target, an unrewritten member or static, an extra statement, a changed comment, a Pick naming one member too many or too few, a renamed host parameter, a retyped parameter, a changed return type, reordered stub arguments, a stub handing over this, and an edited docblock. Writing these tests found one tool bug, fixed here: arguments.length escaped the arguments refusal.

The pilot. tenantAuthoredWriteRefusal (5 rewrite sites, all kind 1) moved to src/protocol-tenant-authored-write.ts. node scripts/host-split.mjs check 838a802bf3 tenantAuthoredWriteRefusal protocol-tenant-authored-write.ts reads ok: true, textIdentical: true. Ablation, with folded.packageId === stamped changed to !== in the module, then restored:

PROBLEM protocol-tenant-authored-write.ts:65: ExclamationEqualsEqualsToken where the base has EqualsEqualsEqualsToken (`===`)
PROBLEM protocol-tenant-authored-write.ts: tenantAuthoredWriteRefusal's body text differs from the base body rewritten and re-indented, first at body line 34

The pins and the census read through the host, for every module. These are the forced test edits:

  • src/protocol.read-verb-canonical-fold.test.ts: delegateBody follows a stub (fn(protocolHost(this), …) or the per-member series' fn.call(host, …)) to the moved function in the module protocol.ts imports it from. A new assertion requires that the pilot is read through the split. Before the edit the pin was red on the pilot (expected [ 'tenantAuthoredWriteRefusal' ] to deeply equal []). Ablation, removing the fold from the pilot module's body (const folded = request;), then restored:
    AssertionError: A `/meta` verb takes a required `type` without folding it at the request boundary. … expected [ 'tenantAuthoredWriteRefusal' ] to deeply equal []
    
  • src/protocol.lookup-artifact-item-call-sites.test.ts: the site scan covers protocol.ts plus every protocol-*.ts module (comments blanked through the shared scripts/js-comment-mask.mjs), and a new test pins that the module list includes the host's and the pilot's. Ablation, planting an unrecorded lookupArtifactItem( call in the pilot module, then restored:
    AssertionError: a site with no recorded disposition: classify it … expected [ Array(1) ] to deeply equal []
    AssertionError: expected 22 to be 21
    
  • Both pins import maskComments from scripts/js-comment-mask.mjs (check:comment-mask-adoption refused a private stripper), so turbo.json's @objectstack/metadata-protocol#test inputs and scripts/cross-package-test-inputs.mjs declare that file (check:cross-package-test-inputs). Two config lines outside the claim's file surface, forced by those gates.
  • scripts/check-durability-degradation-log-level.mjs (HOST_SPLIT): HOST CALL now also accepts host.x(…) where host is the function's first parameter, typed by a host interface declared in the module or imported by name from a split module, with members typed AS the class's instance or static members. STUB accepts fn(protocolHost(this), …). There are 10 new self-test cases (flags, negative controls, BOUND pairs); the self-test reads 75 cases. Ablation, renaming hostParam and hostOf, then restored: 4 failures (the 3 E1 flags plus the deeper BOUND), each expected violation=true seams=1, got violation=false count=0 seams=0. The census reads 100 seams against main's 98. The two new lines are protocol-packageless-overlay-migration.ts's migratePackagelessOverlays() guarding storedPairRow(), a helper that already took its module's own host as a first parameter named host. Both read "no invented answer", so no verdict or ledger row moves. This is recorded in the docblock, as fix(scripts): the read-seam census follows a moved protocol.ts body across its host split (#25467 census extension) #28058 recorded its 93 to 95.

The published .d.ts

Built from main and from this head, twice: before the merge of main (main 838a802, sha256 ef70d5a4…004a8 on both sides) and after it (main 7f277a8, sha256 de90eb4e…ccf88 on both sides, normaliser 2 83d37f67…7e2c0 on both). Each time index.d.ts is byte-identical to main's, and ESM equals CJS. A trial merge of the later main 6af3d29 (which edits protocol.ts again), built and then discarded, also reads byte-identical (0e8c1a2c…ef526 on both sides). The declaration-emit comment leak into getMetaTypes()'s inferred return type struck this diff once: a [#28001] docblock printed inside examples?. It was cleared the way E0 cleared it, by the length of a comment line above the leaking offset (one word in the pilot's import comment); a 1 to 8 character probe read raw-equal at 5 or more.

Deviation the seat ruled: getMetaTypes() explicit return type dropped from E1

E0 carried this item into E1 with the condition "normaliser 2 hashes equal". The inferred return type prints as 793 lines of .d.ts, and an explicit annotation prints as written, so the two meet only if that 793-line literal is pasted inline. Measured: that gives a byte-identical .d.ts and green tsc, but 793 duplicated lines whose drift is silent in the widening direction.

Seat 5 ruled option C: the return type stays inferred, so index.d.ts stays byte-identical (the E-series invariant), and every E2 cut reads the raw .d.ts diff of getMetaTypes(). A named composed type with a type-equality pin against main's .d.ts (option B) becomes its own small step after E1 lands, filed by the seat. The turbo.json and scripts/cross-package-test-inputs.mjs lines above are accepted as forced.

Serial

Re-read just before opening against main 6af3d29 (34 commits past this branch's one merge of main at 7f277a8; git merge-tree with main is clean). Open PRs whose own diff touches packages/metadata-protocol/src/protocol.ts: #28731 only, and git merge-tree with this head is clean. No open PR touches the other files here.

Gates

On 710b8fe: node scripts/pm/dispatch-gates.mjs --commands derived 82 commands, and each records exit 0. --ran reports 82 derived, 82 run, 0 NOT-MEASURED. The first round, on cf94e93's parent, caught the two items above: check:comment-mask-adoption and check:cross-package-test-inputs. check:dual-build-cjs-loads exited 3 there (unbuilt packages); they were built and it now exits 0. The package's typecheck and test, through os-verify-lock, read Test Files 305 passed, 4 skipped; Tests 22191 passed, 26 skipped. The 11 direct dependents' typecheck exits 0. Lint (eslint packages/metadata-protocol plus the three root scripts, --no-inline-config) exits 0. CI is not read.


Generated by Claude Code

… tool with its closed rewrite set check

protocol-host.ts declares ProtocolHost, the one object a body moved out of
ObjectStackProtocolImplementation reaches the class through.
scripts/host-split.mjs moves a member's body under exactly three rewrites
(this.x -> host.x, a private static call -> host.fn(...), this.constructor
-> host) and checks a moved body is the base body modulo that set, by AST
identity over every token and by text. host-split.test.mjs pins the set,
each refusal and fifteen negative controls.

Claude-Session: https://claude.ai/code/session_01ADHkmR4MjTzeUv5x6tNkbk
Co-authored-by: Claude <noreply@anthropic.com>
…hrough the host, and read moved bodies through it

The pilot member's body moves to protocol-tenant-authored-write.ts; the
method keeps its docblock and signature and becomes a stub. The class's
static block builds the host. The read-verb fold pin, the lookupArtifactItem
call-site pin and the read-seam census follow host.x(...) calls and
fn(protocolHost(this), ...) stubs into every split module.

Claude-Session: https://claude.ai/code/session_01ADHkmR4MjTzeUv5x6tNkbk
Co-authored-by: Claude <noreply@anthropic.com>
…shared comment mask

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

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/error-catalog.mdx (via INVALID_METADATA (literal, a string literal in tenantAuthoredWriteRefusal))
  • content/docs/automation/email-templates.mdx (via INVALID_METADATA (literal, a string literal in tenantAuthoredWriteRefusal))
  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/data-modeling/currency.mdx (via INVALID_METADATA (literal, a string literal in tenantAuthoredWriteRefusal))
  • content/docs/data-modeling/external-datasources.mdx (via INVALID_METADATA (literal, a string literal in tenantAuthoredWriteRefusal))
  • content/docs/data-modeling/field-types.mdx (via INVALID_METADATA (literal, a string literal in tenantAuthoredWriteRefusal))
  • content/docs/deployment/install-modes.mdx (via INVALID_METADATA (literal, a string literal in tenantAuthoredWriteRefusal))
  • content/docs/permissions/authorization.mdx (via INVALID_METADATA (literal, a string literal in tenantAuthoredWriteRefusal))
  • content/docs/protocol/kernel/error-handling.mdx (via INVALID_METADATA (literal, a string literal in tenantAuthoredWriteRefusal))
  • content/docs/ui/forms.mdx (via INVALID_METADATA (literal, a string literal in tenantAuthoredWriteRefusal))

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

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), INVALID_METADATA (literal, a string literal in tenantAuthoredWriteRefusal))
  • content/docs/releases/v17/17-5.mdx (via INVALID_METADATA (literal, a string literal in tenantAuthoredWriteRefusal))
  • content/docs/releases/v17/17-6.mdx (via INVALID_METADATA (literal, a string literal in tenantAuthoredWriteRefusal))
  • content/docs/releases/v17/17-7.mdx (via INVALID_METADATA (literal, a string literal in tenantAuthoredWriteRefusal))
  • content/docs/releases/v18/18-0.mdx (via INVALID_METADATA (literal, a string literal in tenantAuthoredWriteRefusal))
  • content/docs/releases/v18/18-1.mdx (via INVALID_METADATA (literal, a string literal in tenantAuthoredWriteRefusal))
  • content/docs/releases/v18/18-2.mdx (via INVALID_METADATA (literal, a string literal in tenantAuthoredWriteRefusal))

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

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 04e0c2c0c797ce5a9a0c926982be449eeb7523ea → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4420febf4869263598b7c68c186f561842734a80 — the merge of head 710b8fe34ef34c431e12595fc7ef91d7ace3f97b into base 04e0c2c0c797ce5a9a0c926982be449eeb7523ea, 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 4420febf4869263598b7c68c186f561842734a80 && git checkout 4420febf4869263598b7c68c186f561842734a80
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 04e0c2c0c797ce5a9a0c926982be449eeb7523ea 710b8fe34ef34c431e12595fc7ef91d7ace3f97b && git checkout -B drift-repro 04e0c2c0c797ce5a9a0c926982be449eeb7523ea && git merge --no-ff 710b8fe34ef34c431e12595fc7ef91d7ace3f97b

node scripts/docs-audit/affected-docs.mjs --json 04e0c2c0c797ce5a9a0c926982be449eeb7523ea

⚠️ 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 04e0c2c0c797ce5a9a0c926982be449eeb7523ea → 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#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

维护者速读

  • 改了什么:protocol.ts 拆分第 E1 步。新增 protocol-host.ts 宿主对象,试点把 tenantAuthoredWriteRefusal 的方法体搬到独立模块,类上的签名和文档注释不动。同时加了搬移工具 scripts/host-split.mjs(survey/move/check),后续 E2 按它成批搬。
  • 为什么改:[maintainer] refactor(metadata-protocol): split src/protocol.ts (33,065 lines, +10,169 in 30 days) along its ObjectStackProtocolImplementation seams, one move-only slice per PR #25467 裁决 E:用一个宿主对象分段搬出 protocol.ts,不再每个桩各建一个 this 字面量。
  • 风险与回滚:无行为变化。发布的 index.d.ts(ESM 与 CJS)与 main 字节一致。回滚即 revert。
  • 先看:packages/metadata-protocol/src/protocol-host.ts、scripts/host-split.mjs 的 check。
  • 两分钟验证:node --test packages/metadata-protocol/scripts/host-split.test.mjs,35 条全绿(含 18 条反例)。
  • 需要你做的:无,席位落地(1,533 行,未过 3000 行线)。

席位意见:与 E1 派发相符。getMetaTypes() 显式返回类型按席裁从 E1 移出(保持推断,d.ts 不变),另立一步。

席位核对

ACCEPT · domain:server seat 5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1 · 2026-11-02T16:22Z · head 710b8fe34e · Part of #25467 (E1, claim 6578609040)

  1. Shape: draft against main, Part of #25467 (the card stays open for E2). A patch changeset for @objectstack/metadata-protocol. Clause-②: no, Metadata-protocol: none. Not governed (check-governed-merges: 11 files clear). 1,533 changed lines, face TIER_DEFAULT.
  2. Against the order: the host is built in the class static block, so the class gains no member. The pilot body differs from the method only by this. reading host., proven by the tool's AST identity check. The read-verb pin, lookup pin and durability census resolve through the host. The census goes from 98 to 100 seams; both new lines are a pre-existing helper, and no verdict moved. index.d.ts is byte-identical to main.
  3. Accepted deviations: getMetaTypes()'s explicit return type is dropped from E1 by seat ruling, and its own step follows. turbo.json and scripts/cross-package-test-inputs.mjs each gain one forced line declaring scripts/js-comment-mask.mjs.
  4. Gates: the dev reports 82/82 derived gates exit 0 (0 NOT-MEASURED), the suite at 22,191 passed, 11 direct dependents typecheck 0, and lint 0. Each pin ablation went red and was restored. The seat readies the PR only when no check-run on this head is red or running.
  5. Out of scope: E2 (the remaining sections). Its order carries the 11 members the tool refuses today.
  6. Landing: pr_ready + automerge_enable through the relay.

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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant