Skip to content

Commit ba6912c

Browse files
refactor(metadata-protocol): move the record reads out of protocol.ts into protocol-record-reads.ts (#25467 cut 4) (#25589)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4Uu2dTVig4NQQadyC11Fs5BFZ)_ Part of #25467 Clause-②: no Metadata-protocol: none Ruling-ref: 6270097840 Cut 4 of the `protocol.ts` split (#24586 ruling A, recorded in `6270097840`). Cut 1 landed as #25494, cut 2 as #25518 and cut 3 as #25538. The claim is `6376061670`. The seat agreed scope C and its five conditions before the claim, and agreed the narrowing below after it. Six `ObjectStackProtocolImplementation` methods, the record reads, move unedited into a new module, `packages/metadata-protocol/src/protocol-record-reads.ts`, together with the four declarations of the OCC version-token reading: 568 lines, 142 of them from the last 30 days (measured at `798d480269`; the lines are unchanged on this PR's base). It is move-only, and the card stays open for the later cuts. Branch: `claude/project-thread-uasfag`, the harness-pinned branch for this session, restarted from `origin/main` at `798d480269` after cut 3 merged, and rebuilt on `ce18e73483` after #25550 and #25560 landed (the move re-run from scratch on that base, see below). The branch names no issue, so the claim comment is the identity record. **Serial with the file's in-flight PRs.** Measured just before this PR was opened, on main `ba9e0c3749`. None of the 11 open PRs touches `protocol.ts`, `protocol-record-reads.ts` or the row-serving pin (each PR's diff read against its merge base). `git merge-tree` of this head with each of them, and with main, is clean. At claim, #25533 was the one open PR on `protocol.ts` and was disjoint; it merged as `35d942ed58`. Before `pr_create`, #25550 (#25234) added a line directly after the `@objectstack/spec/contracts` import this cut narrows, and `merge-tree` conflicted, so this cut yielded until it landed (`b531016f6d`), then was rebuilt on `ce18e73483` and re-measured in full. `origin/main` has not touched these files since `ce18e73483`. ## What moves | Method | Base line | Visibility | Return type | |---|---|---|---| | `omitInternalWriteFields` | `:14526` | public | annotated `: void` | | `assertRecordExists` | `:14600` | private | — | | `assertVersionOf` | `:14662` | private | — | | `assertVersionMatch` | `:14713` | private | — | | `readAsCaller` | `:14731` | private | — | | `searchAll` | `:14849`–`:15322` | public | annotated `: Promise<{…}>` | None is on the series' skip list of public members with inferred return types. Four are private. The two public ones declare their return type, so the published `.d.ts` prints the declared type from the stub, unchanged. **Narrowed from scope C during the trial, before the claim.** Both members below stay in the class exactly as they are. - **`probeRecord` stays.** Its system-context `engine.findOne` is one of `protocol.ts`'s 6 sites in `scripts/query-options-erasure-baseline.json`. That baseline is keyed by file and only shrinks, so the site cannot change files unedited: `query-options/no-any-erasure` refuses it in the new file. The moved bodies call it through the protocol, as a host closure. Its `isSystem` site therefore stays put too. - **`getMetaItemCached` stays.** The #9157 pin, `protocol.read-verb-canonical-fold.test.ts`, derives the `/meta` request-boundary verbs from the method bodies in `protocol.ts` itself, and requires each verb to call `canonicalizeMetaRequestType`. With the body moved, the stub no longer folds and the pin goes red (measured). Teaching the pin to follow a stub into another module would be a change to the pin, not a move. - `simpleHash` and `metadataItemNotFoundError` stay with it, because only it reads them. - `canonicalizeMetaRequestType` needs no `export`. ## The shape (#23050 shape B, as cuts 1–3 apply it) - **The class keeps every method** with its name, docblock, signature and position, less `async` on the five async stubs. Each body is now one host literal plus `fn.call(host, …)`, marked `[#25467 cut 4]`. - **Typing.** `RecordReadHost` types every member as the class member itself (`ObjectStackProtocolImplementation['m']`). Each moved function types its `this` as the `Pick` of the host over exactly the members its body reads, measured by AST. Each stub annotates its literal as `ThisParameterType<typeof fn>`. There is no cast, and no member becomes public or protected. - **Members read**, measured by AST: - Fields, as getters so each read happens when the body makes it: `engine` and `getServicesRegistry`. - Methods, as late-bound closures (`(...a) => this.m(...a)`), so a spy or double set on the instance is still the one the body reaches: `assertVersionOf`, `getMetaItems`, `probeRecord` and `readAsCaller`. | Function | `this: Pick<RecordReadHost, …>` | |---|---| | `omitInternalWriteFields` | `'engine'` | | `assertRecordExists` | `'probeRecord'` | | `assertVersionOf` | `'readAsCaller'` | | `assertVersionMatch` | `'assertVersionOf' \| 'probeRecord'` | | `readAsCaller` | `'engine'` | | `searchAll` | `'engine' \| 'getMetaItems' \| 'getServicesRegistry'` | **The caller-read path (seat condition 3).** `readAsCaller` and `searchAll` move token-identical: 59 and 1,976 tokens, 0 differ. - Neither host `Pick` carries an elevation member. The host has no system context, no `isSystem` reader and no privileged engine handle, only the protocol's own `engine` field (the one the method read before). - `searchAll` still asks the security service's `canReadObject` with `request.context` and the spec's `canServeApiOperation`, in the same order, in the moved body. ## Module-level declarations, by reader Each declaration the moved bodies read was checked by an AST walk over the whole of base `protocol.ts`. The walk finds every identifier reference to the declaration outside its own declaration, attributes it to the enclosing class member or top-level statement, and takes the transitive closure over the moved set. **Move with the bodies, byte-identical and unexported.** Each has 0 readers outside the moved bodies and the other moved declarations. Each carries exactly its own docblock (leading-trivia blocks checked: one each). | Declaration | Base line | Readers | |---|---|---| | `assertVersionTokenNotMalformed` | `:3176` | `assertVersionOf`, `assertVersionMatch` | | `NormalisedVersion` | `:3208` | `normaliseVersionToken`, `versionTokensAgree` | | `normaliseVersionToken` | `:3300` | `assertVersionOf`, `versionTokensAgree` | | `versionTokensAgree` | `:3327` | `assertVersionOf` | None is read while the module is first evaluated. **Stay in `protocol.ts` and gain `export` in place**, the only edit to it, imported back by the module: | Declaration | Read by code that stays | |---|---| | `canonicalVersionInstant` (`:3246`) | `auditInstantMs` (`:3350`) | The exported classes `ConcurrentUpdateError` and `MalformedVersionTokenError` are imported as values. `src/index.ts` exports from `./protocol.js` by name, so nothing new is published (measured below). **Imports.** `protocol.ts` drops the names that lose every reader (`noUnusedLocals`): `canServeApiOperation` from `@objectstack/spec/data`, `ISecurityService` from `@objectstack/spec/contracts` (the `RLS_MEMBERSHIP_RESOLVER_SERVICE` import #25550 added on the next line is untouched), and `resolveI18nLabel` from `@objectstack/spec/ui`. The module imports them from the same specifiers. `protocol.ts` gains one statement, the import of the new module. ## The published `.d.ts` Under the series bar (ruling `6348891160`), measured last, on the final layout, at base `ce18e73483` and at head. Both were built with `pnpm --filter @objectstack/metadata-protocol build` against the same dependency tree (`turbo run build --filter='@objectstack/metadata-protocol^...'` at base first). - **Raw sha256** of `dist/index.d.ts` and `dist/index.d.cts`: `25da084d43455d125bfd41b85b1e70477ef7caf77f2cb7da23143b325e51a14e` at base and at head, both files. That is **raw identity**, 770,690 bytes each, so there is no union swap this cut. (The value differs from cut 3's because #25550 and #25560 changed the published surface on main, not this PR.) - **Normalised sha256**, base and head: `ff7d6892b9f1161da356208d51301816c48dc021051544b9c28a3aa14329d57e`. The normaliser, run from `packages/metadata-protocol`: ```sh node -e 'const s=require("fs").readFileSync(process.argv[1],"utf8");process.stdout.write(s.replace(/"[^"\n]*"(?:\s*\|\s*"[^"\n]*")+/g,r=>r.split(/\s*\|\s*/).sort().join(" | ")))' dist/index.d.ts | sha256sum ``` - **Positive control** on a copy of the head `dist/index.d.ts`: making line 1361, `private engine;`, public (`engine;`) gives raw `4bea7ae0…` and normalised `be016197…`. Both differ from the real hashes. - **Export tables.** The ESM and CJS bundles each export the same 78 names at base and head, loaded at runtime (`import()` and `require` of the built bundles). The sorted list hashes `e47ac7c753bcb8bee1e79c1a5f79f7394cf030bd526de81ea8bcbfcb71fa024a` on all four, as at cuts 1–3. `canonicalVersionInstant` does not appear. No changeset: `skip-changeset`, per ruling `6348891160`. ## Proof of move-only - **Bodies and declarations, by AST token** (recursive `getChildren` leaves). For each moved function, the parameters (less the added `this`), return type and body have the same token sequence as the base method. Each moved declaration has the same tokens, and its own docblock is byte-identical. 2,574 tokens compared against base `ce18e73483`, 0 differ. The module re-generated on that base is byte-identical to the one first cut on `798d480269`. Negative control: one changed token makes the check report a difference. - **Class side.** The class has 299 members at base and at head. The 293 that did not move are byte-identical to base, and each of the 6 stubs keeps its docblock and signature byte-identical, less `async`. - **Top level of `protocol.ts`.** 239 of the 247 base statements other than the class are byte-identical with their leading comments. The other 8 are: - the 4 moved declarations; - `canonicalVersionInstant` (`export` added); - the 3 narrowed import statements. The one new statement is the import of the new module. - **Size.** +796 / −603 over 3 files, 1,399 changed lines. `protocol.ts` goes from 31,264 to 30,718 lines, and `protocol-record-reads.ts` has 740. ## Readers of the moved lines - **Row-serving door exposure pin** (`packages/core/src/security/row-serving-door-exposure.pin.test.ts`, seat condition 1). The pin classifies every caller of the read admission `canReadObject` and checks each door's decision site. - `searchAll`'s `canReadObject` call and its `canServeApiOperation` decision both move. Without a migration the pin fails 3 of its tests: an unclassified caller, a stale key, and a decision site that no longer asks. - The cross-object-search row's `CALLERS` key and `decisionSite` move to `protocol-record-reads.ts`. Its door, operation and behaviour pin (`protocol.search-api-exposure.test.ts`) are unchanged. - No wiring line of this door stays in `protocol.ts`: the call matcher no longer fires there, so keeping `protocol.ts` as a key would make the pin's stale-entry check fail. - No row is added, removed or reworded. The test passes at head, as at base, together with `second-object-read-exposure.pin` (7/7; no path of that one is touched this cut). - **Erasure ratchet (seat condition 2).** `check-query-options-erasure-ratchet` gives identical output at both ends: 50 unswept non-test sites in 15 files, none new, and no baseline file key added. The engine reads that move (`readAsCaller`'s `findOne`, `searchAll`'s `find`) are not counted sites. The counted one stays with `probeRecord`. - **System-context census (seat condition 2).** Identical output at both ends: 149 elevation read sites in 22 packages across 77 files. The moved text has no `isSystem` read. No file-count move. - **Tenant census.** `check-tenant-audit-census`: identical output at both ends, no `--write`. - **ADR symbol anchors and the platform checklist.** `docs/qa/platform-checklist/areas/*.json` anchor `protocol.ts#searchAll` and other moved methods. The method shells stay, so the anchors still resolve; they are not edited. `check-adr-symbol-anchors` and `check-platform-checklist`: identical output at both ends. The blanking check before scope C found no anchor that resolves only inside the moved bodies (seat condition 4). - **Spec liveness.** `object.json` cites `protocol.ts#searchAll` for `enable.searchable`. Its key-mention check still passes, on the docblock that stays. `check-liveness` gives identical output at both ends. The file is not edited. - **ADR anchors and decision letters.** `check:adr-anchors`: 84 → 84 anchored files. Decision-letter citations on the anchored surface go **1,947 → 1,942** at `ce18e73483` → head, measured exactly at both ends (1,954 → 1,949 at `798d480269` before the rebuild: the same 5). - The 5 that leave are letter citations in comments inside `searchAll`'s moved body: `ADR-0110 D3` ×2, `ADR-0079 D3`, `ADR-0131 D6` and `ADR-0090 D2`. - `protocol.ts`'s anchor entry governs ADR-0005/0029/0087/0119/0126. None of its paragraphs describes these bodies (read paragraph by paragraph), so the new module gets no entry and those citations leave the anchored surface unedited. - **Bracketed-refusal pin** (`PRODUCERS`). No `new Error(` site moves: `protocol.ts` is 87 → 87 and the new module has 0, so no row is added. - **Source scans.** The package's source scans that read `./protocol.ts` all pass at head, including `read-verb-canonical-fold` (the reason `getMetaItemCached` stays, above). ## Verification - `tsc --noEmit` (package) exit 0. `eslint` on `protocol.ts` and `protocol-record-reads.ts` exit 0. - **`packages/metadata-protocol` suite**, base and head: 21,489 tests each (21,470 passed, 0 failed, 19 skipped), 266 files. Compared per test name and status: 0 differences. - **Tests outside the package that read `protocol.ts` by path**, run at head, all pass: - `core` `row-serving-door-exposure.pin` and `second-object-read-exposure.pin` (7); - `objectql` `core-boundary.ratchet` (2); - `spec` `metadata-type-registry-allow-org-override-absence` and `scripts/liveness/evidence` (44); - `client` and `rest` `discovery-double-retired-key` (3 + 2). - **Hand gates at base and head**, all exit 0 with identical output at both ends except the letter count above: `check-adr-anchors`, `check-adr-symbol-anchors`, `check-platform-checklist`, `check-system-context-census`, `check-tenant-audit-census`, `check-query-options-erasure-ratchet`, and spec `check-liveness`. - **Derived gates.** `dispatch-gates --commands --changed` derives 61 gates on the final tree (3 changed paths). All 61 exit 0 at head, and `--ran` reconciles 61 of 61 with 0 NOT-MEASURED. `check-adr-symbol-anchors` and `check-liveness` are not derived and were run by hand (above). The full list and the `--ran` reconcile are in the report on this PR. ## Carry-forward (unchanged) A public member whose return type is inferred stays in `protocol.ts` in this series; annotating its type would be a signature edit and needs a ruling. Later cuts skip `getDiscovery`, `getMetaTypes`, `getMetaItems`, `getMetaItemsForExecution`, `getMetaItem`, `getMetaItemForExecution`, `getUiView`, `findData`, `getData`, `validateData`, `createData`, `cloneData`, `updateData`, `deleteData` and `saveMetaItem`. Also staying, from this cut, are `probeRecord` (per-file erasure-baseline site) and `getMetaItemCached` (read by the #9157 pin by path). The inferred-return `.d.ts` still depends on `protocol.ts` byte offsets (cut 3's series note), so each cut measures it last. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01J8pUyVs5eas1AjMdReJM7C Co-authored-by: Claude <noreply@anthropic.com>
1 parent e073455 commit ba6912c

3 files changed

Lines changed: 796 additions & 603 deletions

File tree

‎packages/core/src/security/row-serving-door-exposure.pin.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,11 @@ type Classification =
7777

7878
/** Every non-test caller of the read admission, classified. */
7979
const CALLERS: Record<string, Classification> = {
80-
'packages/metadata-protocol/src/protocol.ts': {
80+
'packages/metadata-protocol/src/protocol-record-reads.ts': {
8181
kind: 'door',
8282
door: 'the cross-object search (`GET /search`, `searchAll`)',
8383
operation: 'search',
84-
decisionSite: 'packages/metadata-protocol/src/protocol.ts',
84+
decisionSite: 'packages/metadata-protocol/src/protocol-record-reads.ts',
8585
pin: 'packages/metadata-protocol/src/protocol.search-api-exposure.test.ts',
8686
},
8787
'packages/services/service-analytics/src/plugin.ts': {

0 commit comments

Comments
 (0)