Skip to content

Commit d677879

Browse files
refactor(metadata-protocol): move the metadata-service code-layer read out of protocol.ts into protocol-metadata-service-read.ts (#25467 cut 20) (#26366)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X7JKgiM29GUcitRvoGGj5BU)_ Part of #25467 Clause-②: no Metadata-protocol: none Ruling-ref: 6270097840 Cut 20 of the `protocol.ts` split (#24586 ruling A, recorded in `6270097840`). Cuts 1–19 landed as #25494, #25518, #25538, #25589, #25601, #25623, #25642, #25719, #25731, #25740, #25797, #25939, #25972, #26014, #26104, #26168, #26264, #26317 and #26350. The claim is `6429678924`. The scope is the cut 20 proposal of the cut 19 report (`6428860550`), which the server seat approved in its dispatch order. The member passed the AST screen and moved unedited. One `ObjectStackProtocolImplementation` member moves unedited into a new module, `packages/metadata-protocol/src/protocol-metadata-service-read.ts` (#23050 shape B): - `readItemFromMetadataService`: the code-layer read through the `metadata` service. It reads one item, package-scoped (ADR-0048), and takes the service's diagnosis when the service gives one. It falls back to the singular/plural spelling. A stored object row's copy is not counted as a code layer (#23645). `protocol.ts` loses 30 net lines (29,240 to 29,210). The cut is move-only, and the card stays open for later cuts. Branch `claude/project-thread-ybjm3h` is the harness-pinned branch for this thread. It was restarted from `main` `e2129cbf06` after cut 19 merged. It names no issue, so the claim comment is the identity record. It holds the move commit `1cb285a797` and the merge of `main` `c19ad22f62` (`54aaeba31e`). That merge touches no file of `packages/metadata-protocol/`: the package's `src/` tree is `953bab7a` at both ends. ## Serial with the file's in-flight PRs Measured just before this PR was opened: of the 19 open PRs, three touch `packages/metadata-protocol/src/`. None names `readItemFromMetadataService`. Each was checked with `git merge-tree` against this branch's head (`54aaeba31e`) and against `origin/main` alone. - **#26244** (draft, head `c33b95d89f`) is the only one that edits `protocol.ts`, at `:163` (imports) and `:23178`, far from `:9328`. Its merge-tree with this head conflicts in `protocol.ts` and `packages/spec/src/type-alias-convention.pin.test.ts`. Its merge-tree with `main` alone conflicts in the same two files. The `protocol.ts` conflict is the import-line conflict with cut 18's `ALL_CONVERSIONS` drop; #26317 spelled out the union resolution. This cut adds no conflict. - **#26138** edits `protocol.*.test.ts` files. Its two conflicts (`scripts/sdui-manifest.sources/core.json`, `types.json`) are likewise with `main` alone. - **#26337** (head `2448c0035a`) edits three `protocol.*.test.ts` files. Its merge-tree with this head is clean. No open PR edits the new module. ## The order's screen (private statics, `new Error(`) The member passed: - **No private-static read.** The AST walk found no access on the `ObjectStackProtocolImplementation` identifier and none on `this.constructor`. - **No error constructed.** The body has no `new Error(` and no other error constructor, so no `PRODUCERS` row is owed. It has no `try`, no `catch` and no log line. ## What moves | Member | Base line | Visibility | Body lines | AST tokens (params + return + body) | |---|---|---|---|---| | `readItemFromMetadataService` | `:9328` | private async | 40 | 383 | The proposal said 40. ## The shape (#23050 shape B, as cuts 1–19 apply it) - **The class keeps the member.** It keeps its name, docblock, signature and position. Its body now delegates, marked `[#25467 cut 20]`. - **Typing.** `MetadataServiceReadHost` types its one member as the class member itself (`ObjectStackProtocolImplementation['getServicesRegistry']`). The function types its `this` as `Pick<MetadataServiceReadHost, 'getServicesRegistry'>`. The stub annotates its literal as `ThisParameterType<typeof readItemFromMetadataService>`. No member changes visibility. - **Members read, measured by AST walk.** The walk covers property and element accesses on `this`, with `as` casts, parentheses and `!` stripped, and bare `this` checked too. The body reads `getServicesRegistry` only, as `this.getServicesRegistry?.()`. - `getServicesRegistry` is an optional private FIELD holding the constructor-supplied callback, not a method. So it is handed over as a getter (`get getServicesRegistry() { return readGetServicesRegistry(); }`), and the body reads the protocol's field at call time. - An absent callback stays `undefined`, so the body's `?.()` short-circuits exactly as before. - This is the same hand-over `protocol-package-install.ts`, `protocol-record-reads.ts` and `protocol-save-door-refusals.ts` already use for this field. - **Every caller still calls the class method,** as the order asks for the `:9262` caller. There are four `this.readItemFromMetadataService(` sites in `protocol.ts` (`:9262`, `:11091`, `:11422`, `:18960` at base), and none changes. ## Module-level declarations, by reader Measured by an AST walk over the body's identifiers, against `protocol.ts`'s top-level declarations and import bindings (property names excluded): | Name | Where `protocol.ts` has it | Other readers in `protocol.ts` | What this cut does | |---|---|---|---| | `canonicalMetaType` | exported function, `:484` | yes (23) | already exported; the new module imports it from `./protocol.js` at runtime, as `protocol-lock-state.ts` and `protocol-registry-materialize.ts` do | | `isTenantAuthored` | the `@objectstack/metadata-core` import | yes | stays; the new module imports it from there too | | `PLURAL_TO_SINGULAR`, `SINGULAR_TO_PLURAL` | the `@objectstack/spec/shared` import | yes | stay; imported from there too | Nothing gains `export`, and no import of `protocol.ts` changes. The one new statement in `protocol.ts` is the import of the new module. ## The durability `--list` `check-durability-degradation-log-level` exits 0 at both ends with identical output. Its `--list` is **identical once `[@:][0-9]+` is stripped** (158 lines, and no entry changes file). The body has no `catch` and makes no engine call. Its reads go through the `metadata` service. ## The published `.d.ts` The series bar is ruling `6348891160`: sorted-union identity, measured first, with raw identity reported when it holds. **This cut holds the bar, and raw identity does NOT hold**, for the reason below. Base `e2129cbf06` and head `1cb285a797` were built with `pnpm --filter @objectstack/metadata-protocol build` against one dependency tree. The closure was built at head by a full `pnpm build` (119/119). For the base build, the package's `src/` was swapped tree-only to base and restored from `HEAD` under a trap. Afterwards `git diff HEAD` was empty, `git status` was clean, and both touched blobs equalled `HEAD`'s. The readings were taken on the pre-merge tree, and the head readings again after merging `main` (head `54aaeba31e`, closure rebuilt by a full `pnpm build`, 119/119). - **Normalised sha256** (the ruling's bar): `f7f9306ed46a5605c5e4c59bbc212a7d5288061760ec85d473d2f3047a836ba5` at both ends, on `dist/index.d.ts` and `dist/index.d.cts`. The normaliser sorted 80 string-literal union runs (the cut 12 normaliser). **Sorted-union identity holds.** - **Raw sha256**: base `a8f6adb6f9c56b8f441b7224ca8c4d43b84891bce0373446e1577a7884079dd8`, head `1b134685a474f83cfa85a743e49a811d607fcaeed53ae8856dd62db65ad132f5`. Both files are 776,660 bytes at each end. - The full raw diff is ONE line, `dist/index.d.ts:2448`, inside `getDiscovery()`'s inferred return type: ``` < status: "registered" | "available" | "degraded" | "unavailable" | "stub"; > status: "registered" | "degraded" | "available" | "unavailable" | "stub"; ``` - The same five members appear in a different order. This is the case the ruling's sorted-union bar exists for. `tsc` prints an inferred union in the order its literal types were first created in the program. Moving a body changes which file the checker visits first, which changes that order, and changes nothing else. - No signature, no member and no name moves. `readItemFromMetadataService` stays a `private NAME;` line. - **The two builds differ where they should.** - `index.js` hashes `9495894cdf5ce39b…` at base and `7be270d93afd5f1e…` at head. - The `.call(host` site of `readItemFromMetadataService` occurs 0 times in the base bundles and once in the head bundles (ESM and CJS alike). - `function readItemFromMetadataService(` appears 0 times at base and once at head. - **Positive control** on the head tree. `private` was dropped from `readItemFromMetadataService` on disk (restored from `HEAD` under a trap) and the package rebuilt. Both `.d.ts` files move to `c9acdc769a2b94ed…` (normalised `c1ebd3ff0d127a7e…`, which differs from the bar's `f7f9306e…`) and print `readItemFromMetadataService(type: string, name: string, packageId?: string): Promise<{ data: unknown; degraded: boolean; errors: string[]; }>;`. So the bar can see a change on this member. - **Export tables.** The ESM and CJS bundles, loaded at runtime from the package, each export the same 78 names at base and head. The sorted list hashes to `9f18a0177ac736ed…` on all four. Neither the moved function nor `MetadataServiceReadHost` appears. No changeset: `skip-changeset`, per ruling `6348891160`. ## Proof of move-only **Body, by AST token.** The compare reads the TypeScript compiler's AST leaf tokens, with JSDoc skipped, over the parameters (less the added `this`), the return type and the body. It compares base `protocol.ts` at `e2129cbf06` against the head module: ``` readItemFromMetadataService: base tokens 383, head tokens 383, differ 0, text-equal(ws-collapsed, comments incl.) true TOTAL tokens=383 differ=0 members=1 CLASS members base=301 head=301 order-same=true byte-identical=300 stubs-with-identical-docblock+signature=1 other=[] TOP base=248 head=249 byte-identical=248 ``` **Negative control.** One token was changed in an in-memory copy of the head module: `?? SINGULAR_TO_PLURAL` became `|| SINGULAR_TO_PLURAL`, one occurrence. ``` MUTATED packages/metadata-protocol/src/protocol-metadata-service-read.ts "?? SINGULAR_TO_PLURAL" -> "|| SINGULAR_TO_PLURAL" readItemFromMetadataService: base tokens 383, head tokens 383, differ 1, text-equal(ws-collapsed, comments incl.) false TOTAL tokens=383 differ=1 members=1 ``` - **Class side.** There are 301 members at base and at head, in the same order. The 300 that did not move are byte-identical to base, leading comments included. The stub sits at its base position and keeps its docblock and signature byte-identical (everything before the body's opening brace). - **Top level of `protocol.ts`.** All 248 base statements are byte-identical, leading comments included. Head adds one statement, the new module's import. - **Size.** +101 / −37 over 2 files, 138 changed lines (the proposal's estimate was 180–230). `protocol-metadata-service-read.ts` has 94 lines. ## Readers of the moved lines Every test and script that reads `protocol.ts` by path (70 files) was grepped for the member name and for its body's text: `getDiagnosed`, `metadataService`, `isTenantAuthored`, `get('metadata')` and the plural/singular fallback. So were the test files in `packages/` and `ui/`, and `docs/qa/`. - **By path: none reads the moved body.** `protocol.metadata-store-outage.test.ts` matches `getDiagnosed` in comments. It does not read `protocol.ts` by path: it names the file in prose and drives the read through the protocol. It passes at both ends, in the package suite. - **Behaviour through the class method, no edit owed.** `packages/objectql/src/protocol-delete-boot-hydrated-object-heal.test.ts` names `readItemFromMetadataService` in its header and drives it through the delete door. Run at head after the full build: 1 file, 5 tests passed. - **The `@objectstack/spec` liveness ledgers.** No row points at `readItemFromMetadataService`. Carried forward from cut 19, two rows still hold `protocol.ts#member` evidence pointers whose quoted line has moved to a seam module: - `manifest.json`'s `dependencies` row → `protocol.ts#resolveWritePackageScope` (cut 19); - `seed.json` → `protocol.ts#applySeedBodies` (cut 14). `check:liveness` checks only that the `#symbol` is named in the file, and both stubs keep it, so it passes. Re-pointing them is a `packages/spec/**` edit, outside this card. - No hit in `docs/qa/`. - **ADR anchors.** `check-adr-anchors` exits 0 at both ends. The decision-letter count is unchanged at 1,879. The citation count goes 59,279 → 59,280 and the file count 8,356 → 8,357. The `ADR-0048` comment in the body moves with it, and the new module's header names `ADR-0048` once more. The `protocol.ts` anchor's governing ADRs are all still referenced. - **Engine-double contract** (`check-engine-double-contract`): exits 0 at both ends. - The `updateData`/`deleteData` seams keep their entries at new line numbers (13926 → 13896, 13951 → 13921). - `productionFiles` is 3,748 at base and 3,750 at head. One of the +2 is the new file. The other is the gitignored `packages/spec/src/migrations/registry.ts` that the spec build writes in the head tree. - **Identical output at both ends:** `check-adr-symbol-anchors`, `check-system-context-census`, `check-tenant-audit-census`, `check-query-options-erasure-ratchet`, `check-filter-alias-parity` and `check-durability-degradation-log-level`. `check-nul-bytes` differs only in the scanned file count, +1. The base-end readings were taken in a clean detached worktree at `e2129cbf06`. - **Governed surface.** `check-governed-merges --branch claude/project-thread-ybjm3h` finds 0 of 2 paths in the register, so the PR is NOT governed. It has 138 changed lines, under the 3,000-line human-merge bar. ## Verification - `tsc --noEmit` in `packages/metadata-protocol` exits 0 before and after the merge. - `pnpm exec eslint --no-inline-config packages/metadata-protocol` exits 0 with no output. It covers the whole touched package, which has no `lint` script; these are the root `lint` script's flags. - **`packages/metadata-protocol` suite**, base and head, compared per test name and status. Each leg ran vitest `--maxWorkers=2` with the JSON reporter under the shared verify lock, and both gave `VERDICT command-exit 0`. The runs are on the pre-merge tree; the merge changed no file of the package. - Base `e2129cbf06`: 21,568 tests (21,549 passed, 0 failed, 19 skipped), 270 files. - Head `1cb285a797`: 21,568 tests (21,549 passed, 0 failed, 19 skipped), 270 files. - None lost, none added, none changed status. The base leg is the package `src/` swapped tree-only from `e2129cbf06` (the new module removed) and restored from `HEAD` under a trap. - `check-platform-checklist --changed-from origin/main` (merged tree): OK. 1 anchored file is in the change set, 29 anchors were judged, and 0 were lost. - **Derived gates.** `dispatch-gates --commands --repo objectstack-ai/objectstack` derives 51 gates on 2 changed paths from the merged tree `54aaeba31e`, with no stale-tree warning; all 51 exit 0 after the full `pnpm build` (119/119). `dispatch-gates --ran`: 51 derived, 51 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. ## Carry-forward Unchanged from cut 19. These stay in `protocol.ts`: - Public members whose return type is inferred. - The bodies kept by a reader: `probeRecord`, `getMetaItemCached`, `findReferencesToMeta`, `packagelessAddress`, `refuseUnmintableMetaType`, `metaTypeNamespaceExists`, `viewContainerNameCollisionRefusal` with `overlaidShippedContainerViewNames`, `reportPageRequiresAbsentAtLoad`, the 7 `applyRegistryWriteThrough` callers, `readCodeLayerForCarryForward` and `ensureObjectStorage`. - The private-static readers: `refusePackagedBaseRemoval`, `packagedBaseRefusal`, `runPublishSideEffects`, `promoteDraftForPublish`, `publishPackageDrafts` and `discardPackageDrafts`. - The field writers: `warnClosureReadUnavailable`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016SP1spj613tyGCgxoD1An6 Co-authored-by: Claude <noreply@anthropic.com>
1 parent f02e644 commit d677879

2 files changed

Lines changed: 101 additions & 37 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25467 cut 20] The code-layer read of `ObjectStackProtocolImplementation`
5+
* through the `metadata` service (`readItemFromMetadataService`): one item,
6+
* package-scoped (ADR-0048), its diagnosis when the service gives one, the
7+
* singular/plural spelling as a fallback, and a stored object row's copy not
8+
* counted as a code layer (#23645). Moved out of `protocol.ts` whole.
9+
*
10+
* A MOVE, not a redesign. The function below is the body of the
11+
* `ObjectStackProtocolImplementation` method of the same name as it stood,
12+
* re-indented one level and otherwise unedited: the same service lookup, the
13+
* same two reads in the same order, and the same merged answer.
14+
*
15+
* ## How the moved body reaches the class
16+
*
17+
* The shape #23050 set for `rest-server.ts` (its `data-action-routes.ts`
18+
* header carries the full reasoning), as `protocol-batch-data.ts` applies it.
19+
* The class keeps the method — the published `dist/*.d.ts` lists its name,
20+
* docblock and declared signature — and its body is one literal of exactly
21+
* the {@link MetadataServiceReadHost} members it reads, plus a `.call`. The
22+
* `getServicesRegistry` field is handed over as a getter, as
23+
* `protocol-package-install.ts` and `protocol-save-door-refusals.ts` hand it,
24+
* so the body reads the protocol's field at call time. Every other reader of
25+
* the method keeps calling the class method.
26+
*
27+
* What the body reads of `protocol.ts`'s own module-level declarations it
28+
* imports from it; what it imports from elsewhere, it imports from where
29+
* `protocol.ts` imported it. Nothing re-exports this module, so the published
30+
* surface does not move.
31+
*/
32+
33+
import { isTenantAuthored } from '@objectstack/metadata-core';
34+
import { PLURAL_TO_SINGULAR, SINGULAR_TO_PLURAL } from '@objectstack/spec/shared';
35+
import type { ObjectStackProtocolImplementation } from './protocol.js';
36+
import { canonicalMetaType } from './protocol.js';
37+
38+
/**
39+
* The `ObjectStackProtocolImplementation` members the function below reads —
40+
* every one of them, and no other; the function's `this` is the `Pick` of the
41+
* members it reads. Each is typed AS the class member (an indexed access on
42+
* the class), so a change to a member's signature on the class is a change
43+
* here too, and naming a member the class does not have is a compile error.
44+
*/
45+
export interface MetadataServiceReadHost {
46+
readonly getServicesRegistry: ObjectStackProtocolImplementation['getServicesRegistry'];
47+
}
48+
49+
/** The body of `ObjectStackProtocolImplementation#readItemFromMetadataService`; the method's docblock there is its contract. */
50+
export async function readItemFromMetadataService(
51+
this: Pick<MetadataServiceReadHost, 'getServicesRegistry'>,
52+
type: string,
53+
name: string,
54+
packageId?: string,
55+
): Promise<{ data: unknown; degraded: boolean; errors: string[] }> {
56+
const services = this.getServicesRegistry?.();
57+
const metadataService: any = services?.get('metadata');
58+
if (!metadataService || typeof metadataService.get !== 'function') {
59+
return { data: undefined, degraded: false, errors: [] };
60+
}
61+
// ADR-0048 — thread the caller's package id so a single-item fetch is
62+
// package-scoped. Passed positionally to BOTH reads, so whatever the
63+
// occupant of the `metadata` slot makes of a third argument today is
64+
// unchanged by which of the two methods answers.
65+
const read = async (t: string): Promise<{ data: unknown; degraded: boolean; errors: string[] }> => {
66+
let answer: { data: unknown; degraded: boolean; errors: string[] };
67+
if (typeof metadataService.getDiagnosed === 'function') {
68+
const diagnosed = await metadataService.getDiagnosed(t, name, packageId);
69+
answer = {
70+
data: diagnosed?.data,
71+
degraded: diagnosed?.degraded === true,
72+
errors: Array.isArray(diagnosed?.errors) ? diagnosed.errors : [],
73+
};
74+
} else {
75+
answer = { data: await metadataService.get(t, name, packageId), degraded: false, errors: [] };
76+
}
77+
// [#23645] A stored object row's copy is not a code layer. See the header.
78+
return canonicalMetaType(t) === 'object' && isTenantAuthored(answer.data)
79+
? { ...answer, data: undefined }
80+
: answer;
81+
};
82+
83+
const primary = await read(type);
84+
if (primary.data !== undefined && primary.data !== null) return primary;
85+
const alt = PLURAL_TO_SINGULAR[type] ?? SINGULAR_TO_PLURAL[type];
86+
if (!alt) return { data: undefined, degraded: primary.degraded, errors: primary.errors };
87+
const secondary = await read(alt);
88+
if (secondary.data !== undefined && secondary.data !== null) return secondary;
89+
return {
90+
data: undefined,
91+
degraded: primary.degraded || secondary.degraded,
92+
errors: [...primary.errors, ...secondary.errors],
93+
};
94+
}

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

Lines changed: 7 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,8 @@ import { classifyObjectContribution, releaseAdoptedObjectOwner } from './protoco
404404
import { flowSaveReplaysRetired } from './protocol-flow-save-replay.js';
405405
// [#25467 cut 19] A package write's validation scope: the written package and its declared dependency closure, from the registry.
406406
import { resolveWritePackageScope } from './protocol-write-package-scope.js';
407+
// [#25467 cut 20] The code-layer read through the `metadata` service: package-scoped, singular/plural fallback, a stored object row's copy not counted as code.
408+
import { readItemFromMetadataService } from './protocol-metadata-service-read.js';
407409

408410
/**
409411
* Canonical Zod schema per metadata type lives in
@@ -9330,44 +9332,12 @@ export class ObjectStackProtocolImplementation implements
93309332
name: string,
93319333
packageId?: string,
93329334
): Promise<{ data: unknown; degraded: boolean; errors: string[] }> {
9333-
const services = this.getServicesRegistry?.();
9334-
const metadataService: any = services?.get('metadata');
9335-
if (!metadataService || typeof metadataService.get !== 'function') {
9336-
return { data: undefined, degraded: false, errors: [] };
9337-
}
9338-
// ADR-0048 — thread the caller's package id so a single-item fetch is
9339-
// package-scoped. Passed positionally to BOTH reads, so whatever the
9340-
// occupant of the `metadata` slot makes of a third argument today is
9341-
// unchanged by which of the two methods answers.
9342-
const read = async (t: string): Promise<{ data: unknown; degraded: boolean; errors: string[] }> => {
9343-
let answer: { data: unknown; degraded: boolean; errors: string[] };
9344-
if (typeof metadataService.getDiagnosed === 'function') {
9345-
const diagnosed = await metadataService.getDiagnosed(t, name, packageId);
9346-
answer = {
9347-
data: diagnosed?.data,
9348-
degraded: diagnosed?.degraded === true,
9349-
errors: Array.isArray(diagnosed?.errors) ? diagnosed.errors : [],
9350-
};
9351-
} else {
9352-
answer = { data: await metadataService.get(t, name, packageId), degraded: false, errors: [] };
9353-
}
9354-
// [#23645] A stored object row's copy is not a code layer. See the header.
9355-
return canonicalMetaType(t) === 'object' && isTenantAuthored(answer.data)
9356-
? { ...answer, data: undefined }
9357-
: answer;
9358-
};
9359-
9360-
const primary = await read(type);
9361-
if (primary.data !== undefined && primary.data !== null) return primary;
9362-
const alt = PLURAL_TO_SINGULAR[type] ?? SINGULAR_TO_PLURAL[type];
9363-
if (!alt) return { data: undefined, degraded: primary.degraded, errors: primary.errors };
9364-
const secondary = await read(alt);
9365-
if (secondary.data !== undefined && secondary.data !== null) return secondary;
9366-
return {
9367-
data: undefined,
9368-
degraded: primary.degraded || secondary.degraded,
9369-
errors: [...primary.errors, ...secondary.errors],
9335+
// [#25467 cut 20] The body moved to `protocol-metadata-service-read.ts` (see `MetadataServiceReadHost`).
9336+
const readGetServicesRegistry = () => this.getServicesRegistry;
9337+
const host: ThisParameterType<typeof readItemFromMetadataService> = {
9338+
get getServicesRegistry() { return readGetServicesRegistry(); },
93709339
};
9340+
return readItemFromMetadataService.call(host, type, name, packageId);
93719341
}
93729342

93739343
/**

0 commit comments

Comments
 (0)