Skip to content

Commit 85f77e9

Browse files
refactor(metadata-protocol): move the metadata list reads, the audit read and the UI-view read through the protocol host (#25467 E2 cut 1a) (#29548)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9UmbUGPtw5rdRcd3UpYXs5N)_ Part of #25467 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:把 `protocol.ts` 里“元数据读取”一节的前半(列表读取 `getMetaItems` 及其两种变体、共享的 `readFlattenedMetaItems`,外加 `auditMetaItem`、`getUiView`)的方法体,经 E1 的宿主机制原样搬进两个新模块;6 个方法仍留在类上(签名与文档不变),方法体变成一行转发。 - **为什么改**:#25467 裁决 E 的 E2 第 1 节(按席位裁定拆成 1a/1b,每个 PR 1,000–3,000 行),在声明的 `protocol.ts` 冻结日落地;1b(单项读取、分层读取)随后另开。 - **风险与回滚**:低。只做搬移:工具逐个证明 6 个方法体与原方法体在“`this.` 读作 `host.`”之外 AST 与文本全同;对外 `index.d.ts`(ESM 与 CJS)与 main 构建出的逐字节相同,`getMetaTypes()` 的推断类型块也逐字节相同。回滚即整体 revert。 - **先看**:`packages/metadata-protocol/src/protocol-meta-list-reads.ts` 文件头;`protocol.ts` 里 6 个方法的转发体与类 `static {}` 块新增的宿主条目;`protocol.lookup-artifact-item-call-sites.test.ts` 的改动。 - **两分钟验证**:`cd packages/metadata-protocol && node scripts/host-split.mjs check origin/main readFlattenedMetaItems protocol-meta-list-reads.ts`,预期 `"ok":true,"textIdentical":true`。 - **需要你做的**:无(约 2,500 行,低于 3,000 行线,席位自审)。 ## What changes Step **E2, section 1 "metadata reads", part 1a** of ruling E (6537939895), on the `protocol.ts` freeze day seat 5 declared (#28677). Seat 5 split section 1 into 1a and 1b to keep each PR within ruling E's band of 1,000 to 3,000 lines. This is a pure application of the E1 host pattern: no logic changes, and no new rewrite kind. | member | moved to | rewrite sites (all `this.x` to `host.x`) | |---|---|---:| | `getMetaItems` | `protocol-meta-list-reads.ts` | 1 | | `getMetaItemsForExecution` | `protocol-meta-list-reads.ts` | 1 | | `getMetaItemsPerPackage` | `protocol-meta-list-reads.ts` | 1 | | `readFlattenedMetaItems` (809 lines) | `protocol-meta-list-reads.ts` | 44 | | `auditMetaItem` | `protocol-meta-audit-ui-view.ts` | 3 | | `getUiView` | `protocol-meta-audit-ui-view.ts` | 1 | Each method keeps its docblock, modifiers and signature on the class; its body becomes `return fn(protocolHost(this), …)`. `ProtocolHost` and the class's static block gain 26 entries (the members those bodies read; `engine`, `environmentId` and `getServicesRegistry` are read-only getters, the rest are call-time closures). `node scripts/host-split.mjs check origin/main MEMBER MODULE` reads `ok: true, textIdentical: true` for all six. Negative control, with `layer === 'effective'` changed to `!==` in `readFlattenedMetaItems`'s moved body, then restored: ``` PROBLEM protocol-meta-list-reads.ts:85: ExclamationEqualsEqualsToken where the base has EqualsEqualsEqualsToken (`===`) PROBLEM protocol-meta-list-reads.ts: readFlattenedMetaItems's body text differs from the base body rewritten and re-indented, first at body line 3 ``` `protocol.ts` drops seven import names only the moved bodies used (`ITEM_KEY_DISCRIMINATORS`, `decorateMetadataItems`, `soleShippingPackage`, `isActionWithheldByDisabledPackage`, `viewIdentityPatch`, `perPackageLayerEnvelope`, `storedEntryRow`); each module imports what its bodies read from where `protocol.ts` imports it. No import comment was dropped: the one attached comment (`[#7774]`) also covers `itemDiscriminator`, which stays. **The tool (`scripts/host-split.mjs`).** Measuring this section showed the survey misread two stub shapes, so its E1 note of "9 local named hosts" was wrong. `stubKindOf` now recognises: - the per-member series' generic spelling, a parenthesised `fn` with an explicit type argument, then `.call(host, …)`; - a plain `return fn(…)` passing exactly its own parameters, which is a body that read no member and moved earlier. `move` now refuses a member that is already a stub. Before this, it would have "moved" `throwMetadataServiceUnavailable` and `namesWithOwnStoredRow`, which are already plain stubs. The survey now reads 174 stubs and 4 refusals. Two of the refusals are mixed stubs (`packagelessOverlayWriteRefusal`, `namedBaseOverlayWriteRefusal`), and the other two are `reconcileViewExpansions` (a private static field) and `publishPackageDrafts` (a bare `this`); none of the four is in section 1. Three new tests cover this, and `host-split.test.mjs` reads 38 tests. Ablation, deleting the instantiation-expression unwrap, then restored: 2 failed (`expected undefined to be 'call'`; `refuses to move stubbedGeneric again`). ## Forced test edits Two by-path pins now read through the split. What each asserts is unchanged. `src/protocol.lookup-artifact-item-call-sites.test.ts`: - The `readFlattenedMetaItems` site row carries `file: 'protocol-meta-list-reads.ts'`, where its body now stands. - The member scanner names the class's `static {}` block, and a row records the host factory's `lookupArtifactItem` closure there. It has disposition `no`, because it forwards the moved bodies' calls unchanged. This is the same reasoning as cut 15's `shippedArtifactsOf` `...a` row. - "Both read doors take their envelope from one rule" reads the list read's `const itemPackageId = envelopePackageId(packageId, it);` in `protocol-meta-list-reads.ts`. It also requires that module to import `envelopePackageId` from `protocol-package-bindings.ts` and to define no copy. Ablation, with that line changed to `const itemPackageId = packageId;` in the module, then restored: `Tests 1 failed | 5 passed (6)`, `expected '// Copyright …' to contain 'const itemPackageId = envelopePackage…'`. `src/protocol.hydrate-overlay-canonical-type.test.ts` (#9111's closed caller set of `hydrateOverlayIntoRegistry`): the full suite found it red after the move (`expected [ …(2) ] to have a length of 3 but got 2`), because one of the three calls now stands in `readFlattenedMetaItems`'s moved body as `host.hydrateOverlayIntoRegistry(`. The pin now counts `this.` calls in protocol.ts plus `host.` calls in every `protocol-*.ts` module, still exactly 3, and requires that at least one is read in a module. Ablation, planting a fourth `host.hydrateOverlayIntoRegistry(` call in `protocol-meta-audit-ui-view.ts`, then restored: `Tests 1 failed | 10 passed (11)`, `expected [ …(4) ] to have a length of 3 but got 4`. A bare reference that is not a call (`void host.hydrateOverlayIntoRegistry`) does not count: 11 passed. The read-verb pin (`protocol.read-verb-canonical-fold.test.ts`) and the read-seam census needed no edit: E1's resolution follows the new stubs. Read-verb ablation, dropping the fold from `getMetaItemsForExecution`'s moved body, then restored: `Tests 1 failed | 17 passed (18)`, `expected [ 'getMetaItemsForExecution' ] to deeply equal []`. The census reads 100 seams, the same as main 399df5b (read in a scratch worktree of main). ## The published `.d.ts` Built from main 399df5b (this branch's merge of main) and from this head. `index.d.ts` is byte-identical: raw sha256 `450c9334…5d744e` on both sides, ESM equal to CJS, normaliser 2 `8d160c2b…78065` on both. The raw diff of `getMetaTypes()`'s emitted block (793 lines, its return type still inferred per the seat's E1 ruling) is empty. The declaration-emit comment leak into that inferred type struck once after the cut: the `[#23316]` docblock printed inside `key?`. It was cleared the way E0 and E1 cleared it, by the length of the new import comment above the leaking offset. A probe of +1 to +8 characters read raw-equal at +3 to +6; the comment carries +5 (the word "also"). ## Serial Re-read just before opening against main 390bd2a (6 commits past this branch's merge of main, none in `packages/metadata-protocol`; `git merge-tree` with main is clean). Open PRs whose own diff touches `protocol.ts` or a `protocol-*.ts` module: - #29456 (`protocol.ts` and `protocol-credential-carry-forward.ts`): its hunks sit at main lines 355 (the import list), 3739 to 3742 (`carryForwardRedactedCredentials`) and 17167 onward (`saveMetaItem`), all outside the moved section (main lines about 3851 to 7057). `git merge-tree` with this head is clean. - #29437 (`protocol-save-door-refusals.ts` only): `git merge-tree` with this head is clean. Seat 3's #29390 work (branch `claude/project-thread-gvqm55`, head d776e25, no PR yet): its hunks sit at main lines 22104 and 22457 (the restore limb), outside the moved section. `git merge-tree` with this head is clean. ## Gates On ec53149: `node scripts/pm/dispatch-gates.mjs --commands` derived 62 commands, and each records `exit 0`. `--ran` reports 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN. The package's typecheck and test, through os-verify-lock, read Test Files 307 passed, 4 skipped; Tests 22232 passed, 26 skipped. The 11 direct dependents' typecheck exits 0. Lint (`eslint packages/metadata-protocol --no-inline-config`) exits 0. CI is not read. --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmR4MjTzeUv5x6tNkbk)_ <!-- signed: domain:server#5 · os-steve · session_01ADHkmR4MjTzeUv5x6tNkbk --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ebbb5b8 commit 85f77e9

9 files changed

Lines changed: 1385 additions & 1126 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
refactor(metadata-protocol): the metadata list reads, the audit read and the UI-view read move out of `protocol.ts` through the protocol host
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
No behaviour change. The bodies of `getMetaItems`, `getMetaItemsForExecution`, `getMetaItemsPerPackage`, `readFlattenedMetaItems`, `auditMetaItem` and `getUiView` now live in `protocol-meta-list-reads.ts` and `protocol-meta-audit-ui-view.ts`, reached through the per-instance host (`protocol-host.ts`). Each method keeps its docblock and signature on `ObjectStackProtocolImplementation`, and each moved body differs from the method body only by `this.` reading `host.`. The published `index.d.ts` (ESM and CJS) is byte-identical to the one `main` builds before this change. Nothing to migrate.

‎packages/metadata-protocol/scripts/host-split.mjs‎

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,8 @@ export function planMove(source, member, moduleFile, className = CLASS_NAME) {
304304
if (!cls) return { refusals: [{ line: 0, reason: `no class ${className}` }] };
305305
const method = methodOf(cls, member);
306306
if (!method) return { refusals: [{ line: 0, reason: `no instance method ${member} with a body` }] };
307+
const already = stubKindOf(method);
308+
if (already) return { refusals: [{ line: 0, reason: `already a stub ('${already}'): its body moved before` }] };
307309
const table = memberTable(cls);
308310
const { sites, refusals, used } = rewriteSites(sf, method, table, className);
309311
const sig = signatureParts(sf, method);
@@ -526,6 +528,41 @@ export function checkMove({ baseSource, headSource, moduleSource, moduleFile, me
526528

527529
// ── Survey ──────────────────────────────────────────────────────────────────
528530

531+
/**
532+
* The function a per-member stub calls: `fn` in `fn.call(host, …)`, and in the
533+
* generic spelling `(fn<R>).call(host, …)` (an instantiation expression in
534+
* parentheses), which the per-member series wrote for a generic method.
535+
*/
536+
function calleeOf(node) {
537+
let n = node;
538+
while (ts.isParenthesizedExpression(n) || ts.isExpressionWithTypeArguments(n)) n = n.expression;
539+
return n;
540+
}
541+
542+
/**
543+
* Is this method already a stub — its body moved before? One of three shapes,
544+
* its last statement returning (awaited or not):
545+
* 'call' — `fn.call(host, …)` or `(fn<R>).call(host, …)` (the per-member series);
546+
* 'host' — `fn(protocolHost(this), …)` (this tool);
547+
* 'plain' — `fn(<its own parameters, in order>)`, `fn` named as the method:
548+
* a body that read no member moved as a plain function.
549+
*/
550+
export function stubKindOf(m) {
551+
const last = m.body?.statements[m.body.statements.length - 1];
552+
let call = last && ts.isReturnStatement(last) ? last.expression : undefined;
553+
if (call && ts.isAwaitExpression(call)) call = call.expression;
554+
if (!call || !ts.isCallExpression(call)) return undefined;
555+
if (ts.isPropertyAccessExpression(call.expression) && call.expression.name.text === 'call'
556+
&& ts.isIdentifier(calleeOf(call.expression.expression)) && !!call.arguments[0] && ts.isIdentifier(call.arguments[0])) return 'call';
557+
if (call.arguments[0] && ts.isCallExpression(call.arguments[0]) && ts.isIdentifier(call.arguments[0].expression)
558+
&& call.arguments[0].expression.text === HOST_OF) return 'host';
559+
const name = nameOf(m);
560+
const params = m.parameters.map((p) => (ts.isIdentifier(p.name) ? p.name.text : undefined));
561+
if (ts.isIdentifier(call.expression) && call.expression.text === name && call.arguments.length === params.length
562+
&& call.arguments.every((x, i) => ts.isIdentifier(x) && x.text === params[i])) return 'plain';
563+
return undefined;
564+
}
565+
529566
/** Every instance method with a body: its line count, sites and refusals under the set. */
530567
export function survey(source, className = CLASS_NAME) {
531568
const sf = parse('protocol.ts', source);
@@ -536,14 +573,7 @@ export function survey(source, className = CLASS_NAME) {
536573
if (!ts.isMethodDeclaration(m) || !m.body || hasModifier(m, ts.SyntaxKind.StaticKeyword)) continue;
537574
const name = nameOf(m);
538575
const { sites, refusals } = rewriteSites(sf, m, table, className);
539-
const last = m.body.statements[m.body.statements.length - 1];
540-
let call = last && ts.isReturnStatement(last) ? last.expression : undefined;
541-
if (call && ts.isAwaitExpression(call)) call = call.expression;
542-
const stub = !!call && ts.isCallExpression(call) && ts.isPropertyAccessExpression(call.expression)
543-
&& call.expression.name.text === 'call' && ts.isIdentifier(call.expression.expression)
544-
&& !!call.arguments[0] && ts.isIdentifier(call.arguments[0]) ? 'call'
545-
: !!call && ts.isCallExpression(call) && call.arguments[0] && ts.isCallExpression(call.arguments[0])
546-
&& ts.isIdentifier(call.arguments[0].expression) && call.arguments[0].expression.text === HOST_OF ? 'host' : undefined;
576+
const stub = stubKindOf(m);
547577
const lines = sf.getLineAndCharacterOfPosition(m.getEnd()).line - sf.getLineAndCharacterOfPosition(m.getStart(sf)).line + 1;
548578
rows.push({ name, lines, sites: sites.length, refusals, stub });
549579
}
@@ -593,7 +623,7 @@ function main(argv) {
593623
const all = survey(readFileSync(PROTOCOL, 'utf8'));
594624
const { clashes } = all;
595625
const stubs = all.rows.filter((r) => r.stub);
596-
console.log(`already stubs: ${stubs.length} (${stubs.filter((r) => r.stub === 'call').length} by \`fn.call(host, …)\` from the per-member series, ${stubs.filter((r) => r.stub === 'host').length} by \`fn(${HOST_OF}(this), …)\`)`);
626+
console.log(`already stubs: ${stubs.length} (${stubs.filter((r) => r.stub === 'call').length} by \`fn.call(host, …)\` from the per-member series, ${stubs.filter((r) => r.stub === 'host').length} by \`fn(${HOST_OF}(this), …)\`, ${stubs.filter((r) => r.stub === 'plain').length} by a plain \`fn(<params>)\`)`);
597627
const rows = all.rows.filter((r) => !r.stub);
598628
const movable = rows.filter((r) => !r.refusals.length);
599629
const reasons = new Map();

‎packages/metadata-protocol/scripts/host-split.test.mjs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ line two \${k}\`;
6060
destructured({ a }: { a: number }) { return a + this.count; }
6161
hostAsPropertyName() { return { host: this.count }.host; }
6262
stubbed(k: string) { const self = { count: this.count }; return stubbedBody.call(self, k); }
63+
private plainStub(k: string, n: number) { return plainStub(k, n); }
64+
stubbedGeneric<R>(k: R) { const host = { count: this.count }; return (stubbedGenericBody<R>).call(host, k); }
6365
}
6466
`;
6567

@@ -150,6 +152,11 @@ describe('host-split: the move', () => {
150152
expect(plan.fnText).toBeUndefined();
151153
});
152154

155+
it.each(['stubbed', 'stubbedGeneric', 'plainStub'])('refuses to move %s again: it is already a stub', (member) => {
156+
const plan = planMove(BASE, member, 'protocol-target.ts');
157+
expect(plan.refusals.map((r) => r.reason).join(' | ')).toContain('already a stub');
158+
});
159+
153160
it('does not refuse `host` as a property name', () => {
154161
expect(planMove(BASE, 'hostAsPropertyName', 'protocol-target.ts').refusals).toEqual([]);
155162
});
@@ -159,6 +166,11 @@ describe('host-split: the move', () => {
159166
const by = Object.fromEntries(rows.map((r) => [r.name, r]));
160167
expect(clashes).toEqual([]);
161168
expect(by.stubbed.stub).toBe('call');
169+
// The per-member series' generic spelling `(fn<R>).call(host, …)` is a stub too,
170+
// not a body that binds `host`.
171+
expect(by.stubbedGeneric.stub).toBe('call');
172+
// A body that read no member and moved as a plain function: `return fn(<params>)`.
173+
expect(by.plainStub.stub).toBe('plain');
162174
expect(by.target.refusals).toEqual([]);
163175
expect(by.bareThis.refusals).toHaveLength(1);
164176
const { head } = move();

‎packages/metadata-protocol/src/protocol-host.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,36 @@ import type { ObjectStackProtocolImplementation } from './protocol.js';
5454
* the class's static block.
5555
*/
5656
export interface ProtocolHost {
57+
readonly artifactLockLayerAt: ObjectStackProtocolImplementation['artifactLockLayerAt'];
58+
readonly baseServesBundleMember: ObjectStackProtocolImplementation['baseServesBundleMember'];
59+
readonly convertStoredItem: ObjectStackProtocolImplementation['convertStoredItem'];
60+
readonly declinesStoredRow: ObjectStackProtocolImplementation['declinesStoredRow'];
61+
readonly disabledActionsOf: ObjectStackProtocolImplementation['disabledActionsOf'];
62+
readonly engine: ObjectStackProtocolImplementation['engine'];
63+
readonly environmentId: ObjectStackProtocolImplementation['environmentId'];
64+
readonly expandStoredViewContainers: ObjectStackProtocolImplementation['expandStoredViewContainers'];
65+
readonly findEnvironmentRows: ObjectStackProtocolImplementation['findEnvironmentRows'];
66+
readonly flowRegistrationsOf: ObjectStackProtocolImplementation['flowRegistrationsOf'];
67+
readonly foldObjectExtendersFromRegistry: ObjectStackProtocolImplementation['foldObjectExtendersFromRegistry'];
68+
readonly getServicesRegistry: ObjectStackProtocolImplementation['getServicesRegistry'];
69+
readonly governServedObject: ObjectStackProtocolImplementation['governServedObject'];
70+
readonly hydrateOverlayIntoRegistry: ObjectStackProtocolImplementation['hydrateOverlayIntoRegistry'];
71+
readonly isStoredEntryOfDeclinedName: ObjectStackProtocolImplementation['isStoredEntryOfDeclinedName'];
72+
readonly isUnservedSealedOverlay: ObjectStackProtocolImplementation['isUnservedSealedOverlay'];
73+
readonly isWritablePackage: ObjectStackProtocolImplementation['isWritablePackage'];
74+
readonly lookupArtifactItem: ObjectStackProtocolImplementation['lookupArtifactItem'];
75+
readonly namesWithOwnStoredRow: ObjectStackProtocolImplementation['namesWithOwnStoredRow'];
5776
readonly overrideWriteVerdict: ObjectStackProtocolImplementation['overrideWriteVerdict'];
5877
readonly packagedArtifactOwner: ObjectStackProtocolImplementation['packagedArtifactOwner'];
5978
readonly packagedBaseRefusal: ObjectStackProtocolImplementation['packagedBaseRefusal'];
79+
readonly readActiveOverlayRows: ObjectStackProtocolImplementation['readActiveOverlayRows'];
80+
readonly readFlattenedMetaItems: ObjectStackProtocolImplementation['readFlattenedMetaItems'];
81+
readonly rethrowUnlessMetadataStoreUnprovisioned: ObjectStackProtocolImplementation['rethrowUnlessMetadataStoreUnprovisioned'];
82+
readonly shippedArtifactsOf: ObjectStackProtocolImplementation['shippedArtifactsOf'];
83+
readonly shippingPackagesOf: ObjectStackProtocolImplementation['shippingPackagesOf'];
84+
readonly shipsBundleName: ObjectStackProtocolImplementation['shipsBundleName'];
6085
readonly storedFlowBindingAgrees: ObjectStackProtocolImplementation['storedFlowBindingAgrees'];
86+
readonly storedOverlayEntries: ObjectStackProtocolImplementation['storedOverlayEntries'];
6187
readonly uninstalledNamedBaseRefusal: ObjectStackProtocolImplementation['uninstalledNamedBaseRefusal'];
6288
}
6389

0 commit comments

Comments
 (0)