Skip to content

Commit b6c2ed9

Browse files
refactor(metadata-protocol): move the bodies of eight members out of protocol.ts into four themed modules (#25467 cut 35) (#28090)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DXvkFFVPCoX5PE3m7xJeyn8)_ Part of #25467 Clause-②: no Metadata-protocol: none Ruling-ref: 6520565826 ## 维护者速读 - 改了什么:把 `protocol.ts` 里 8 个方法的方法体原样搬进 4 个按主题划分的新文件(环境自有行读取、存储集合折叠、包卸载、非托管导入),类上的方法、签名和文档注释都保留,只剩一个调用。`protocol.ts` 从 29,585 行降到 29,114 行。 - 为什么改:#25467 裁决 B 的第二个 PR。#28058 让读缝普查能跟着拆分走,cut 33/34 里只因普查被拒的成员(含 300 行的 `deletePackage`)现在可以搬了。 - 风险与回滚:低。8 个方法体逐 token 相同(3,278 个 token,差异 0);发布的 `.d.ts` 与 `main` 原始字节完全相同;包测试与 `main` 逐条比对 0 丢失 0 变化,只多 2 条新登记行带来的用例;普查仍是 95,只有 7 行换了文件。回滚即还原本 PR。 - 先看:`protocol.ts` 里带 `[#25467 cut 35]` 标记的 8 个桩方法,以及 `protocol.bracketed-refusal-opener-absence.test.ts` 里新增的 2 行 `PRODUCERS` 登记。 - 两分钟验证:`pnpm check:durability-log-level`,预期末行 `95 read seam(s), none invents an unreported answer`;再跑 `pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 src/protocol.bracketed-refusal-opener-absence.test.ts src/protocol.package-delete-refusal.test.ts`,预期全部通过。 - 需要你做的:无,席位落地。 ## What changes PR 2 of 2 under ruling B on #25467. PR 1 (#28058) taught the read-seam census to follow a moved body across the host split; this cut moves the members the census alone rejected at cuts 33 and 34. 8 `ObjectStackProtocolImplementation` members move unedited into 4 new modules under `packages/metadata-protocol/src/` (#23050 shape B): | Module | Members (base line, body lines, AST tokens) | Host | |---|---|---| | `protocol-environment-rows.ts` (161 lines) | `findEnvironmentRows` (`:10240`, 12, 259), `findEnvironmentRow` (`:10261`, 6, 161), `findServedOverlayRow` (`:10470`, 29, 221), `packagelessAddress` (`:17212`, 20, 243) | `EnvironmentRowsHost` | | `protocol-stored-collection-fold.ts` (153) | `foldStoredCollection` (`:7404`, 95, 487) | `StoredCollectionFoldHost` | | `protocol-package-uninstall.ts` (358) | `deletePackage` (`:24914`, 300, 1,107; public) | `PackageUninstallHost` | | `protocol-unmanaged-import.ts` (148) | `unmanagedImportCollisions` (`:26121`, 39, 464), `rollBackUnmanagedImport` (`:26177`, 38, 336) | `UnmanagedImportHost` | Base lines are `protocol.ts` at `main` `2e955dde`. 539 body lines and 3,278 AST tokens in total. `deletePackage` is the one public member; its declared return type is `Promise<DeletePackageResponse>`, so the published signature is unchanged. - **The class keeps every member**: name, modifiers, signature, docblock and position. Each body is now one call into its module, marked `// [#25467 cut 35] The body moved to ...`. - **Shape B, as cuts 20–34.** Each stub builds one `const host: ThisParameterType<typeof fn> = {...}` and returns `fn.call(host, ...)`. Methods are passed as closures over `this`, `engine` and `getServicesRegistry` as getters. Each module has one named host type whose members are typed as the class members. - **Every caller still calls the class method.** No call site changes and no member changes visibility. - `protocol.ts` goes from 29,585 to 29,114 lines. The PR is +895 / −542 over 6 files. ### Screened and kept out - `storedFlowBindingAgrees` and `metaTypeNamespaceExists`: each body's `catch` delegates to `this.rethrowUnlessMetadataStoreUnprovisioned`, and the census's catch-side guard resolution (`establishesBenign`) is same-file only. Moving either turns the census red. #28058 extended the read side only, as the ruling asked. - `rethrowUnlessMetadataStoreUnprovisioned` stays: it is the benign-error discriminator the census reads. The 8 that move pass the other screens on fresh `main`: no private-static read, no `this.constructor` read, no by-path reader of their bodies (the suite and the hand gates below agree), and no open PR edits them. ## Tests One test file changes, the path-reader migration the series requires. `protocol.bracketed-refusal-opener-absence.test.ts` keeps a per-file floor of `new Error(` producers (`PRODUCERS`). Three `new Error(` sites move (two in `deletePackage`, one in `rollBackUnmanagedImport`), so two modules get a row, under a `[#25467 cut 35]` comment, after cut 33's rows: ``` 'protocol-package-uninstall.ts': 2, 'protocol-unmanaged-import.ts': 1, ``` Each row adds one generated case, so the suite grows by 2. The moved members are driven end to end by the existing suites (package uninstall and its refusals, the unmanaged import, the runtime authoring gate, the served overlay reads); all pass at both ends. ## Acceptance notes - **The read-seam census stays at 95.** Seven `--list` lines change file with the same verdict: the three guards in `deletePackage`, the guard in `packagelessAddress`, the two in the unmanaged import, and `foldStoredCollection`'s reported pass-through. Every other line is identical once line numbers are stripped. This is the reading cut 34 lost (93 → 89) and #28058 recovers. - **Four top-level helpers gain `export` in place**: `remainingRowsPhrase`, `servedOverlayRowCandidates`, `mergePackageAwareOverlay` and `metadataReadFailureError`, each `protocol.ts`'s own non-exported function, read by a moved body. Each gains the `export` keyword with nothing else in its statement changed, as cut 33 did. All four keep readers in `protocol.ts`. `index.ts` names its re-exports one by one and names none of them, so the published surface does not move (the `.d.ts` is raw-identical, below). - **No import of `protocol.ts` lost its last reader**, so none is removed. - **`check-adr-anchors` counts move, and the gate passes at both ends.** Citations 61,216 → 61,218 and files 8,748 → 8,752. Decision-letter citations on the anchored surface 1,998 → 1,993: five moved letter citations left `protocol.ts` for modules outside the anchored set. `check-adr-symbol-anchors` output is identical. - **Serial with in-flight PRs.** PR #27964, named by the dispatch, merged before this branch was cut, so its diff is in the base. At `pr_create`, none of the 19 open PRs edits `packages/metadata-protocol/src/` or the census script (each head fetched and diffed against its merge base), so no `git merge-tree` proof is owed. - No changeset: `skip-changeset`, per ruling `6348891160`. ## Verification ### Proof of move-only The compare reads the TypeScript compiler's AST leaf tokens, with JSDoc skipped, over each member's type parameters, parameters (the module's `this` parameter dropped), return type and body, `protocol.ts` at `main` against the head module: ``` findEnvironmentRows 259 tokens differ 0 text equal findEnvironmentRow 161 tokens differ 0 text equal findServedOverlayRow 221 tokens differ 0 text equal packagelessAddress 243 tokens differ 0 text equal foldStoredCollection 487 tokens differ 0 text equal deletePackage 1107 tokens differ 0 text equal unmanagedImportCollisions 464 tokens differ 0 text equal rollBackUnmanagedImport 336 tokens differ 0 text equal TOTAL 3278 differ 0 ``` "text equal" is the whitespace-collapsed body text, comments included. Negative controls, one token changed in an in-memory copy of the head module, each `differ 1`: `deletePackage` `===` → `!==`; `findEnvironmentRow` `null` → `undefined`; `foldStoredCollection` `registered` → `registeredX`; `rollBackUnmanagedImport` `await` → `void`. - **Class side.** 307 members at both ends, same order. The 299 that did not move are byte-identical, leading comments included; the 8 stubs keep their leading trivia and signature byte-identical. - **Top level.** 274 → 278 statements; 269 byte-identical. The rest: the 4 module imports (each with a one-line `[#25467 cut 35]` comment), the four helpers with `export` added, and the class. - `tsc --noEmit` in `packages/metadata-protocol`: exit 0. `pnpm exec eslint --no-inline-config packages/metadata-protocol`: exit 0, no output. ### The published `.d.ts` (ruling `6348891160`) Built with `turbo run build --filter='@objectstack/metadata-protocol...'` at each end, under the shared verify lock. | Reading | `main` `2e955dde` | Head `6b074fce` | |---|---|---| | raw sha256 `dist/index.d.ts` = `dist/index.d.cts` | `00e090f9865173d73ac2d0bf451663790d89a9aabf6ad74fc6470ff2fbf2ae61` (791,803 bytes) | same | | normalised sha256 | `66d4be5eb145c97d09699a67e8d5014ec009fa2838f05c980d492edf00c491cd` | same | | raw-diff lines | | 0 (`cmp` equal) | Bundles change as expected: `index.js` `a11431fcf0a4301d` → `3975040696890377`, `index.cjs` `3826cf554a460505` → `2bd2adad44302e8c` (sha256, 16). The ESM and CJS export tables are the same 78 names (`9f18a0177ac736ed`) at both ends; none of the moved functions or the four exported helpers appears. The normaliser is PR #27540's, verbatim. ### Gates and suites <details><summary>Package suite</summary> `packages/metadata-protocol`, vitest `--maxWorkers=2`, JSON reporter, under the shared verify lock. - `main` `2e955dde`: 21,883 tests (21,857 passed, 0 failed, 26 skipped), 288 files. - Head: 21,885 tests (21,859 passed, 0 failed, 26 skipped), 288 files. Per file, test name and status over 21,870 distinct names against `main`: 0 lost, 0 changed, 2 added, both the new `PRODUCERS` rows. </details> <details><summary>Hand gates, main and head</summary> All 12 exit 0 at both `main` `2e955dde` and the head. - Identical output: `check-adr-symbol-anchors`, `check-system-context-census`, `check-tenant-audit-census`, `check-query-options-erasure-ratchet`, `check-filter-alias-parity`, `check-platform-checklist`. - `check-durability-degradation-log-level --list`: `95 read seam(s)` at both ends; identical once `[@:][0-9]+` is stripped, except the seven lines that change file (above). - `check-adr-anchors`: counts as noted in Acceptance notes. - `check-startup-registry-verdict`: files 3,756 → 3,760. `check-nul-bytes`: 22,620 → 22,624 files. `check-engine-double-contract`: the `updateData`/`deleteData` seams keep their entries at new line numbers; `productionFiles` 3,950 → 3,954. `measure-return-propagating-durability-seams`: 3,779 → 3,783 files. Each count is the four new modules. - `check-governed-merges --branch claude/project-thread-06gygo`: 0 of 6 paths in the register (13 surfaces), NOT governed; 1,437 changed lines, under the 3,000-line bar. </details> <details><summary>Derived gates</summary> `dispatch-gates --commands` on head `6b074fce` derives 52 gate families on the 6 changed paths; all 52 exit 0. Three exited 3 (PREREQUISITE NOT MEASURED) on the first pass and 0 on the rerun: `check-plugin-teardown-shape --self-test` after fetching its pinned fixture commit into the shallow clone, `check:dual-build-cjs-loads` and `check:lean-entry-closure` after `turbo run build --filter='@objectstack/dogfood^...' --filter='@objectstack/runtime^...' --filter=@objectstack/objectql`. `dispatch-gates --ran` reports 52 derived, 52 run, 0 NOT-MEASURED, and exits 0. `pnpm lint`: exit 0. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016WV4czJUan4AwAS6HLpzZN --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4czJUan4AwAS6HLpzZN)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9cbe507 commit b6c2ed9

6 files changed

Lines changed: 895 additions & 542 deletions
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25467 cut 35] The environment's own `sys_metadata` rows (ADR-0131
5+
* D6/D7), from `ObjectStackProtocolImplementation`, moved out of
6+
* `protocol.ts` whole:
7+
*
8+
* - `findEnvironmentRows` — the family read, without a legacy
9+
* organization's rows;
10+
* - `findEnvironmentRow` — the same for one row;
11+
* - `findServedOverlayRow` — the stored row a by-name read serves;
12+
* - `packagelessAddress` — the package a write that names none addresses.
13+
*
14+
* A MOVE, not a redesign. Each function below is the body of the
15+
* `ObjectStackProtocolImplementation` method of the same name as it stood,
16+
* re-indented one level and otherwise unedited.
17+
*
18+
* ## How the moved bodies reach the class
19+
*
20+
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
21+
* header carries the full reasoning), as `protocol-batch-data.ts` applies it.
22+
* The class keeps each method — the published `dist/*.d.ts` lists its name,
23+
* docblock and declared signature — and its body is one literal of exactly
24+
* the {@link EnvironmentRowsHost} members it reads, plus a `.call`. The `engine`
25+
* field is handed over as a getter, so each read reaches the class field as
26+
* it stands at call time; each call to another member is a closure, so it
27+
* still reaches the class method.
28+
* Every caller keeps calling the class methods.
29+
*
30+
* The read-seam census follows both directions of this shape (its
31+
* `HOST_SPLIT` note in `scripts/check-durability-degradation-log-level.mjs`),
32+
* so the storage reads below are still counted as the seams they were.
33+
*
34+
* What the bodies read from elsewhere, they import from where `protocol.ts`
35+
* imports it. Nothing re-exports this module, so the published surface does
36+
* not move.
37+
*/
38+
39+
import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL } from '@objectstack/spec/shared';
40+
import type { StoredOverlayRow } from './item-lock.js';
41+
import { overlayMemberWhere } from './migrations/overlay-index.js';
42+
import type { ObjectStackProtocolImplementation } from './protocol.js';
43+
import { rowMatchesOverlayWhere, servedOverlayRowCandidates } from './protocol.js';
44+
45+
/**
46+
* The `ObjectStackProtocolImplementation` members the functions below read —
47+
* every one of them, and no other; each function's `this` is the `Pick` of
48+
* the members it reads. Each is typed AS the class member (an indexed access
49+
* on the class), so a change to a member's signature on the class is a change
50+
* here too, and naming a member the class does not have is a compile error.
51+
*/
52+
export interface EnvironmentRowsHost {
53+
readonly engine: ObjectStackProtocolImplementation['engine'];
54+
readonly findEnvironmentRow: ObjectStackProtocolImplementation['findEnvironmentRow'];
55+
readonly findEnvironmentRows: ObjectStackProtocolImplementation['findEnvironmentRows'];
56+
readonly legacyOrganizationRowsOf: ObjectStackProtocolImplementation['legacyOrganizationRowsOf'];
57+
readonly rethrowUnlessMetadataStoreUnprovisioned: ObjectStackProtocolImplementation['rethrowUnlessMetadataStoreUnprovisioned'];
58+
readonly withoutLegacyOrganizationRows: ObjectStackProtocolImplementation['withoutLegacyOrganizationRows'];
59+
}
60+
61+
/** The body of `ObjectStackProtocolImplementation#findEnvironmentRows`; the method's docblock there is its contract. */
62+
export async function findEnvironmentRows(this: Pick<EnvironmentRowsHost, 'engine' | 'legacyOrganizationRowsOf' | 'withoutLegacyOrganizationRows'>, table: string, query: { where: Record<string, unknown>; context?: unknown; [key: string]: unknown }): Promise<any[]> {
63+
const rows = (await this.engine.find(table, query as never)) as any[] | null | undefined;
64+
if (query.limit === undefined && query.offset === undefined) {
65+
return this.withoutLegacyOrganizationRows(table, rows, query.context);
66+
}
67+
// An offset counts rows before the page, so any legacy row can shift
68+
// it; a limit alone, only one the page itself holds.
69+
if ((!rows || rows.length === 0) && query.offset === undefined) return rows ?? [];
70+
const legacy = await this.legacyOrganizationRowsOf(table, query.context);
71+
if (legacy.size === 0) return rows ?? [];
72+
if (query.offset === undefined && !(rows ?? []).some((row) => legacy.has(String(row?.id)))) return rows ?? [];
73+
const where = { $and: [query.where, { id: { $nin: [...legacy.keys()] } }] };
74+
return ((await this.engine.find(table, { ...query, where } as never)) as any[] | null | undefined) ?? [];
75+
}
76+
77+
/** The body of `ObjectStackProtocolImplementation#findEnvironmentRow`; the method's docblock there is its contract. */
78+
export async function findEnvironmentRow(this: Pick<EnvironmentRowsHost, 'engine' | 'legacyOrganizationRowsOf'>, table: string, query: { where: Record<string, unknown>; context?: unknown; [key: string]: unknown }): Promise<any> {
79+
const row = await this.engine.findOne(table, query as never);
80+
if (!row) return row ?? null;
81+
const legacy = await this.legacyOrganizationRowsOf(table, query.context);
82+
if (!legacy.has(String((row as { id?: unknown }).id))) return row;
83+
const rows = (await this.engine.find(table, query as never)) as any[] | null | undefined;
84+
return (rows ?? []).find((r) => !legacy.has(String(r?.id))) ?? null;
85+
}
86+
87+
/** The body of `ObjectStackProtocolImplementation#findServedOverlayRow`; the method's docblock there is its contract. */
88+
export async function findServedOverlayRow(
89+
this: Pick<EnvironmentRowsHost, 'findEnvironmentRow'>,
90+
args: {
91+
type: string;
92+
name: string;
93+
state: 'active' | 'draft';
94+
/** [#23715] `null`: the package-unbound row alone (see {@link servedOverlayRowCandidates}). */
95+
packageId?: string | null;
96+
/** [#22877] The bundle member a by-name door's selector named; omitted, the canonical one. */
97+
member?: string;
98+
},
99+
activeRows?: readonly StoredOverlayRow[],
100+
): Promise<{ row: any } | undefined> {
101+
// [#21804] The order is {@link servedOverlayRowCandidates}, the one
102+
// the list's merge takes over the rows it read: the canonical
103+
// spelling then the other, and within each, the package's
104+
// own row then the package-less row (never another package's, or a
105+
// collision would serve package B's customization for a package A
106+
// read); with no package context (legacy/runtime reader), any row.
107+
const other = PLURAL_TO_SINGULAR[args.type] ?? SINGULAR_TO_PLURAL[args.type];
108+
for (const candidate of servedOverlayRowCandidates({ packageId: args.packageId })) {
109+
const type = candidate.spelling === 'canonical' ? args.type : other;
110+
if (type === undefined) continue;
111+
const where: Record<string, unknown> = {
112+
type, name: args.name, state: args.state,
113+
// [#22877] A by-name read of a bundled type answers the member
114+
// its selector named, else its CANONICAL member, as the
115+
// registry's bare-name read does; nothing for every other type.
116+
...overlayMemberWhere(type, args.member),
117+
};
118+
if (candidate.packageId !== undefined) where.package_id = candidate.packageId;
119+
// [#21911, ADR-0096] The explicit system opt-in: a platform store
120+
// read, which no caller's grants scope — the door that asked
121+
// already authorized the caller, and the protocol scopes the row
122+
// itself ({@link findEnvironmentRow}: the environment's rows only).
123+
// It no longer reaches the data engine as a principal-less context.
124+
const row = activeRows !== undefined && args.state === 'active'
125+
? activeRows.find((stored) => rowMatchesOverlayWhere(stored, where))
126+
: await this.findEnvironmentRow('sys_metadata', { where, context: { isSystem: true } });
127+
if (row) return { row };
128+
}
129+
return undefined;
130+
}
131+
132+
/** The body of `ObjectStackProtocolImplementation#packagelessAddress`; the method's docblock there is its contract. */
133+
export async function packagelessAddress(
134+
this: Pick<EnvironmentRowsHost, 'findEnvironmentRows' | 'rethrowUnlessMetadataStoreUnprovisioned'>,
135+
type: string,
136+
name: string,
137+
member?: string,
138+
state: 'active' | 'draft' = 'active',
139+
activeRows?: readonly StoredOverlayRow[],
140+
): Promise<{ packageId: string | null; candidates: readonly string[]; unbound: boolean }> {
141+
let rows: Array<{ package_id?: unknown }> | null | undefined;
142+
const where = { type, name, state, ...overlayMemberWhere(type, member) };
143+
try {
144+
// [#21911] The explicit system opt-in — see findServedOverlayRow.
145+
rows = activeRows !== undefined && state === 'active'
146+
? activeRows.filter((row) => rowMatchesOverlayWhere(row, where))
147+
: await this.findEnvironmentRows('sys_metadata', { where, context: { isSystem: true } });
148+
} catch (error) {
149+
// [#5532] An unprovisioned store holds no row, so no binding; any
150+
// other failure is a read that did not happen, never "no binding".
151+
this.rethrowUnlessMetadataStoreUnprovisioned(error, 'sys_metadata');
152+
}
153+
const bindings = new Set<string>();
154+
let unbound = false;
155+
for (const row of rows ?? []) {
156+
if (typeof row?.package_id === 'string' && row.package_id !== '') bindings.add(row.package_id);
157+
else unbound = true;
158+
}
159+
const candidates = [...bindings].sort();
160+
return { packageId: candidates.length === 1 ? candidates[0] ?? null : null, candidates, unbound };
161+
}

0 commit comments

Comments
 (0)