Skip to content

Commit 17fe921

Browse files
tooling: a structural gate that keeps a registered facade class delegation-only (#30030)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB3aRxzDdhMeXMLzFsUS7ZdE)_ Part of #30019 This PR is the whole of that card. It says `Part of` rather than a closing keyword because this PR's head branch is a harness-pinned `claude/project-thread-*` branch, which `check:closing-target-claim` cannot match to the card's `Claim:`. The seat records the card done when this lands. ## 维护者速读 - 改了什么:新增门禁 `scripts/check-facade-delegation-only.mjs` 和登记表 `scripts/facade-delegation-registry.json`,接入 `lint.yml` 的 repo & tooling 车道。登记为 facade 的类,每个成员必须是一行委托(按 AST 语法判定),否则报出文件、成员和修法;例外行必须写明负责搬走它的卡号。 - 为什么改:裁决 6624258595 第 3 点点名这个门禁,替代 13,000 行门槛;6624345766 第 3 点给出带卡号的例外名单。 - 风险与回滚:登记表目前为空,所以合入后不拦任何文件,main 不会变红;`protocol.ts`/`engine.ts` 各自以 facade 形态落地后再加一行登记。回滚即 revert。 - 先看:① 脚本头部的委托语法表;② 下方两份实测(#29998 头 `bba9d24d`:277 个委托、30 个例外;#30012 头 `f48078bb`:251 个委托、30 个例外);③ 其中 5 + 3 个例外是门禁比两份报告更严的地方,需席位判断。 - 两分钟验证:`node scripts/check-facade-delegation-only.mjs --self-test`,预期 32 例全过;对 #29998 头运行 `--root <checkout> --registry <临时登记表>` 预期退出码 0。 - 需要你做的:改了 `.github/workflows/lint.yml`(Tier H),需要您批准;与两个 facade PR 同一窗口或紧随其后落地。 Clause-②: no Metadata-protocol: none _Keep one value on each of the two lines above, each line starting with its key. [Human Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum) item 5 says what they mean._ ## What changes - **`scripts/check-facade-delegation-only.mjs`** (new). It reads every class registered in the facade registry and refuses any member whose body is more than a delegation, naming the file, the line, the class, the member and the fix line. The delegation grammar is spelled over the AST in the script header (rationale there). In short: - A **forwarding call** is `ref(arg, …)` where `ref` is a name or a `.`-chain off a name or `this`, and every argument is a name, `this`, a literal, a `.`-chain, a spread, one level of nested forwarding call (`protocolHost(this)`), or a forwarding arrow. - A **method or accessor** body is one statement: `return <forwarding call>`, `<forwarding call>;`, `return <.-chain>`, or `<.-chain> = <argument>`. It may be preceded by `const` declarations of forwarding arrows or host object literals of forwarders, which is the hand series' `.call(host, …)` stub. - The **constructor** holds wiring only, the **static block** holds wiring calls only (forwarders, or object literals of them, which covers both generated host factories), a **field** initializer may hold a forwarding arrow but no block-bodied function, and a **parameter default** is data, not a computation. - Anything else is a body, including a member kind the gate does not know (fail closed). - **`scripts/facade-delegation-registry.json`** (new), two lists: `facades` rows `{ file, class, card }`, and `exceptions` rows `{ file, member, card, reason? }`. It starts empty (see *Landing order*). - **Registry refusals** (ruling 6624345766 item 3 and the card body): an exception row with no card or a card that is not `#<number>`, a row naming an unregistered file, a member the class does not have, a member that is already a delegation (its card landed, so the row must go), two rows for one member, and an unknown key are all red. - **No silent no-op**: a registered file that is missing, a registered class absent from its file (or declared twice), a registered facade that reads zero delegations, and a source that does not parse (`ts-parse.mjs`, exit 3) are all red. An empty registry passes, and its success line says in words that the gate guards nothing yet. - **Wiring**: `package.json` gains `check:facade-delegation-only` (self-test, then the gate), and `lint.yml`'s `Lint · repo & tooling gates` lane runs it as one tolerate-and-collect step beside `check:stack-collection-maps`. This makes the PR **Tier H**. - No changeset: root `scripts/`, `package.json` scripts and a workflow only; nothing publishes (`skip-changeset`). ## Why Ruling 6624258595 on #29465, item 3: 「once a file lands in facade form, a gate refuses a method body in the facade file (delegations only)」. It replaces the 13,000-line bar for those files. Ruling 6624345766 item 3 lets today's hot bodies stay as named exceptions, each with the card that will move it (#30020). ## Landing order The registry is empty in this PR, so the gate cannot red `main` whichever PR lands first. A file joins with one `facades` row only once it is in facade form on `main`: - If #29998 / #30012 land first, this PR merges `main` and adds both rows and their exception lists before it leaves draft. - If this PR lands first, each facade PR adds its own row (and its exceptions) in its landing commit. Either way the rows are measured from the landed head, never from an estimate. ## Verification ### Measured on `protocol.ts` facade head (#29998 `bba9d24d`, provisional) Run with a scratch registry holding one `facades` row and 30 exception rows (`--root <#29998 checkout> --registry <file>`), exit 0: | Verdict | Members | |:--|--:| | host-literal stub (`const host = {…}; return fn.call(host, …)`) | 161 | | host delegation (`return Fn(host(this), …)`) | 94 | | forwarding call (`return fn(…args)`) | 20 | | constructor wiring | 1 | | static host factory (`installProtocolHost(…)`) | 1 | | fields | 27 | | named exceptions (#30020) | 30 | | **total** | **334** | Without the exception rows each run is red with exactly its 30 members named. **The 30 exceptions against the #29985 report (6626233635):** 25 of its 26 listed members are refused by the gate, and its 26th, `static resetEnvWritableCache`, is already a one-line forwarding call, so it needs no row (a row for it would be refused as stale). The constructor and the static host factory, which the report also lists, pass as wiring. The gate refuses **5 members the report counts as delegations**, and these are for the seat to judge: - `applySeedBodies`: a hand-series host slot computes (`() => this.getServicesRegistry?.().get(FIELD_FILE_SERVICE) as …`, an optional call and a call on its result). - `static OVERLAY_ALLOWED_TYPES`, `static STATIC_REGISTRY_TYPES`, `static RUNTIME_CREATE_ALLOWED_TYPES`, `static OVERLAY_CAPABLE_TYPES`: each is a static table built by an initializer IIFE with a loop body. ### Measured on `engine.ts` facade head (#30012 `f48078bb`, provisional) Same run against a #30012 checkout, with one `facades` row (`ObjectQL`) and 30 exception rows, exit 0: | Verdict | Members | |:--|--:| | host-literal stub (facet `.call(host, …)` and hand-series `.call`) | 217 | | forwarding call | 28 | | state write (`(this as any)._x = v`) | 3 | | state read (`return (this as any)._x`) | 2 | | static statics host (`Object.defineProperties(ObjectQLStatics, …)`) | 1 | | fields | 67 | | named exceptions (#30020) | 30 | | **total** | **348** | **The 30 exceptions against the #29986 report (6626513858):** 27 of its 31 listed members are refused by the gate. Its other 4 (`setHookMetricsRecorder`, `setStrictHookBinding`, `setWarnLegacyHandler`, `getHookMetricsRecorder`) are one-line state writes and reads, so they need no row. The gate refuses **3 members the report does not list**, for the seat to judge: - `constructor`: `this.logger = hostContext.logger || createLogger(…)` and an `if` on `process.env.OBJECTQL_STRICT_HOOKS`. - `projectHomeAmountsOntoPatch`: its parameter default calls `this.homeAmountServing(context)`. - `serveReferenceNames`: a hand-series host slot holds a `try` block. `ObjectRepository` and `ScopedContext` in the same file are not facades and are not judged. Both provisional registries and their union are in the project files (`issue-30019/registry-provisional-protocol.json`, `registry-provisional-engine.json`, `registry-final-both.json`). ### Self-test and gates - `node scripts/check-facade-delegation-only.mjs --self-test`: 32 cases in 32 pinned batteries (12 harm, 6 accepted shapes, 8 exception rules, 6 anti-no-op), with the battery-floor and verdict handshake copied from `check-agent-model-declared.mjs`. - Ablation: with the body check short-circuited (`judgeDelegationBody` accepts everything), the self-test goes red on 9 cases (H1–H5, H9, H11, H12 and E1, whose allow-listed row then reads as stale). Restored, 32 pass. It ran in a throwaway checkout holding the committed blob (`git hash-object` equal). - `dispatch-gates --commands`: 65 derived. At PR open, 42 have run: all exit 0 except `check:entry-guard`, which was red on the first commit (a top-level loop in an exporting script) and is green after the fix in this PR, and `check:dts-closure`, `check:dual-build-cjs-loads` and `check:lean-entry-closure`, which exit 3 (they need built package dist; this diff touches no package). The rest of the sweep is reported on the card. ## Acceptance notes - **Outside the gate by the ruling's wording ("a method body"):** top-level functions in the facade file and other classes in it. Measured on #29998's head: `protocol.ts` holds two top-level functions (`packagePersistFailureMessage`, `packagePersistFailureError`, 24 lines) and one class. - The #29985 report's finding that 157 private hand-series stubs carry 4,017 docblock lines on the class is not this gate's concern: a docblock is not a body. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [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. (publishes nothing) - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (not touched) - [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 with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WGufzfcbZEJQm3rajN7rri --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent fbbbaee commit 17fe921

4 files changed

Lines changed: 1514 additions & 0 deletions

File tree

‎.github/workflows/lint.yml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,16 @@ jobs:
991991
continue-on-error: true
992992
run: pnpm check:stack-collection-maps
993993

994+
# A class split into facade form stays a facade (#30019, ruling
995+
# 6624258595 on #29465 item 3): every member of a class registered in
996+
# `scripts/facade-delegation-registry.json` is a delegation to its facet,
997+
# bar the named exceptions, each carrying the card that will move it.
998+
# Rationale and the delegation grammar: the script's own header.
999+
- name: Registered facade classes hold delegations only
1000+
id: facade-delegation-only
1001+
continue-on-error: true
1002+
run: pnpm check:facade-delegation-only
1003+
9941004
# The dashboard widget `options` census vs the spec (#12926). The parser's
9951005
# `CONSUMED_WIDGET_OPTION_KEYS` decides which `options` keys get a
9961006
# `unconsumed-widget-option` warning, and five of its six members are the

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"check:parse-guard": "node scripts/check-parse-guard.mjs --self-test && node scripts/check-parse-guard.mjs",
3939
"check:bash32-floor": "node scripts/check-bash32-floor.mjs --self-test && node scripts/check-bash32-floor.mjs",
4040
"check:stack-collection-maps": "node scripts/check-stack-collection-maps.mjs --self-test && node scripts/check-stack-collection-maps.mjs",
41+
"check:facade-delegation-only": "node scripts/check-facade-delegation-only.mjs --self-test && node scripts/check-facade-delegation-only.mjs",
4142
"check:widget-option-census": "node scripts/check-widget-option-census.mjs --self-test && node scripts/check-widget-option-census.mjs",
4243
"check:sdui-reference": "node scripts/gen-sdui-reference.mjs --self-test && node scripts/gen-sdui-reference.mjs --check",
4344
"gen:sdui-reference": "node scripts/gen-sdui-reference.mjs",

0 commit comments

Comments
 (0)