Skip to content

Commit bf5855b

Browse files
refactor(metadata-protocol): move the bulk data writes (batchData, the *ManyData verbs) out of protocol.ts into protocol-batch-data.ts (#25467 cut 1) (#25494)
<!-- 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 1 of the `protocol.ts` split (#24586 ruling A, recorded in `6270097840`): the card's "batch and `*Many` data" section, the seat's suggested first slice (1,067 lines, 33 of them from the last 30 days). The claim is `6368300459`. Sixteen `ObjectStackProtocolImplementation` method bodies move unedited into a new module, `packages/metadata-protocol/src/protocol-batch-data.ts`: - public: `batchData` (with its ADR-0119 atomic path), `createManyData`, `insertManyData`, `updateManyData`, `deleteManyData`; - private: `runAtomicBatchData`, `runAtomicBatch`, `runBatchDataLoop`, `reconcileStoppedBatch`, `locateBatchCause`, `buildBatchDataResponse`, `buildRolledBackBatchResponse`, `runUpdateManyLoop`, `buildUpdateManyResponse`, `runDeleteManyLoop`, `buildDeleteManyResponse`. `triggerAutomation` sits between them in the class but is not batch code, and stays. 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, cut from `origin/main` at `ab510f42e7` (the claim's `ab510f4c43` is a typo for this commit). It names no issue, so the claim comment is the identity record. **Serial with the file's in-flight PRs.** At claim, none of the 21 open PRs touched `protocol.ts` (each PR's file list read against `origin/main`; #25451 touches only `protocol.runtime-authoring-gate.test.ts`). Re-measured just before this PR was opened, on all 18 open PRs: none touches `protocol.ts` (the Version Packages PR only `CHANGELOG.md` and `package.json` in this package). ## The shape (#23050 shape B, as #25222, #25223 and #25224 apply it) - The class keeps each method with its name, signature, docblock and position, because tsup emits members with their JSDoc into the `.d.ts`. Each body is now one host literal plus `fn.call(host, …)`, marked `[#25467 cut 1]`; `locateBatchCause` reads no member and is called directly. The `async` methods drop `async` and return the moved function's promise. - `BatchDataHost` is new. It 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), and the stub annotates its literal as `ThisParameterType<typeof fn>`. No cast, and no member becomes public or protected. - Methods go in as late-bound closures (`(...a) => this.m(...a)`), so a spy or double set on the instance is still the one a body reaches, and the moved bodies that call one another (`batchData` → `runAtomicBatchData` → `runAtomicBatch` → the loop → the builders) do so through the protocol, as before. The members passed this way: `assertObjectRegistered`, `assertRecordExists`, `probeRecord`, and the moved methods themselves. - The one field read, `engine`, goes as a getter, so each read happens when the body makes it (tests replace it on a constructed protocol). - The one caller of a moved method outside the moved code, `applyPublishBackfill` (`this.updateManyData(`), still calls the class method. **Module-level declarations.** - `mergeDroppedFieldEvents` and `rowOperationFailureFallback` are read only by the moved bodies. They move with them, unedited, docblocks included, and stay unexported. - `byIdUpdateRecord`, `rowRequiredIdError`, `toRowApiError`, `type BatchDataRowResult` and `type BatchDataLoopOutcome` are also read by code that stays, so they stay and gain `export` in place, the only edit to each. The module imports them back from `./protocol.js`. - `src/index.ts` exports from `./protocol.js` by name, so none of them is published. Measured below: the `.d.ts` and the export lists do not move. - Each value import is read only inside a function body, at call time, never while either module evaluates, so the module cycle is evaluation-safe. - The module imports `engineCanRollBack` and `recordNotFoundError` (`@objectstack/core`), `omitInternalFieldsFromWriteResponse` (`./write-response-internal-fields.js`) and the spec types the bodies name, from the same specifiers `protocol.ts` uses. `protocol.ts` keeps all its imports (each still has readers there). ## The published `.d.ts` does not change Under the series bar (ruling `6348891160`), measured on base `ab510f42e7` and on the head, both built against the same dependency tree (a full `turbo run build` at base, 77/77 tasks, then `pnpm --filter @objectstack/metadata-protocol build` at head): - Raw sha256 of `dist/index.d.ts` and `dist/index.d.cts`, base and head, all four the same: `8e1a224c7d5a800c65c0174f9bc2245b70e0bab2106ab8dbf8a4b2dedd4ca35f`. The raw diff is 0 lines. - Normalised sha256, base and head the same: `5b98a8e857554959f7561563d6a0742110c5d3d8ad6119065ed5a69b0bc06f95`. 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`: line 1348, `private engine;`, made public (`engine;`) gives raw `66d80e4206a14b2b7e76dc37496d347d9dd39f53c66d016966844e707992e5ac` and normalised `f40390e5c426e0c6e24523cd6908726d3f3cab87f9a4ac36df20992bec04da95`, both different 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. No changeset: `skip-changeset`, per ruling `6348891160`. ## Proof of move-only - **Bodies, by AST token** (recursive `getChildren` leaves, so template literals scan correctly): each moved function's parameters (less the added `this`), return type and body have the same token sequence as the base method, and each body line is the base line with four leading spaces removed. `async` matches on all sixteen. The two moved declarations are token-identical and their docblocks byte-identical. 3,982 tokens compared, 0 differ. Negative control: one changed token in `batchData` makes the check report a difference. - **Class side**: the class has 298 members at base and head. The 282 members that did not move are byte-identical to base. Each of the 16 stubs keeps its docblock and signature byte-identical, less `async`. - **Top level of `protocol.ts`**: 249 of the 256 base statements other than the class are byte-identical. The others: the five `export` additions above, and the two declarations that moved. The one new statement is the import of the new module. - **Size**: +1,093 / -735 over 4 files, under the 3,000-line manual-merge line. `protocol.ts` goes from 33,065 to 32,468 lines (each stub keeps its docblock and carries its host literal, so the file shrinks by less than the bodies' length); `protocol-batch-data.ts` has 944. ## Readers of the moved lines, migrated in this PR - **ADR anchors.** The `protocol.ts` entry in `scripts/adr-anchors/` carries an ADR-0119 paragraph that describes `batchData`'s atomic path, which now lives in the module. As in the #25223 and #25224 cuts, the module gets its own entry (`packages__metadata-protocol__src__protocol-batch-data.ts.json`) carrying ADR-0119 and that paragraph verbatim. The `protocol.ts` entry is unchanged, and ADR-0119 is still cited in `protocol.ts` outside the moved text. `check:adr-anchors`: 80 → 81 anchored files, decision-letter citations on the anchored surface 2,001 → 2,001. - **`protocol.bracketed-refusal-opener-absence.test.ts`.** It scans each producer file's source for refusals opening with a bracketed tag, with a per-file `new Error(` floor so a scan that finds nothing cannot pass for free. The moved text holds 4 `new Error(` sites, so the module joins `PRODUCERS` with a floor of 4. Without it those throw sites would have left the scan silently. `protocol.ts` keeps its floor of 5. - Read and not edited: - `docs/adr/0119-…md` anchors `protocol.ts#batchData` twice. The method shell stays, so it still resolves (`check-adr-symbol-anchors` at both ends below). - `protocol.write-response-internal-fields.tripwire.test.ts` enumerates `*Data` methods on the prototype at runtime; the stubs keep every name. - `protocol.read-verb-canonical-fold.test.ts` and the other source scans that read `./protocol.ts` (`register-item`, `lookup-artifact-item` and `view-expansion-reconcile` call-site pins, `query-transport-dialect`, `object-registry-write-through-spelling`, `hydrate-overlay-canonical-type`, the two recovery-door pins): none of their patterns occurs in the moved text, and all pass at head. - `scripts/engine-double-contract.seams.json` names `protocol.ts` rows for `deleteData` and `updateData` only, which stay. - `scripts/query-options-erasure-baseline.json` counts `protocol.ts` at 6: the moved text has no engine `find` / `findOne` / `count` / `aggregate` call, so no counted site moves. - No `isSystem` read sits in the moved text (system-context census). - The tenant-audit census places no `metadata-protocol` write site (238 sites at base and head, 0 in this package); it exits 0 at both ends with no `--write`. ## Verification - `tsc --noEmit` (package) exit 0; `eslint` on both source files exit 0. - `packages/metadata-protocol` suite: base 21,466 tests (21,447 passed, 0 failed, 19 skipped, 1,181 suites); head 21,467 (21,448 passed, 0 failed, 19 skipped, 1,181 suites). Compared per test name and status: no test lost, none changed status; the one added test is the new `PRODUCERS` row's case. - 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` (2) and `scripts/liveness/evidence` (42), `client` and `rest` `discovery-double-retired-key` (3 + 2); `spec check:liveness` passes. - Hand gates at base and head, all exit 0 at both: `check-adr-symbol-anchors` (2,384 anchors resolve at both), `check-adr-anchors`, `check-tenant-audit-census`, `check-platform-checklist`. - **After merging `origin/main` at `ea33919f56`** (#25451 had added a test file in this package; nothing else in the package moved): rebuilt the package closure (13/13), package `tsc` exit 0, suite 21,476 tests (21,457 passed, 0 failed, 19 skipped, 1,182 suites; the 10 added are #25451's file and the new `PRODUCERS` case), `dist/index.d.ts` still `8e1a224c…`. - `dispatch-gates --commands --changed`: 67 derived gates, each run at head with its exit code; the full list and the `--ran` reconcile are in the report on this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01J8pUyVs5eas1AjMdReJM7C --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6755432 commit bf5855b

4 files changed

Lines changed: 1093 additions & 735 deletions

File tree

0 commit comments

Comments
 (0)