Skip to content

tooling: a structural gate that keeps a registered facade class delegation-only - #30030

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-hv5n7g
Nov 4, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-hv5n7g

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

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。
  • 先看:① 脚本头部的委托语法表;② 下方两份实测(refactor(metadata-protocol): generate the protocol.ts section split with a deterministic codemod #29998 头 bba9d24d:277 个委托、30 个例外;refactor(objectql): generate the engine.ts section split with a deterministic codemod #30012 头 f48078bb:251 个委托、30 个例外);③ 其中 5 + 3 个例外是门禁比两份报告更严的地方,需席位判断。
  • 两分钟验证:node scripts/check-facade-delegation-only.mjs --self-test,预期 32 例全过;对 refactor(metadata-protocol): generate the protocol.ts section split with a deterministic codemod #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
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:

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

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (publishes nothing)
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzfcbZEJQm3rajN7rri

WIP: the gate, its self-test and an empty facade registry.

Claude-Session: https://claude.ai/code/session_01WGufzfcbZEJQm3rajN7rri
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#1 · os-support-ai · session_01WGufzgBDcBNVmSmo7VFHpj

Stamp: 2026-11-04T09:13Z

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c022291d3094eeeba143287d10e236f8144d41f8
Local-runs: none
Scope: .github/workflows/lint.yml + scripts/check-facade-delegation-only.mjs, scripts/facade-delegation-registry.json, package.json (the check:facade-delegation-only script line), scripts/ts-parse.mjs (read), scripts/ci/lane-verdict.mjs (read via the lane's log), packages/metadata-protocol/src/protocol.ts at PR #29998 head bba9d24d (read), packages/objectql/src/engine.ts at PR #30012 head f48078bb (read)

Inputs: card #30019 (body and all three comments: triage 6624652906, claim 6626207129, os-dev-report 6626941227), rulings 6624258595 and 6624345766 on #29465, PR #30030 body, file list (4 files, +926/-0) and net diff against main at merge-base 63b86b385, and the check-runs on the head. The dispatch order and the dispatching seat's conclusions were not inputs.

Gate verdicts on the head: every non-skipped check-run concluded success (55 of them, Governed Surface Queue Guard, Check Changeset, PR-body guards, Lint · ESLint, Lint · PM dispatch-gates, Lint · repo & tooling gates, Lint & Repo Gates, Test Core, Test UI, TypeScript Type Check included). The 12 skipped rows are opt-in harnesses, a second-trigger duplicate of Auto Label/Check Changeset/Check PR Size, the schedule gate and PR-irrelevant live suites. The Lint · repo & tooling gates job log shows the new step ran (Registered facade classes hold delegations only), its self-test printed 32 cases pass, the gate printed its empty-registry line, and the Lane verdict roster lists facade-delegation-only: ok.

① Derived judgments

Accept-set and public-surface changes the diff implies, each judged:

  • New gate scripts/check-facade-delegation-only.mjs. Accept-set for a registered facade class: a member is one of (a) return of a forwarding call, (b) a bare forwarding-call statement, (c) return of a .-chain (state read), (d) .-chain = argument (state write), each optionally preceded by const declarations whose initializers are forwarding arrows or host literals of forwarders; (e) a constructor of super(...), this.x = argument, this.x = new Ref(args) and forwarding calls; (f) a static block of calls whose arguments are forwarders or object literals of forwarders; (g) a field whose initializer holds no non-forwarding function; (h) a parameter default that is data. A forwarding call's callee is a name or a ./[name|literal] chain off a name or this with no ?.; arguments are names, this, literals, chains, spreads, one nested forwarding call, or forwarding arrows; as/!/satisfies/parens are transparent; await is stripped on the returned expression. Everything else, an unknown member kind included, is a body. Right: read against the codemod output (below) every generated shape is inside the set and each refused member is a real computation.
    • Read against protocol.ts at bba9d24d: the hand-series stub (const readX = () => this.x; const host = { f: (...a) => this.f(...a), get x() { return readX(); } }; return fn.call(host, ...)), the facet delegation return ProtocolClassHeadFacet.m(protocolHost(this), ...), the constructor (this.engine = engine as T, resolveQueryReadLimits()), and the static installProtocolHost((protocol) => ({ ... get/set accessors over protocol ... })) all parse under the grammar as written. The five members the gate refuses beyond the Codemod split: generate the rest of the metadata-protocol protocol.ts split with a deterministic script, verified byte-identical #29985 report are computations: applySeedBodies (a host slot with an optional call and a second-level call), and four static tables built by IIFEs. Refusing them is correct under the ruling; they take rows or move.
    • Read against engine.ts at f48078bb: the facet .call(host, ...) stub, the Object.defineProperties(ObjectQLStatics, { X: { get: () => ObjectQL.X }, y: { value: (...args) => ObjectQL.y(...args) } }) static block, and the (this as any)._x = v / return (this as any)._x accessors parse under the grammar. The three members refused beyond the Codemod split: generate the objectql engine.ts split with a deterministic script, verified byte-identical #29986 report are computations too: the constructor (|| fallback and two ifs on process.env), projectHomeAmountsOntoPatch (a parameter default that calls this.homeAmountServing), serveReferenceNames (a host slot holding a try). Correct refusals.
    • Only the registered class is judged. Top-level functions and other classes in the file are outside the accept set and outside the refusal set alike. Declared, and a hole: see ④ should-fix 1.
  • New registry scripts/facade-delegation-registry.json, two empty lists. The accept set for a facades row is {file, class, card} with card matching # plus digits; for an exceptions row {file, member, card, reason?} with the same card rule; unknown keys, duplicate files, duplicate members, rows on unregistered files, rows on absent members and rows on members that already read as delegations are all refused. Right, and tighter than the card asked (a facade row needs its card too, N4).
  • package.json script check:facade-delegation-only: --self-test && gate, the sibling shape of check:stack-collection-maps. Right.
  • lint.yml: one new step in lint-repo-tooling with a unique id and continue-on-error: true, no if: of its own, so it runs on pull_request, merge_group, push to main, schedule and workflow_dispatch exactly as its neighbours (the job's only if is the schedule gate). Right.
  • Public surface: none. No package source, export, schema or spec key moves.

② Semver level

The diff publishes nothing from any released package: root scripts/, a root package.json script and a workflow. skip-changeset is the right label and Check Changeset concluded success. No changeset file, so no changeset body lines are owed. The PR body carries Clause-②: no and Metadata-protocol: none, each line-initial with one value. Clause-②: no — matches the diff (no capability widened or narrowed for a metadata author or a consumer).

③ Boundary flags

The dev's flags are in the os-dev-report (6626941227) and the PR body's acceptance notes; open_questions is not a key in that report, its deviations and the "for the seat to judge" lists are the open items.

  1. Part of #30019, not a closing keyword (harness-pinned claude/project-thread-* branch vs check:closing-target-claim). Answered: correct under the guard; the seat records the card done on landing. The PR-body guards check concluded success.
  2. Registry ships empty. Answered: right, and the landing order in the PR body (whichever of refactor(metadata-protocol): generate the protocol.ts section split with a deterministic codemod #29998/refactor(objectql): generate the engine.ts section split with a deterministic codemod #30012 lands second adds the rows, measured from the landed head) is the order that cannot red main. The gate's N6 case runs the committed registry against the checkout on every CI run, so a row added ahead of its facade reds that PR, not main.
  3. Every provisional exception row cites refactor: move shared state out of the protocol.ts and engine.ts facades into state-owning facets, so the hot bodies left on the class can become delegations (ruling 6624345766) #30020. Not in this diff, so not judged here. Escalated to the row-adding PR: rows whose reason is not shared state (the four IIFE static tables in protocol.ts, the computed parameter default and the env ifs in the ObjectQL constructor, the try in serveReferenceNames) should either move in that PR or cite a card that will move them; refactor: move shared state out of the protocol.ts and engine.ts facades into state-owning facets, so the hot bodies left on the class can become delegations (ruling 6624345766) #30020 (state-owning facets) does not own them. The gate itself is indifferent to which card, by design.
  4. Scope: only the registered class is judged. Escalated as ④ should-fix 1 below; a seat or maintainer word is owed on whether the ruling's "a method body in the facade file" is read as "no body in the facade file".
  5. No assignee or reviewer on the PR. Process, not tree; the seat sets them.
  6. The 5 + 3 members the gate refuses beyond the two reports. Judged in ①: each is a computation; refusing is correct, the gate is right and the reports were lenient.
  7. Tier. lint.yml makes the PR Tier H. This record is the contract review on the head; it does not lift the Tier H landing, which stays with the maintainer's approval.

④ Design judgment

hits file: .github/workflows/lint.yml, read as one change with the gate, registry and script line it wires.

Does the gate refuse a method body in a registered facade file? Yes. judgeDelegationBody refuses an empty body, any non-const statement before the last, any const whose initializer is not a forwarder, and any last statement that is not a forwarding return, forwarding call or state write; the pins H1 to H12 cover a branch, a loop, a computed argument, a two-deep call, an inline object literal, a block-bodied arrow in a field, control flow in a constructor, a non-call static block, an empty body, a computed default and a computing host slot.

Does it accept exactly the codemod shapes? Yes on the shapes read (① above, both heads), with the accepted widenings named in ① (self-calls, [name] callee links, nested-call depth resetting inside a forwarding arrow). The dev's measured runs (334 and 348 members, exit 0 with 30 rows each, red on exactly 30 without) are consistent with the grammar as read; not re-run here.

Does it refuse an exception row without a card? Yes (E2, E3), and a facade row without one (N4).

Does it fail rather than pass silently on a missing file or zero delegations? Yes: a missing file, a class declared 0 or 2 times, and zero delegations each push a problem (N1, N2, N3); an unparseable source exits 3 through scripts/ts-parse.mjs (parseSourceFile writes the refusal report and exits EXIT_UNPARSEABLE). The registry read is the one place that produces loadError, and main exits 1 on it. An empty facades list prints, in words, that the gate guards nothing.

Is the lint.yml wiring real? Yes. The step carries id: facade-delegation-only and continue-on-error: true; the lane's last step reads toJSON(steps) and the log on this head lists the step as ok; no family if: scopes it away on merge_group; with the registry empty the gate exits 0 on main, so the merge cannot red main. Self-test shape: named batteries pinned 1 each, roster floor 32, duplicate-label check, and the selfTestReachedVerdict handshake set after the success line with the dispatch refusing when unset, the check-agent-model-declared.mjs shape.

Findings (none must-fix):

  • should-fix 1 (grammar scope). A member may delegate in one line to a function declared at the top of the same file, and the gate passes: return helper(this, a) with helper a 500-line function in protocol.ts is a delegation under this grammar. The ruling replaces the 13,000-line bar for these files with the facade rule, so once that replacement is made in the line gate the file has no bound at all against growth through that door. Today the hole is latent (the registry is empty and the line bar is still in code). Close it before the first facades row lands: refuse new top-level function and class declarations in a registered facade file beyond a pinned allow-list (the two at bba9d24d), or carry an explicit maintainer word that top-level bodies are outside the rule.
  • should-fix 2 (field initializers). bodyInInitializer judges only function-shaped nodes, so a field initializer with arbitrary non-function logic (private readonly table = buildTable(REGISTRY).filter(Boolean)) reads as field. A5 pins new Map() and a + of literals as GREEN, so the choice is deliberate; the tighter rule is the one parameter defaults already get (isDataValue, plus new Ref(args) and a forwarding call).
  • should-fix 3 (refusal text). An exception row whose member is spelled without its static or get /set prefix is refused as "the row is stale; delete it", which is the wrong prescription for a spelling slip. Name the nearest member keys in that message.
  • nit. judgeDelegationBody has a ternary whose two arms are identical (ts.isReturnStatement(last) ? stripAwait(last.expression) : stripAwait(last.expression)); and isStateWrite accepts any reference on the left, so a setter writing a module-level binding (Facet.cache = v) passes as a state write.

Implemented-by: claude/project-thread-hv5n7g
Reviewed-by: session_01WGufzgBDcBNVmSmo7VFHpj

VERDICT: PASS

…mber key

Contract review follow-ups on the facade delegation-only gate:

- A forwarding call or `new` that reaches a function, class, enum or
  variable declared at the top level of the facade file itself is now a
  body: a delegation must reach another module. Self-test H13; ablating
  the check reds H13.
- An exception row whose member lacks its `static ` or `get `/`set `
  prefix is refused with "did you mean `static x`?". Self-test E9.
- A state write must hang off `this`, or off the instance parameter of a
  host factory arrow; a write to a module binding, an imported name or a
  caller's object is a body. Self-test H14, and A4 now covers a host
  factory setter.
- The awaited-return check reads through stripAwait.

Self-test: 35 cases, floor 35. Both facade heads (bba9d24, f48078b)
pass with their measured exception rows; no member forwards into its
own file.

Claude-Session: https://claude.ai/code/session_01WGufzfcbZEJQm3rajN7rri
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl and removed size/l labels Nov 4, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#1 · os-support-ai · session_01WGufzgBDcBNVmSmo7VFHpj

Stamp: 2026-11-04T09:25Z

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 146f59a08b7ada607b766c0fff3ea0e53cd4283e
Local-runs: none
Scope: .github/workflows/lint.yml + scripts/check-facade-delegation-only.mjs (the delta c022291d..146f59a0, one commit, +130/-14 in that file alone), scripts/facade-delegation-registry.json and package.json (unchanged since the PASS record 6627146481 on c022291d), packages/objectql/src/engine.ts at f48078bb and packages/metadata-protocol/src/protocol.ts at bba9d24d (read for same-file targets)

Delta review. The record 6627146481 on c022291d stands for everything the delta does not touch: lint.yml, the registry, the script line, the grammar outside the hunks below. This record judges the one commit against that record's three should-fixes and two nits. The dev's account (review-fixes-146f59a0.md) was read as the dev's account; the judgments below are from the diff and the cited code.

Check-runs on 146f59a0 at the time of this reading: 13 success (Governed Surface Queue Guard, PR-body guards, Check PR Size, Auto Label, Validate Package Dependencies, Type Check · workspace, Type Check · debt ledger, Build Core, Build UI Deps, Plan Test Core shards, Dogfood Regression Gate, Live E2E UI, filter), 8 skipped (opt-in harnesses, the schedule gate, the second-trigger Check Changeset, PR-irrelevant live suites), and 32 in_progress, among them the lanes that run the changed file: Lint · repo & tooling gates (the gate's step and self-test), Lint · ESLint, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Lint · CI, release & engine gates, Lint · docs & code guards, the two other scoped lanes, Type Check · source gates/consumer gates, Type Check UI, Lint UI, Build & E2E UI, Build Docs, Test Core (1/8, 2/8) and Test UI (1..16/16). Their conclusions are the gate verdicts and are not yet rendered; the queue guard's own "every check green" precondition covers them. On the previous head every one of these concluded success, and the delta touches one scripts/*.mjs file.

① Derived judgments

Accept-set changes the delta implies, each judged:

  • Narrowing: same-file forwarding targets are bodies. sameFileTargets collects the names of top-level function, class, enum and identifier-named const/let/var declarations in the facade file (the facade class removed); sameFileForward walks every call and new in a member and refuses the member when the callee chain's root is one of them, before any other judgment (fields included). Right as the direct door: return helper(this, a), helper.call(host, a) and new Local(...) are all refused; an imported facet is never in the set; Facade.k2() on the facade class itself is not (H13 pins all three). Read against the heads: protocol.ts at bba9d24d declares two top-level functions and no member calls either (both appear in docblocks only); engine.ts at f48078bb declares ObjectQLStatics (a const object, passed to Object.defineProperties as an argument, which the rule does not touch, correctly) and two further classes, and two ObjectQL members do construct one of them: buildHookApi (line 2910, return new ScopedContext(safeCtx, ...)) and createContext (line 13374, return new ScopedContext(carryAppAutomationElevation(...), this)). Both are already bodies under the old grammar and both are rows in the dev's provisional engine registry, so the measured outcome (exit 0, 30 rows) is unchanged. The dev's line "same-file forwarding: 0 members on either head" is wrong as stated: there are two, and under the new rule their refusal text now names ScopedContext. Consequence for refactor: move shared state out of the protocol.ts and engine.ts facades into state-owning facets, so the hot bodies left on the class can become delegations (ruling 6624345766) #30020 and the row-adding PR, not for this gate: when those two rows retire, ScopedContext (and ObjectRepository) must leave engine.ts, which is the facade form anyway.
  • Narrowing: a state write must hang off this or off a parameter of an enclosing arrow/function expression. isStateWrite now requires a property/element-access left side whose chainRoot is this or an identifier that isEnclosingHostParameter finds as a parameter of an arrow or function expression between the write and the class element boundary. Right: Facet.cache = v and target.x = v (a member's own parameter) are refused (H14), (this as any)._x = v still passes (unwrap reaches this), and protocol.clusterNodeId = value inside installProtocolHost((protocol) => ({ set ... })) passes through the host-parameter arm, which protocol.ts's static block needs (three setters at bba9d24d; A4 pins one). The walk works because ts-parse's parseSourceFile sets parent nodes.
  • Refusal text: nearest member key. When an exception row names no member, the keys that equal the row's member after stripping static , get or set are listed with "did you mean"; otherwise the stale-row text stands. Right (E9 pins static table and get size).
  • stripAwait ternary collapsed to one call. Behaviour unchanged.
  • Self-test: three batteries added (H13, H14, E9), A4 widened by one setter line, roster floor 32 → 35 with 35 declared. Shape unchanged (named batteries, roster floor, verdict handshake).
  • Header docblock: the setter rule, the same-file rule and the nearest-key note added; "What it does NOT judge" now says top-level bodies are not judged but cannot be forwarded to. Accurate against the code.
  • Public surface: none; the diff is one root scripts/*.mjs file.

② Semver level

Unchanged from 6627146481: nothing publishes, skip-changeset stands, Clause-②: no and Metadata-protocol: none on the PR body still match the diff. Clause-②: no.

③ Boundary flags

  1. should-fix 1 (same-file scope): closed for the direct door (call and new on a same-file declaration). A residual stays open, named in ④ as a new should-fix: aliasing.
  2. should-fix 2 (field initializers): left as is by the seat's call. Noted; it stands as a should-fix on the file, not a FAIL.
  3. should-fix 3 (refusal text): closed (E9).
  4. nit 1 (stripAwait): closed.
  5. nit 2 (state-write root): closed, with the host-parameter arm the protocol.ts static block requires.
  6. Registry commit held locally (dev's account: 75f6a21d, 2 facades, 60 rows, not pushed). Correct: the registry on this head is still empty, so main cannot red; the rows land only with or after the facades, as the PR body's landing order says.
  7. Dev's "0 same-file forwards" reading: corrected in ① (two, both excepted). No change to the gate's verdict on either head.
  8. Check-runs pending at reading (32 in_progress, listed above): this record's PASS judges the tree and the concluded runs; the landing waits on the rest going green, which the queue guard enforces on its own.

④ Design judgment

hits file .github/workflows/lint.yml is untouched by the delta; the step it wires now runs a stricter gate with the same exit contract (0 / 1 / 2 / 3), the same empty-registry success line and the same --self-test && gate script line, so the wiring judgment of 6627146481 holds unchanged: a real step with id and continue-on-error, on pull_request and merge_group, exit 0 on main while the registry is empty.

Does the delta close what it set out to close, without loosening anything? Yes. Every change is a narrowing of the accept set or a message improvement; no previously refused shape is now accepted. The host-parameter arm is the one addition to the accept set, and it admits exactly the setter shape the codemod's static host factory generates, scoped to an arrow or function expression parameter and stopped at the class element boundary so a member's own parameters do not qualify.

Findings (none must-fix):

  • should-fix 4 (residual of should-fix 1, aliasing). sameFileForward looks only at call and new roots, so a same-file declaration reached through an alias passes: const h = helper; return h(this, a) (the const is a forwarding declaration because a bare reference is a forwarder value), this.h = helper in constructor wiring followed by return this.h(a), or a field private readonly h = helper with the same call. Destructured top-level variables (const { a } = ...) are also not collected. Close it by refusing any value-position identifier reference to a same-file function, class or enum (and to a variable whose initializer is function-like) anywhere in a member, while keeping plain data variables such as ObjectQLStatics legal as arguments. Latent today: the registry is empty, and the evasion is deliberate rather than a shape the codemods or the hand series emit.
  • nit. isEnclosingHostParameter accepts any enclosing arrow's parameter, so return run((o) => { o.x = v; }) passes as a forwarding arrow with a one-statement state write through the caller's object. Negligible beside what a forwarding arrow may already call; recorded so nobody assumes the arm is host-factory-only.

Implemented-by: claude/project-thread-hv5n7g
Reviewed-by: session_01WGufzgBDcBNVmSmo7VFHpj

VERDICT: PASS

…red names

A facade member could still reach a top-level function of its own file
through an alias: `const h = helper; return h(this, a)`, a constructor
slot `this.h = helper`, or a field `h = helper`. A value-position
reference to a same-file function, class, enum or function-initialised
variable is now a body ("it takes a reference to `helper`"). Data
variables stay legal as arguments (`ObjectQLStatics`). Type positions,
and a parameter or local that shadows the name, are not references.
Destructured top-level names are now collected, so a call through one
is refused too.

Self-test H15 covers the three aliases and the destructured call, which
are red, and the data argument, the shadowing parameter and the
type-only use, which pass. Ablating the reference arm, the destructured
collection, the shadow check or the type-position check reds H15 alone.
36 cases, floor 36. Both facade heads keep their verdicts.

Claude-Session: https://claude.ai/code/session_01WGufzfcbZEJQm3rajN7rri
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#1 · os-support-ai · session_01WGufzgBDcBNVmSmo7VFHpj

Stamp: 2026-11-04T09:32Z

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e8ff61538320adf3c4b14aa893ae0eb57e987f0d
Local-runs: none
Scope: .github/workflows/lint.yml + scripts/check-facade-delegation-only.mjs (the delta 146f59a0..e8ff6153, one commit, +117/-17 in that file alone), scripts/facade-delegation-registry.json and package.json (unchanged since the PASS record 6627146481 on c022291d), packages/objectql/src/engine.ts at f48078bb and packages/metadata-protocol/src/protocol.ts at bba9d24d (read for same-file declarations)

Second delta review. The records 6627146481 (c022291d) and 6627372576 (146f59a0) stand for everything this commit does not touch. This record judges the one commit against the should-fix 4 of 6627372576 (aliasing of same-file declarations) and the nit left open by the seat. The dev's Round 2 account was read as the dev's account; the judgments are from the diff and the cited code.

Check-runs on e8ff6153 at the time of this reading: 8 success (Governed Surface Queue Guard, PR-body guards, Check PR Size, Auto Label, Validate Package Dependencies, Type Check · workspace, Type Check · debt ledger, filter), 8 skipped (opt-in harnesses, the schedule gate, the second-trigger Check Changeset, PR-irrelevant live suites), and 16 in_progress: Lint · repo & tooling gates (the gate's step and self-test), Lint · ESLint, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Lint · CI, release & engine gates, Lint · docs & code guards, the two other scoped lanes, Type Check · source gates, Type Check · consumer gates, Build Core, Build Docs, Build UI Deps, Plan Test Core shards, Dogfood Regression Gate, Live E2E UI; the Test Core and Test UI shards had not been planned yet. Their conclusions are the gate verdicts and are not yet rendered; the queue guard's own "every check green" precondition covers them. On both previous heads every one of these concluded success, and the delta touches one scripts/*.mjs file.

① Derived judgments

Accept-set changes the delta implies, each judged:

  • Narrowing: a value-position reference to a same-file code declaration is a body. sameFileTargets now returns a map of name → code (a top-level function, class or enum, or a variable whose initializer unwraps to an arrow, function or class expression) or data (any other variable, destructured names included via bindingNames). sameFileForward keeps the call/new rule for every name and adds: an Identifier naming a code target, not in a name position, not inside a type node, not shadowed, is a reach ("it takes a reference to"). Right. const h = helper, this.h = helper in the constructor, a field h = helper and a shorthand { helper } are all value positions and are refused (H15 pins the first three; the shorthand falls out of isNamePosition, which lists PropertyAssignment names but not ShorthandPropertyAssignment, correctly). Object.freeze(TABLE) with a data TABLE passes; s(helper: string) shadows and passes; y(x: typeof helper) is a TypeQuery under a type node and passes.
  • Narrowing: destructured top-level names are targets. const { picked } = makeHelpers() makes picked a data target, so return picked(a) is refused as a forward (H15). Right; classifying destructured names as data is the conservative choice (a call through them is refused, a bare reference is not).
  • Exclusions, read for false positives. isNamePosition covers property names on access and qualified names, declared names of properties, methods, accessors, signatures, enum members, variables, parameters, functions, classes, type parameters, binding elements and labels; a method or field of the facade that happens to share a top-level function's name is therefore not a reach. isInType walks up to the member and stops at any TypeNode other than ExpressionWithTypeArguments, so helper<T>(...) as an instantiation expression stays a value and x: typeof helper, as Local, satisfies T do not. isShadowed checks function-like parameters (the member's own included, because n === stop is tested after the node's own checks), block and case-clause declarations, for/for-in/for-of initializers and catch bindings, and never walks above the member. All read correct; the one shape not covered is a local type helper = ... or interface helper declared inside a member body, whose declared name is not in the name-position list and would read as a reference. Not a shape anyone writes in a facade; recorded as a nit.
  • Read against the heads. protocol.ts at bba9d24d: two top-level functions, both code; inside the class they occur only in docblock {@link} text, which ts.forEachChild does not visit, so no member is a reach (consistent with the dev's reading of 0). engine.ts at f48078bb: ObjectQLStatics is {} as ObjectQLStaticsHost, which unwrap reduces to an object literal, so it is data and its use as an argument of Object.defineProperties in the static block stays legal; ObjectRepository and ScopedContext are code; the two members that construct ScopedContext (buildHookApi, createContext) are the same two reaches named in 6627372576, both already bodies and both rows in the dev's provisional registry. Measured outcome unchanged on both heads per the dev's account, and nothing in the diff could have moved a non-excepted member of either head to red.
  • Self-test: H15 added (one battery, seven members, exactly four problems asserted by name and arm); floor 35 → 36 with 36 declared. Shape unchanged.
  • Header docblock: the same-file rule now states the alias arm, the data-variable allowance, the type-position and shadowing exclusions. Accurate against the code.
  • Public surface: none; one root scripts/*.mjs file.

② Semver level

Unchanged from 6627146481: nothing publishes, skip-changeset stands, Clause-②: no and Metadata-protocol: none on the PR body still match the diff. Clause-②: no.

③ Boundary flags

  1. should-fix 4 (aliasing): closed for every door named in 6627372576 (local const, constructor slot, field, destructured names), with the exclusions the fix needed to stay green on the codemod output (data variables as arguments, type positions, shadowing).
  2. any-arrow-parameter nit (state-write root): left as is by the seat's call. Stands as a nit, not a finding.
  3. Registry commit still held locally (dev's account: rebased as os30019-registry-commit-v3, exit 0 with 60 rows on a both-facades scratch root). Correct: the registry on this head is empty, main cannot red, rows land with or after the facades.
  4. Check-runs pending at reading (16 in_progress, listed above): this record's PASS judges the tree and the concluded runs; the landing waits on the rest going green, which the queue guard enforces on its own, and on the maintainer's Tier H approval for lint.yml.

④ Design judgment

hits file .github/workflows/lint.yml is untouched; the step it wires runs a stricter gate with the same exit contract, success line and --self-test && gate script line, so the wiring judgment of 6627146481 holds unchanged.

Does the delta close should-fix 4 without loosening anything? Yes. Every change narrows the accept set or refines a refusal message; no previously refused shape is now accepted. The three exclusions are precision devices, each bounded to the member and each pinned by an H15 arm (data argument, shadowing parameter, type-only use).

No must-fix. The remaining observations are evasion-grade and below the bar for another logic round:

  • nit (data-classified code). A top-level variable whose initializer is not itself a function expression but yields code (const helper = memoize(impl), const helpers = { run() { ... } }) is data, so an alias of it (const h = helpers; return h.run(a)) is not a reach, although a direct call through the name still is. Closing it would mean classifying object literals with function-valued properties and call-initialised variables as code; not worth a round while the registry is empty and neither codemod emits such a top-level binding.
  • nit (name positions). A type/interface declared inside a member body with a same-file function's name would read as a reference; add TypeAliasDeclaration/InterfaceDeclaration names to isNamePosition when the file is next touched.

Implemented-by: claude/project-thread-hv5n7g
Reviewed-by: session_01WGufzgBDcBNVmSmo7VFHpj

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#1 · os-support-ai · session_01WGufzgBDcBNVmSmo7VFHpj

Stamp: 2026-11-04T10:50Z

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f24cb61dd3d0dd0b47f9e3389c14cafeb7af4e9e
Local-runs: none
Scope: .github/workflows/lint.yml + scripts/facade-delegation-registry.json (the registry commit f24cb61d on the merge bc1e932c), scripts/check-facade-delegation-only.mjs and package.json (byte-unchanged since e8ff6153), packages/metadata-protocol/src/protocol.ts and packages/objectql/src/engine.ts on origin/main b0c2209d (read, every registered member)

Final delta review. The records 6627146481 (c022291d), 6627372576 (146f59a0) and 6627515582 (e8ff6153) stand for the gate, its self-test and its wiring. This record judges what e8ff6153..f24cb61d adds: the merge of origin/main (which now holds the landed facades, #29998 and #30012) and the registry commit that registers both facades with their 60 exception rows. The dev's account (exit 0 on both files, 36/36 self-test, red on exactly 60 without the rows) was read as an account; the judgments below are from the tree.

Check-runs on f24cb61d at the time of this reading: 11 success (Governed Surface Queue Guard, PR-body guards, Check PR Size, Auto Label, Validate Package Dependencies, Build Core, Build UI Deps, Plan Test Core shards, Dogfood Regression Gate, Live E2E UI, filter), 8 skipped (opt-in harnesses, the schedule gate, the second-trigger Check Changeset, PR-irrelevant live suites), and 34 in_progress, among them Lint · repo & tooling gates (the step that runs the gate against this registry and the landed files, and the N6 self-test case that does the same), Lint · ESLint, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, the other Lint and Type Check lanes, Type Check UI, Lint UI, Build & E2E UI, Build Docs, Test Core (1/8, 2/8) and Test UI (1..16/16). Their conclusions are the gate verdicts and are not yet rendered; the queue guard's "every check green" precondition and the maintainer's Tier H approval (lint.yml) cover the landing. Lint · repo & tooling gates on this head is the first CI reading of the gate with a non-empty registry, so its conclusion is the one that matters most here.

① Derived judgments

  • The merge changes none of the PR's own files. git diff origin/main f24cb61d is exactly the four files of the earlier records (lint.yml +10, package.json +1, the gate script +1127, the registry +376); the gate script and the script line are byte-identical to e8ff6153. Everything else in e8ff6153..f24cb61d under scripts/ (the two codemods, check-durability-degradation-log-level.mjs, and others) is origin/main's, not this PR's. Right.
  • The landed facades are the measured heads. Blob identity, read from git: protocol.ts on origin/main = 8ea5370d… = bba9d24d:protocol.ts; engine.ts on origin/main = 43616eff… = f48078bb:engine.ts; the PR head carries the same blobs. So every earlier reading of the gate against bba9d24d/f48078bb is a reading against the files this registry now governs. Right.
  • Registry shape. Top-level keys facades, exceptions only. Two facades rows: {protocol.ts, ObjectStackProtocolImplementation, #29985} and {engine.ts, ObjectQL, #29986}, each citing the card that landed the facade. 60 exceptions rows, 30 per file, keys file/member/card/reason only, no duplicate (file, member), every card exactly #30020, every reason a non-empty string. Each registered class is declared once in its file (read). Accepted by the registry rules as written.
  • Every row names a real member that is a body. All 60 member keys resolve to exactly one implementation in the landed class under the gate's key spelling (static prefix where the member is static, constructor), and each body was read:
    • protocol.ts (30): getDiscovery, findData, getData, cloneData, publishPackageDrafts, reconcileViewExpansions are multi-statement bodies (11 to 72 statements); saveMetaItem, publishMetaItem, rollbackMetaItem, deleteMetaItem are one return withRefusalAudit(arrow, { inline object with computed fields }, arrow), an inline object literal argument (H5's shape); packagelessOverlayWriteRefusal and namedBaseOverlayWriteRefusal end in return closed ? f(...) : null, a conditional; the nine static methods each hold a const with ??, an if, a throw or string concatenation; rethrowUnlessMetadataStoreUnprovisioned (if + throw), triggerAutomation (throw), objectExtensionItemRefusal (if + inline object), packagedBaseRefusal (try); applySeedBodies (a host slot () => this.getServicesRegistry?.().get(...)); the four static tables are IIFE initializers. None is a delegation; none is stale.
    • engine.ts (30): update, insert, delete, find, findOne, recomputeSummaries, resolveReadableIdsSecurity, bindHooks, readMigrationFlagRowMemoized, registerApp, registerHook, registerDriver, init, triggerHooks, destroy, resolveMasterDetailParent(s), probeReferencingRows, homeAmountServing are multi-statement bodies; createContext and buildHookApi construct the same-file ScopedContext; ownershipQueueService, seedAutonumber, referenceExists hold a try; inspectDanglingReferences passes an inline object of closures; setDatasourceMapping is a state write followed by a logger call (two statements, the first not a const); getStatus returns an object literal; constructor has a || fallback and ifs on process.env; projectHomeAmountsOntoPatch has a parameter default that calls this.homeAmountServing(context); serveReferenceNames has a host slot holding a try. None is a delegation; none is stale.
    • No over-broad row. A row covers one member key; it cannot cover a sibling, an overload set beyond the one implementation, or a member added later under another name. The members already read as delegations (for example static resetEnvWritableCache, setHookMetricsRecorder, getHookMetricsRecorder, setStrictHookBinding, setWarnLegacyHandler) carry no row, as the gate's stale-row rule requires.
  • The gate on main after the merge cannot red from this diff. The registry names two files that exist on main with the blobs the rows were measured against; each class is declared once; each file reads hundreds of delegations (so the zero-delegation refusal cannot fire); every body the grammar refuses on these blobs is either a row here or would have been red in the dev's measured run, whose exit 0 the Lint · repo & tooling gates lane on this head re-measures. If main moves one of the two files before the landing, the queue build re-measures and reds loudly rather than passing silently; that is the gate working.
  • Public surface: none.

② Semver level

Unchanged: nothing publishes (scripts/, root package.json scripts, a workflow, a data file under scripts/), skip-changeset stands, Clause-②: no and Metadata-protocol: none on the PR body match the diff. Clause-②: no.

③ Boundary flags

  1. Landing order kept. The rows landed only after both facades reached main (ruling 6624258595 item 4 and the PR body's order), measured from the landed blobs, not from the facade PR heads. Confirmed by blob identity.
  2. Every row cites refactor: move shared state out of the protocol.ts and engine.ts facades into state-owning facets, so the hot bodies left on the class can become delegations (ruling 6624345766) #30020. The ruling (6624345766 item 3) asks for "the card that will move it", and refactor: move shared state out of the protocol.ts and engine.ts facades into state-owning facets, so the hot bodies left on the class can become delegations (ruling 6624345766) #30020 is the state-owning-facets card. Of the 60 reasons, the bare-this and gate reading rows are state or wiring matters that refactor: move shared state out of the protocol.ts and engine.ts facades into state-owning facets, so the hot bodies left on the class can become delegations (ruling 6624345766) #30020 owns; the inferred-return-type, closed-set, static-reads-class, no-host-read, name-taken, host-entry-predicate, field-call and query-options erasure pin rows are codemod limitations named by the two split reports. The gate accepts any card by design; whether refactor: move shared state out of the protocol.ts and engine.ts facades into state-owning facets, so the hot bodies left on the class can become delegations (ruling 6624345766) #30020 is the right owner of the codemod-limitation rows is the refactor: move shared state out of the protocol.ts and engine.ts facades into state-owning facets, so the hot bodies left on the class can become delegations (ruling 6624345766) #30020 owner's call (the dev's earlier report says that card's body retires every row or gives each its own ruling). Escalated to the seat as a card-hygiene item, not a gate or registry defect.
  3. Check-runs pending at reading (34 in_progress, listed above), including the lane that first runs the gate with a non-empty registry. This record's PASS judges the tree; the landing waits on that lane and the rest going green, which the queue guard enforces, and on the maintainer's Tier H approval.
  4. The dev's account of the gate counts (277/27/30 and 251/67/30) is consistent with the member census read here (30 rows per file, each a body) and with the earlier records' readings of both blobs; not re-run.

④ Design judgment

hits file .github/workflows/lint.yml is unchanged by the merge and the registry commit; the step it wires now has something to guard. The wiring judgment of 6627146481 holds, with one consequence the registry adds: from this landing on, a push that puts a body back into either facade class, or that adds a row without a card, or that leaves a row on a member that has become a delegation, reds Lint · repo & tooling gates on the PR and in the merge queue. That is the ruling's gate, live.

Does every row name a real member of the landed file that is in fact a non-delegation body? Yes, all 60, read one by one above. Does each cite a card? Yes, #30020, in the # plus digits form the gate requires. Can the gate on main red after the merge? Not from this diff: the files, classes and bodies the rows name are the ones on main, byte for byte.

No must-fix. Findings:

Implemented-by: claude/project-thread-hv5n7g
Reviewed-by: session_01WGufzgBDcBNVmSmo7VFHpj

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#1 · os-support-ai · session_01WGufzgBDcBNVmSmo7VFHpj

Stamp: 2026-11-04T10:52Z
Head-sha: f24cb61

维护者速读

席位核对

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 4, 2026 10:53
@objectstack-fleet
objectstack-fleet Bot requested a review from hotlong as a code owner November 4, 2026 10:53
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 4, 2026 10:53
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants