Repository navigation
Commit ba6912c
<!-- 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
- metadata-protocol/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
77 | 77 | | |
78 | 78 | | |
79 | 79 | | |
80 | | - | |
| 80 | + | |
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
84 | | - | |
| 84 | + | |
85 | 85 | | |
86 | 86 | | |
87 | 87 | | |
| |||
0 commit comments