Repository navigation
Commit 17fe921
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
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
991 | 991 | | |
992 | 992 | | |
993 | 993 | | |
| 994 | + | |
| 995 | + | |
| 996 | + | |
| 997 | + | |
| 998 | + | |
| 999 | + | |
| 1000 | + | |
| 1001 | + | |
| 1002 | + | |
| 1003 | + | |
994 | 1004 | | |
995 | 1005 | | |
996 | 1006 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| 41 | + | |
41 | 42 | | |
42 | 43 | | |
43 | 44 | | |
| |||
0 commit comments