Skip to content

Commit 17c34b8

Browse files
refactor(metadata-protocol): move the object-extender fold out of protocol.ts into protocol-object-extender-fold.ts (#25467 cut 30) (#27129)
<!-- 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 30 of the `protocol.ts` split (#24586 ruling A, recorded in `6270097840`). Cuts 1–29 landed as #25494, #25518, #25538, #25589, #25601, #25623, #25642, #25719, #25731, #25740, #25797, #25939, #25972, #26014, #26104, #26168, #26264, #26317, #26350, #26366, #26404, #26449, #26654, #26704, #26807, #26873, #26933, #26980 and #27029. The claim is `6460856054`. It takes the cut 30 proposal of the cut 29 report (`6458845738`): one member on its own theme. It passed the AST screen and the read-seam census and moved unedited. One `ObjectStackProtocolImplementation` member moves unedited into a new module, `packages/metadata-protocol/src/protocol-object-extender-fold.ts` (#23050 shape B): `foldObjectExtendersFromRegistry`. It folds the registry's `extend` contributors onto an OBJECT body that came from the MetadataService, so that the by-name read serves the object's resolved schema as the list read does (#7556). `protocol.ts` loses 10 net lines (29,425 to 29,415). 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` `cbb3a1a1a5` (after cut 29's merge). It names no issue, so the claim comment is the identity record. It holds the move commit `b542764e48` and three merges of `main`: - `29cc169592` (7 commits): nothing in this package. - `752f03c260` (12 commits): **brings #27084's `protocol.ts` change (+95 / −15)**, which auto-merged. The first merge's derivation had reported a STALE TREE. - `307e5c9975` (13 commits): nothing in this package. The second merge's derivation reported a STALE TREE on CI workflow files. The claim asked for one merge. The extra two follow the derivation's STALE TREE findings. The merged tree was measured against `main` itself (see "The merged tree against `main`"). ## A deviation, disclosed: the cut 25 comment-offset trap fired, and the cut's own comment was shortened The first layout of this cut (move commit `cc3ec04204`, never pushed) broke raw and sorted-union `.d.ts` identity: - raw went to `701056df78aed858…`, 788,085 bytes (+4,967); - normalised went to `522aaecdadbe5b51…`. The only hunk was the one cut 25 met. tsc's declaration emitter reused an inferred `@objectstack/spec` node (`ast?: unknown`) and printed comment ranges read from `protocol.ts` at that node's offset. That pasted the "Zod schemas used to validate overlay items" docblock into the type. Printed with comments removed (the TS printer's `removeComments`), base and that head were identical (`1583192e38f78f85`). It was still a published `.d.ts` change, so it failed the ruling `6348891160` bar. As the claim's carried note directs, the fix was the cut's own added text, never the bar. The import comment the cut adds was reworded 21 characters shorter, and the move commit was amended to `b542764e48`. Raw identity came back. Every head reading below is from `b542764e48`, including the tsc, hand-gate, positive-control and suite legs, which were re-run on it. ## Serial with the file's in-flight PRs The claim found no open PR editing `protocol.ts` and asked for a re-check before this PR was opened. - #27084 appeared and edited `protocol.ts`, but it merged into `main` before this PR. It is part of the second merge above, and it names no moved member. - At `pr_create`, of the 26 open PRs, two touch this package: #26937 (one test) and #26517 (`CHANGELOG.md` / `package.json`). **None edits `protocol.ts`.** Both are merge-tree clean against this head and against `main`. `main` moved 5 commits after the third merge (to `07d18e6927`). Their `git merge-tree` with this head is clean, and none touches this package. The derivation on `307e5c9975` still names one stale file, `.github/workflows/lint.yml`. So the gates were derived again on that merge-tree (this head with `07d18e6927`): it reads **no stale finding and the same 46 gate commands**, line for line. `main` was not merged a fourth time. The PR's merge ref runs CI against current `main`. ## The order's screen (private statics, `new Error(`, the read-seam census) 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(`, so no `PRODUCERS` row is owed. - **Read-seam census.** `check-durability-degradation-log-level --list` was taken on the base and again on the cut. Both read `89 read seam(s)`, the claim's base, and the lists are identical once `[@:][0-9]+` is stripped. The full readings are under "The durability `--list`". ## What moves | Member | Base line | Visibility | Body lines | AST tokens (params + return + body) | Reads | |---|---|---|---|---|---| | `foldObjectExtendersFromRegistry` | `:8901` | private | 18 (with its comments) | 112 | `engine` (field); module-level `PLURAL_TO_SINGULAR` | `protocol-object-extender-fold.ts` has 68 lines. ## The shape (#23050 shape B, as cuts 1–29 apply it) - **The class keeps the member.** It keeps its name, docblock, signature and position. Its body now delegates, marked `[#25467 cut 30]`. - **`ObjectExtenderFoldHost`** types the one member the body reads as the class member itself (`ObjectStackProtocolImplementation['engine']`). - **The `engine` field is handed over as a getter**, so the read finds it at call time, as before. - **The `try`/`catch` around the in-memory `registry.foldObjectExtendersOnto` call moves with the body, unedited**, as the claim says. - **No member changes visibility.** - **Every caller still calls the class method.** There are 6, at `:7412`, `:9613`, `:11092`, `:11144`, `:11493` and `:11723` (base). None changes. ## Module-level declarations, by reader: no import leaves `protocol.ts` The one module-level name the body reads is `PLURAL_TO_SINGULAR`, from `@objectstack/spec/shared`. It has many other readers in `protocol.ts` and stays. The new module imports it from there too. So the top level of `protocol.ts` changes by the new module's import alone. ## The published `.d.ts` The series bar is ruling `6348891160`: sorted-union identity, measured first, with raw identity reported when it holds. **Both hold for this cut** at `b542764e48`, and on the merged tree against `main`. See the deviation above for the first layout. Base `cbb3a1a1a5` and head `b542764e48` were built with `pnpm --filter @objectstack/metadata-protocol build` against one dependency tree. The closure was built at head by a full `pnpm build` (116/116). For the base build, the package's `src/` was swapped tree-only to base (the new module removed) and restored from `HEAD` under a trap. Afterwards `git diff HEAD` was empty, `git status` was clean, and both touched blobs equalled `HEAD`'s. - **Normalised sha256** (the ruling's bar): `cd4b7becaeffffd99b2e6db36c0147efbcaf17948bf2301a02cf203b45b9e504` at both ends, on `dist/index.d.ts` and `dist/index.d.cts`. The normaliser sorted 80 string-literal union runs. **Sorted-union identity holds.** - **Raw sha256**: `7a0b7ca10b7be13b346d7034f854267d57a8efbc41350f12aab769c36ee78a88` at both ends, 783,118 bytes. **Raw identity holds.** - **The two builds differ where they should.** - `index.js` hashes `5376c52e2c80abdd…` at base and `5dd2faad4fb4d0c3…` at head (`index.cjs` `84507a06aac8b045…` → `e347dd6c9b78122c…`). - `foldObjectExtendersFromRegistry.call(host` goes 0 → 1 in both bundles. - Its `function …(` declaration goes 0 → 1. - **Positive control** on the head tree. `private` was dropped from `foldObjectExtendersFromRegistry` on disk (restored from `HEAD` under a trap) and the package rebuilt. Both `.d.ts` files move to `04fd085e8a03776c…` (normalised `aa13275083654b7a…`, which differs from the bar's `cd4b7bec…`) and print `foldObjectExtendersFromRegistry(type: string, name: unknown, body: unknown): unknown;`. 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 of them. Neither the function nor `ObjectExtenderFoldHost` appears. ### The merged tree against `main` The merged tree `307e5c9975` was measured against `main` `6817bf1eae`, its second parent, in the same way. The closure was rebuilt by a full `pnpm build` (116/116), and `main`'s package `src/` was swapped in tree-only and restored under a trap. The merged tree differs from `main` only by this cut: 2 files, +76 / −18. - **`.d.ts`**: raw `9f7d69b1624e9398…` (784,351 bytes) and normalised `fd5431a235098745…`, the same at `main` and merged. Both identities hold. The offset trap did not fire after #27084's `protocol.ts` change. - **`index.js`**: `aa69b0ce794c8221…` at `main`, `18aded551747cdaa…` merged. Export tables: 78 names, `9f18a0177ac736ed…` on both. - **AST compare**, `main`'s `protocol.ts` against the merged one: 112 tokens, 0 differ. - **Suite**: `main` and merged both give 21,775 tests (21,749 passed, 0 failed, 26 skipped, 281 files), per test identical. The second merge `752f03c260` was measured the same way against `main` `6845035c6b`, with the same `.d.ts` result. No changeset: `skip-changeset`, per ruling `6348891160`. ## The durability `--list` `check-durability-degradation-log-level` exits 0 at both ends with identical output: `89 read seam(s)` at both ends. Its `--list` is **identical once `[@:][0-9]+` is stripped** (165 lines, and no entry changes file). - The moved `try` guards the in-memory `registry.foldObjectExtendersOnto` call, not the gate's read vocabulary (`find`/`findOne`/`count`). - No seam enters or leaves the census. - On the merged tree the list is identical too, `89 read seam(s)`. ## 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 `cbb3a1a1a5` against the head module: ``` foldObjectExtendersFromRegistry: base tokens 112, head tokens 112, differ 0, text-equal(ws-collapsed, comments incl.) true TOTAL tokens=112 differ=0 members=1 CLASS members base=304 head=304 order-same=true byte-identical=303 stubs-with-identical-docblock+signature=1 other=[] TOP base=263 head=264 byte-identical=263 ``` **Negative controls.** Each changed one token in an in-memory copy of the head module, and the member moved (differ 1): ``` "if (singular !== 'object') return body;" -> "… !== 'view' …" differ 1 "foldObjectExtendersOnto(name, body)" -> "…(body, body)" differ 1 ``` - **Class side.** There are 304 members at base and at head, in the same order. The 303 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. - **Top level of `protocol.ts`.** All 263 base statements are byte-identical, leading comments included. Head adds one statement, the new module's import. - **Size.** +76 / −18 over 2 files, 94 changed lines (the claim's estimate was 60–90). ## Readers of the moved lines These were grepped for the member name and the body's tags and distinctive text (`foldObjectExtendersOnto`, `#8027`, `#7556`): - the 24 files that name `protocol.ts` and call `readFileSync`; - the test files in `packages/`, `docs/qa/` and `scripts/`; - the `@objectstack/spec` liveness ledgers. - **By path: none reads a moved line.** - **The one other mention.** `protocol.runtime-gate-stored-universe.test.ts` names the member in a comment only. It and the doors that serve an object by name are driven by the package suite, which passes at both ends. - **The liveness ledgers.** No row points at the member. Carried forward: `manifest.json`'s `dependencies` row → `protocol.ts#resolveWritePackageScope` (cut 19), and `seed.json` → `protocol.ts#applySeedBodies` (cut 14). These are `packages/spec/**`, outside this card. - **ADR anchors.** `check-adr-anchors` exits 0 with identical output at both ends (1,898 decision-letter citations, 60,136 citations). No moved line cites an ADR. - **Engine-double contract** (`check-engine-double-contract`): exits 0 at both ends. - The `updateData`/`deleteData` seams keep their entries at new line numbers (14009 → 13999, 14034 → 14024). - `productionFiles` is 3,826 at base and 3,828 at head. The +2 are the new file and the gitignored `packages/spec/src/migrations/registry.ts` that the spec build writes in the head tree, as in cuts 20–29. - **Identical output at both ends:** `check-adr-anchors`, `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 `cbb3a1a1a5`. ## Verification - `tsc --noEmit` in `packages/metadata-protocol` exits 0 at head and on the merged tree. - `pnpm exec eslint --no-inline-config packages/metadata-protocol` exits 0 with no output (the whole touched package, with the root `lint` script's flags). - **`packages/metadata-protocol` suite**, compared per test name and status. Each leg ran vitest `--maxWorkers=2` with the JSON reporter under the shared verify lock. - Base `cbb3a1a1a5`: 21,761 tests (21,735 passed, 0 failed, 26 skipped), 280 files. - Head `b542764e48`: 21,761 tests (21,735 passed, 0 failed, 26 skipped), 280 files. - None lost, none added, none changed status. The base leg is the package `src/` swapped tree-only from `cbb3a1a1a5` (the new module removed) and restored from `HEAD` under a trap. - The merged head `307e5c9975` and `main` `6817bf1eae` both give 21,775 tests, per test identical (0 lost, 0 added, 0 changed). - `check-platform-checklist --changed-from origin/main` (merged tree): OK, 31 anchors judged, 0 lost. - **Governed surface.** `check-governed-merges --branch claude/project-thread-ybjm3h` finds 0 of 2 paths in the register (13 surfaces), so the PR is NOT governed. It has 94 changed lines, under the 3,000-line human-merge bar. - **Derived gates.** `dispatch-gates --commands --repo objectstack-ai/objectstack` derives 46 gates on 2 changed paths from `307e5c9975`. All 46 exit 0 after the full `pnpm build` (116/116). `dispatch-gates --ran` reports 46 derived, 46 run, 0 NOT-MEASURED and 0 UNRUN, and exits 0. The stale-tree finding and its re-derivation are under "Serial with the file's in-flight PRs". ## Carry-forward These stay in `protocol.ts`: - **The read-seam members** (cut 21): `findEnvironmentRows`, `findEnvironmentRow`, `packagelessAddress`, `findServedOverlayRow`, `foldStoredCollection`, `applyObjectRegistryMutation`, `unmanagedImportCollisions`, `rollBackUnmanagedImport`, `readRevertableCommit` and `storedFlowBindingAgrees`. `rethrowUnlessMetadataStoreUnprovisioned` cannot move at all. - Public members whose return type is inferred. - **The bodies kept by a by-path reader:** - `probeRecord`, `getMetaItemCached`, `findReferencesToMeta`, `refuseUnmintableMetaType` and `metaTypeNamespaceExists`. - `viewContainerNameCollisionRefusal` with `overlaidShippedContainerViewNames`, and `reportPageRequiresAbsentAtLoad`. - The 7 `applyRegistryWriteThrough` callers, `readCodeLayerForCarryForward`, `ensureObjectStorage`, `dropObjectStorage` and `applyRemoteMetadataMutation`. - `emitMetadataMutation`, and `getOverlayRepo`. - `isArtifactBacked`, `isNestedArtifactField` and `packagedArtifactBase` (the `lookupArtifactItem` site pins). - The private-static readers: `refusePackagedBaseRemoval`, `packagedBaseRefusal`, `runPublishSideEffects`, `promoteDraftForPublish`, `publishPackageDrafts` and `discardPackageDrafts`. - The field writers: `warnClosureReadUnavailable` and `resolveSduiManifest`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016SP1spj613tyGCgxoD1An6 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4db4598 commit 17c34b8

2 files changed

Lines changed: 76 additions & 18 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25467 cut 30] The object-extender fold, from
5+
* `ObjectStackProtocolImplementation`, moved out of `protocol.ts` whole:
6+
* `foldObjectExtendersFromRegistry`, which folds the registry's `extend`
7+
* contributors onto an OBJECT body that came from the MetadataService, so the
8+
* by-name read serves the object's resolved schema as the list read does
9+
* (#7556).
10+
*
11+
* A MOVE, not a redesign. The function below is the body of the
12+
* `ObjectStackProtocolImplementation` method of the same name as it stood,
13+
* re-indented one level and otherwise unedited.
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 ObjectExtenderFoldHost} member it reads, plus a `.call`. The
22+
* `engine` field is handed over as a getter, so the read finds it as it
23+
* stands at call time. Every caller keeps calling the class method.
24+
*
25+
* What the body reads from elsewhere, it imports from where `protocol.ts`
26+
* imports it. Nothing re-exports this module, so the published surface does
27+
* not move.
28+
*/
29+
30+
import { PLURAL_TO_SINGULAR } from '@objectstack/spec/shared';
31+
import type { ObjectStackProtocolImplementation } from './protocol.js';
32+
33+
/**
34+
* The `ObjectStackProtocolImplementation` member the function below reads —
35+
* the only one; the function's `this` is the `Pick` of it. It is typed AS the
36+
* class member (an indexed access on the class), so a change to its
37+
* signature on the class is a change here too.
38+
*/
39+
export interface ObjectExtenderFoldHost {
40+
readonly engine: ObjectStackProtocolImplementation['engine'];
41+
}
42+
43+
/** The body of `ObjectStackProtocolImplementation#foldObjectExtendersFromRegistry`; the method's docblock there is its contract. */
44+
export function foldObjectExtendersFromRegistry(
45+
this: Pick<ObjectExtenderFoldHost, 'engine'>,
46+
type: string,
47+
name: unknown,
48+
body: unknown,
49+
): unknown {
50+
const singular = PLURAL_TO_SINGULAR[type] ?? type;
51+
if (singular !== 'object') return body;
52+
if (body === null || typeof body !== 'object') return body;
53+
// [#8027] The list caller reads the name off the ROW rather than off a
54+
// request, and a row with no usable `name` has no contributor list to
55+
// look up — `mergePackageAwareOverlay` skips such rows too.
56+
if (typeof name !== 'string' || name === '') return body;
57+
const registry = (this.engine as any)?.registry;
58+
// Partial registry doubles in tests predate this method; a host that
59+
// cannot fold answers exactly as it did before.
60+
if (!registry || typeof registry.foldObjectExtendersOnto !== 'function') return body;
61+
try {
62+
return registry.foldObjectExtendersOnto(name, body);
63+
} catch {
64+
// The fold is a read over in-memory contributors; a failure here
65+
// must not turn a served schema into a 5xx.
66+
return body;
67+
}
68+
}

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

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,8 @@ import { flowRegistrationsOf, resolveFlowCanonicalizer, resolveFlowRegistrationR
423423
import { isDeclaredCodeDatasource, isHostCodeDatasource } from './protocol-code-datasource-predicates.js';
424424
// [#25467 cut 29] The legacy-organization row filters: which rows a not-yet-migrated database still holds.
425425
import { legacyOrganizationRowsOf, withoutLegacyOrganizationRows } from './protocol-legacy-organization-rows.js';
426+
// [#25467 cut 30] The object-extender fold onto a MetadataService object body.
427+
import { foldObjectExtendersFromRegistry } from './protocol-object-extender-fold.js';
426428

427429
/**
428430
* Canonical Zod schema per metadata type lives in
@@ -8899,24 +8901,12 @@ export class ObjectStackProtocolImplementation implements
88998901
* would duplicate both.
89008902
*/
89018903
private foldObjectExtendersFromRegistry(type: string, name: unknown, body: unknown): unknown {
8902-
const singular = PLURAL_TO_SINGULAR[type] ?? type;
8903-
if (singular !== 'object') return body;
8904-
if (body === null || typeof body !== 'object') return body;
8905-
// [#8027] The list caller reads the name off the ROW rather than off a
8906-
// request, and a row with no usable `name` has no contributor list to
8907-
// look up — `mergePackageAwareOverlay` skips such rows too.
8908-
if (typeof name !== 'string' || name === '') return body;
8909-
const registry = (this.engine as any)?.registry;
8910-
// Partial registry doubles in tests predate this method; a host that
8911-
// cannot fold answers exactly as it did before.
8912-
if (!registry || typeof registry.foldObjectExtendersOnto !== 'function') return body;
8913-
try {
8914-
return registry.foldObjectExtendersOnto(name, body);
8915-
} catch {
8916-
// The fold is a read over in-memory contributors; a failure here
8917-
// must not turn a served schema into a 5xx.
8918-
return body;
8919-
}
8904+
// [#25467 cut 30] The body moved to `protocol-object-extender-fold.ts` (see `ObjectExtenderFoldHost`).
8905+
const readEngine = () => this.engine;
8906+
const host: ThisParameterType<typeof foldObjectExtendersFromRegistry> = {
8907+
get engine() { return readEngine(); },
8908+
};
8909+
return foldObjectExtendersFromRegistry.call(host, type, name, body);
89208910
}
89218911

89228912
/**

0 commit comments

Comments
 (0)