Skip to content

Commit 7145bf3

Browse files
fix(metadata-protocol): move findData into its facet; host-split refuses a method read as a value, so getDiscovery stays (#30838)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DEaLDouphrUMvBWbK52b3uh)_ Fixes #30518 Refs #30054 Refs #30147 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:用 `--members` 把 `findData` 的方法体搬进 `ProtocolDataCrudFacet`,删掉它的门面例外登记;`host-split.mjs` 新增一条规则:实例方法若不是被调用、而是当作值读取,就拒绝搬移;`getDiscovery` 因此留在类上,登记行改挂 #30147。 - 为什么改:首版也搬了 `getDiscovery`,但它用 `typeof this.searchAll === 'function'` 算出 `capabilities.search`;搬走后宿主条目是一个转发闭包,永远是函数,于是不能搜索的协议也会宣称可以搜索(rest 的 `discovery-search-capability-agreement.test.ts` 抓到)。 - 风险与回滚:纯搬移加一条拒搬规则,方法体逐字节不变,公开 `index.d.ts` / `index.d.cts` 前后字节相等;回滚即 revert 本 PR。 - 先看:`host-split.mjs` 新增的 4 行与 `host-split.test.mjs` 的两个用例;`scripts/facade-delegation-registry.json` 的 getDiscovery 行;`protocol.query-transport-dialect.test.ts` 的 4 行。 - 两分钟验证:`node scripts/codemod-split-protocol.mjs --members --verify --base b618224`,预期 `1 moved method(s) (findData)`、退出码 0;`node scripts/codemod-split-protocol.mjs --members getDiscovery` 预期退出码 2,理由为 `instance method \`searchAll\` read as a value`。 - 需要你做的:无,席位落地。 ## What changes **The move.** `findData` (751 body lines) now delegates in one line to `ProtocolDataCrudFacet`; its signature and docblock stay on the class. `protocol-host.ts` gains the eight entries its body reads (`searchAll` and seven `assert*` field guards). `protocol.ts` goes from 13,775 to 13,007 lines and drops 28 import bindings only the moved body read. Written by `node scripts/codemod-split-protocol.mjs --members findData --write`, no hand edit after it. Its registry row leaves. **The rule.** `host-split.mjs` refuses an instance method read other than as a callee (`typeof this.m`, `const f = this.m`): a host entry forwards a method through a closure, which is always a function, so the moved read would see the closure, not the instance's member. This mirrors the rule it already had for a private static method read as a value. `host-split.test.mjs` gains two refusal cases (the `getDiscovery` probe shape and a plain value read); both fail without the rule. Over the first pass's own `--verify --base 48914fd^`, the rule changes exactly one member's verdict, `getDiscovery`. **`getDiscovery` stays.** Its body sets `capabilities.search` from `typeof this.searchAll === 'function'` (#7541). The first version of this PR moved it, and `packages/rest` `discovery-search-capability-agreement.test.ts` went red (2 of 4): a protocol that shadows `searchAll` with `undefined` advertised search while `/search` answered 501. Its row returns to `scripts/facade-delegation-registry.json` under #30147 (the codemod-limit rows card), with that reason; the `liveness/qa.json` citation stays on `protocol.ts#getDiscovery`. **The path reader.** `protocol.query-transport-dialect.test.ts` ("the fold names the spec export at the call site") read `protocol.ts` by path for `findData`'s call site; it now reads `protocol.ts` and `protocol-data-crud.ts` joined. **The citations** that moved with the body: `packages/spec/liveness/query.json` and `docs/qa/platform-checklist/areas/search.json` (`protocol.ts#findData` -> `protocol-data-crud.ts#findData`), and `docs/qa/platform-checklist/areas/api-backend.json` (`protocol.ts#UNSUPPORTED_QUERY_PARAM` -> `protocol-data-crud.ts#UNSUPPORTED_QUERY_PARAM`, a string token inside `findData`'s body that `check-platform-checklist.mjs` reported as ABSENT SYMBOL). ## Why #30054 spelled the eight doors' return types (PR #30516) so their bodies could leave the facade; #30518 asked for the pass that moves them. PR #30687 added the pass and moved six; this PR moves `findData` and closes #30518 with `getDiscovery` a named codemod-limit exception carried by #30147. ## Tests - `--members --verify --base b618224` at `0df9bd1d`: exit 0, 1 moved, `checkMove` 1/1. `--members getDiscovery`: exit 2 with the new reason. - `node scripts/codemod-split-protocol.mjs --self-test`: 85 cases in 10 batteries pass. `host-split.test.mjs`: 40 pass; 2 fail with the rule removed (negative control). - `@objectstack/metadata-protocol` at `0df9bd1d`: vitest 320 files passed, 4 skipped; 22,487 tests passed, 26 skipped. `typecheck` exit 0. - `packages/rest` `discovery-search-capability-agreement.test.ts`: 4/4 at `0df9bd1d` (2/4 at `946d40b7`). Full `packages/rest` locally: 12 failures, all `_migrate-record-currency-codes` rows of `rest-write-route-hook-refusal-sentence.ledger.test.ts`, the same 12 with this PR's `src` reverted (a local build artifact of #30809's packages; CI's verdict on the new head decides). - `check:liveness` exit 0 and its tests (359) pass; `check:platform-checklist` exit 0; `check-facade-delegation-only.mjs` exit 0 (46 named exceptions). ## Verification <details><summary>Declaration files</summary> `pnpm --filter @objectstack/metadata-protocol build` at base `b6182249` and at `0df9bd1d`: `dist/index.d.ts` and `dist/index.d.cts` `cmp` byte-equal (sha256 `5cf3216a10daa533…` on both sides). </details> <details><summary>dispatch-gates</summary> `node scripts/pm/dispatch-gates.mjs --commands` at `0df9bd1d`: 80 families derived, all 80 exit 0 (exit codes captured before any pipe). `--ran`: 80 derived, 80 run, 0 NOT-MEASURED, 0 UNRUN. </details> <details><summary>Changed lines</summary> 1,626 changed lines (+834 / −792, 11 files, changeset included), under the 3,000 budget. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4cweKLi1i8z12wPbsnw)_ <!-- signed: domain:devx#3 · os-elon-musk · session_016WV4cz3RTQjLh7hZBLzgtZ --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent cf478ea commit 7145bf3

11 files changed

Lines changed: 834 additions & 792 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
'@objectstack/spec': patch
4+
---
5+
6+
refactor(metadata-protocol): `findData` hands its body to its facet
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
- `ObjectStackProtocolImplementation#findData` keeps its signature and docblock on the class and delegates in one line to `ProtocolDataCrudFacet`, which now holds the body, moved unchanged. The built `index.d.ts` and `index.d.cts` are byte-equal before and after, and no behaviour changes. Nothing to do on upgrade.
12+
- `getDiscovery` stays on the class: its body reads `typeof this.searchAll === 'function'`, which a forwarded host method would always answer true. The move tool now refuses a method read as a value.
13+
- `@objectstack/spec`'s published liveness ledger (`liveness/query.json`) cites `findData` at its new file.

‎docs/qa/platform-checklist/areas/api-backend.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@
525525
"source": [
526526
"packages/spec/src/data/filter.zod.ts#AST_OPERATOR_MAP (AST_OPERATOR_MAP / FieldOperatorsSchema — the operator variant source)",
527527
"packages/spec/src/data/query.zod.ts#QueryAST (QueryAST keys)",
528-
"packages/metadata-protocol/src/protocol.ts#UNSUPPORTED_QUERY_PARAM (supported $-params + UNSUPPORTED_QUERY_PARAM #2926 ⑩, INVALID_FIELD #4134)",
528+
"packages/metadata-protocol/src/protocol-data-crud.ts#UNSUPPORTED_QUERY_PARAM (supported $-params + UNSUPPORTED_QUERY_PARAM #2926 ⑩, INVALID_FIELD #4134)",
529529
"packages/spec/src/data/filter.zod.ts#checkFilterConditionComparands (the request-schema refusal of a scalar list comparand on the POST door, #20116 — the $-object spelling, located at query.where.<field>.$nin)",
530530
"packages/spec/src/data/data-engine.zod.ts#lowerFoldedTransportValues (the AST/FilterArray spelling on the POST door, refused at query.where when it cannot be lowered)",
531531
"packages/spec/src/data/data-engine.zod.ts#QueryTransportParamsSchema (`filter` is a declared transport spelling of `where` — why negative[0] no longer probes with it)",

‎docs/qa/platform-checklist/areas/search.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@
282282
"examples/app-showcase/src/security/permission-sets.ts#invoice_own_rows (invoice_own_rows: owner == current_user.email, positions ['contributor'])",
283283
"examples/app-showcase/src/data/seed/index.ts (INV-1001..1012 owner spread; 'ada sees INV-1001/1002's lines but never linus's INV-1003')",
284284
"packages/objectql/src/engine.ts#expandSearchOnAst (expandSearchOnAst ANDs into ast.where; count() rides the read middleware — the #2737 total-leak fix)",
285-
"packages/metadata-protocol/src/protocol.ts#findData (the paged arm counts a searched list: engine.count is handed the query's search and searchFields, so total is the counted number of matches and hasMore agrees with it; the `countable` exception and its page-local estimate were deleted by #22790, PR #22857) + packages/rest/src/list-search-total-door.test.ts ('[#22790] a paged search reports the counted total', including 'a member counts only the matches it can read')"
285+
"packages/metadata-protocol/src/protocol-data-crud.ts#findData (the paged arm counts a searched list: engine.count is handed the query's search and searchFields, so total is the counted number of matches and hasMore agrees with it; the `countable` exception and its page-local estimate were deleted by #22790, PR #22857) + packages/rest/src/list-search-total-door.test.ts ('[#22790] a paged search reports the counted total', including 'a member counts only the matches it can read')"
286286
],
287287
"history": [
288288
{ "revision": 1, "date": "2026-08-07", "change": "new item: RLS × $search both-personas contract; total-leakage clause grounded in the protocol's page-local total for searched lists and the #2737 count middleware fix, persona provisioning gap recorded honestly", "ref": "claude/platform-test-checklist-ocwugl" },

‎packages/metadata-protocol/scripts/host-split.mjs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ export function rewriteSites(sf, method, table, className = CLASS_NAME) {
186186
const member = table.instance.get(name);
187187
if (!member) { refuse(node, `\`this.${name}\` names no declared instance member`); return; }
188188
if (table.statics.has(name)) { refuse(node, `\`${name}\` is both an instance and a static member: the host cannot carry both`); return; }
189+
// [#30518] A method's host entry is a forwarding closure, always a function, so a
190+
// method read other than as a callee (`typeof this.m`, `const f = this.m`) would
191+
// read the closure, not the instance's member. Same rule as a private static below.
192+
if (member.kind === 'method' && !isCallee(p)) { refuse(node, `instance method \`${name}\` read as a value: the host carries a method as a forwarding closure, so only a call is in the closed set`); return; }
189193
sites.push({ start: node.getStart(sf), end: node.getEnd(), text: HOST_PARAM, kind: 'member', member: name });
190194
useInstance(name, isAssignmentTarget(p) ? 'write' : isCallee(p) ? 'call' : 'read');
191195
ts.forEachChild(p, (c) => { if (c !== node) visit(c); });

‎packages/metadata-protocol/scripts/host-split.test.mjs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ line two \${k}\`;
5454
namedHost() { const host = 1; return host; }
5555
staticField() { return ${C}.LIMIT; }
5656
staticAsValue() { const f = ${C}.helper; return f(1); }
57+
probesMethod() { return { search: typeof this.read === 'function' }; }
58+
methodAsValue() { const f = this.read; return f('k'); }
5759
bareConstructor() { return this.constructor; }
5860
undeclared() { return this.nope; }
5961
*generator() { yield this.count; }
@@ -142,6 +144,9 @@ describe('host-split: the move', () => {
142144
['namedHost', 'already named `host`'],
143145
['staticField', 'private static `LIMIT` (field) read'],
144146
['staticAsValue', 'private static `helper` (method) read as a value'],
147+
// [#30518] `getDiscovery`'s shape: a host entry forwards a method through a closure, always a function.
148+
['probesMethod', 'instance method `read` read as a value'],
149+
['methodAsValue', 'instance method `read` read as a value'],
145150
['bareConstructor', '`this.constructor` not followed by `.<static>`'],
146151
['undeclared', '`this.nope` names no declared instance member'],
147152
['generator', 'a generator method'],

0 commit comments

Comments
 (0)