Repository navigation
feat(scripts): gate a vi.mock override's VALUE shape against the declared export - #8954
Merged
Merged
Conversation
…ared export
The two existing `vi.mock` gates judge whether a relative specifier resolves and
whether the factory inherits the real module. Neither judges what the override
is worth: thirteen `RecordDetailView` stubs returned `{ viewers, others }` where
`useRecordPresence` is declared `PresenceUser[]`, and putting them back on disk
made BOTH gates print a byte-identical verdict line and exit 0.
`check-vi-mock-override-shape.mjs` compares the override's value shape against
the export's declared shape, resolving the export through the workspace source
entry and its re-export chain. Both sides are read for certainty and anything
less is `unknown`, which never conflicts in either direction.
It is a new gate rather than an extension because extension was measured to be
unavailable: reading declared types needs `typescript`, and injecting that
import into `check-vi-mock-inherit.mjs` takes `check-pre-install-import-graph`
from exit 0 to exit 1. It therefore runs in `lint.yml`, after the install.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
…gate list `ci-cd-pipeline-doc.test.ts` requires every first-party command the `lint` job runs to be named in `content/docs/guide/ci-cd-pipeline.md`, in run order: every gate in that job blocks a merge on a required check, so one nobody wrote down is a build failure a contributor meets with no way to learn what produced it. Also types `scan`'s options with JSDoc, matching the sibling gate, so the pin test can pass `files`, `floors` and `baseline` under `tsconfig.scripts.json`; and aligns the verdict lines with the two sibling gates' emoji prefixes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
os-steve
marked this pull request as ready for review
September 10, 2026 10:13
This was referenced Sep 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #8903
Adds
scripts/check-vi-mock-override-shape.mjs— a third gate over thevi.mockpopulation, judging the one property neither sibling judges: what the override is worth.The measurement this gate exists for
Re-derived on this branch, independently of the card. The thirteen drifted
useRecordPresencestubs were restored to disk byte-for-byte from PR #8902's pre-image (blob hasha3fe5953aconRecordDetailView.activityMapIdentity-5878.test.tsx— exactly the pre-image that PR's own diff header names), and both existing gates re-run:check-vi-mock-specifiersdiffempty)check-vi-mock-inheritdiffempty)The card's central claim holds.
check-vi-mock-specifiersjudges whether a relative specifier resolves, and these mock the bare@object-ui/collaboration, which its own summary counts underbare (out of scope).check-vi-mock-inheritjudges whether the factory obtains and spreads the real module — all thirteen already did, fully compliant with the drift inside them.One figure moved: the gate's own summary reads 937 bare, not the 935 the card quotes. Re-derived from
check-vi-mock-specifiers' own output ataeaa0f6.The acceptance instrument is mutation, not green
Final run at
dac8fe6, against the code this PR ships. Both legs prove the mutation reached disk (anchor count and blob hash) before any result is read; the restore is proved by state (git diff HEADempty, every blob back to itsHEADhash), never by an exit code.Each finding names which stub, which export, and what shape was expected:
The same two legs are pinned in the test file end to end, over a 1001-file clean population so the exit code cannot be confused with the vacuity floor — the test asserts
COLLAPSEDis absent from stderr before it reads the status. Without that filler both legs exit 1 and the exit code proves nothing; that confound is this gate's own defect, one level up.Which direction can this gate fail in?
It can fail when a judged override's shape conflicts with the declared export — proved above on thirteen real instances.
It cannot fail on an
unknown, by design. That is a real limit, stated rather than hidden: an override whose value is avi.fn(), an identifier or a call is counted and never judged. Certainty or nothing, because a gate that reddens correct code gets deleted rather than fixed. The honest middle is pinned too:() => stubValueis judged at the kind level (function vs function) and never below it, whileuseRecordPresence: []against a function export is still a finding.Route: a new gate, decided on measurement
The suggested route was to extend a sibling first. It is not available, and that was measured rather than argued. Reading a declared shape means parsing TypeScript, so this gate imports
typescript.check-vi-mock-inherit.mjsruns invi-mock-specifiers.yml, which deliberately carries no install step; every pre-install gate's import graph is enforced to be node builtins plus repo-relative modules only (objectui#6148). Injecting that one import into the sibling, blob hash recorded before and after:Extending the sibling would have reddened an existing gate, which Zone-1 boundary 2 forbids outright. So: a new gate, homed in
lint.ymlafterpnpm install.check-pre-install-import-graphreported exit 0. That reading was discarded, not used. The number above comes from the re-run whose anchor count and blob hash both confirm the injection landed.Two further properties argue the same way:
check-vi-mock-inheritjudges 100% of its population, while this predicate is partial by construction (923 of 1496), so fusing them would make one census line mean two things about two denominators; and its predicate is a property of the factory's own text, while this one resolves modules.lint.yml's skip switch is not a blind spot here — its ignore list is Markdown plus thecontent,docsand.changesettrees, and both halves of what this gate compares live in TypeScript files. The test pins that with a glob matcher and fails if a pattern matching a TypeScript file is ever added.The existing population was not reddened
Zone-1 boundary 3 held without needing the ledger. Measured at
aeaa0f6:KNOWN_SHAPE_MISMATCHESis therefore empty: nothing in the tree violates the predicate, so nothing had to be grandfathered, and the gate is hard-red for this class from day one. The 937 bare-specifier sites are untouched — this gate judges override values, not specifiers. The ledger mechanism ships anyway, SHRINK-ONLY: a registered entry that no longer mismatches is itself a failure, so it can never quietly become a blanket. Both behaviours are pinned.No stub was re-fixed; PR #8902's repair is left exactly as it stands.
Gates
scripts/pm/dispatch-gates.mjsdoes not exist in objectui, so the families below were derived by hand from the changed-file set. Every exit code was written to a file before it was read.check:vi-mock-override-shapecheck:vi-mock-specifierscheck:vi-mock-inheritcheck:pre-install-import-graphcheck:control-bytescheck:entry-guardcheck:unreferenced-sourcescheck:phantom-deps/check:unused-deps/check:self-importcheck:doc-fences/check-doc-linkscheck-changeset-presence/-no-major/-fixedtype-check:scriptslint:root— complete 309-file population, 0 errorspnpm exec vitest run scripts/— 134 files, 3914 testslint:rootis the eslint invocation that coversscripts/; it was run whole rather than narrowed, so there is no narrowing to justify.pnpm lint(turbo run lint) is per-package and structurally cannot reachscripts/.Acceptance notes
content/docs/guide/ci-cd-pipeline.mdis edited. It is not optional —ci-cd-pipeline-doc.test.tsfails anylint.ymlstep whose command the doc's gate list does not name, in run order, because every gate in that job blocks a merge on a required check.COVERED_SPECIFIERSfromcheck-vi-mock-inherit.mjsrather than restating it. A population that drifted between the two gates would be a hole neither reports; the test pins the import.deep(391) rose from 271 once local interface / type-alias references were resolved one hop, with a callable-interface escape so a correct function stub is never reddened. Zero mismatches appeared either way — the increase is power, not noise.check-vi-mock-specifiers'bare (out of scope)figure has moved 935 → 937 since the card was written. It is a census line, not a defect.维护者速读(草稿)
改了什么 — 新增第三道
vi.mock门禁,判断桩的返回值形状是否与真实导出声明的形状一致。另配套测试、package.json脚本别名、lint.yml一个步骤、CI 文档的门禁清单一条、一份空声明 changeset(不发版)。零运行时改动,零已发布面改动。为什么改 — 现有两道门对这一类漂移完全无判别力:把 13 个漂移桩原样放回磁盘,两道门打印逐字相同的判定行、同样 exit 0。这类失效是最坏的一种——契约一变,桩对了的文件红、桩错了的静默绿,套件从中间裂开,多数派悄悄停止追踪那个 hook。
风险与代价(含回滚) — 风险集中在误报。控制手段是"要么确定,要么不判":任一侧读不确定就记为
unknown,unknown两个方向都永不构成冲突;可调用的 interface 一律读作unknown(它运行时是函数)。全树 923 个已判站点零误报,这是规模级的阴性对照。回滚成本极低:删掉一个脚本、一个测试、一行package.json、一个 workflow 步骤和一条文档 bullet,不牵动任何产品代码。存量 937 处裸 specifier 桩未被触碰,台账为空。席位意见 — (留空,待维护者)
你要做的 — 确认两点:① 新门禁落在
lint.yml而非兄弟门禁所在的vi-mock-specifiers.yml,是因为它需要typescript,而后者按设计不装依赖(已实测:注入该 import 会让check-pre-install-import-graph从 exit 0 变 exit 1);② 台账KNOWN_SHAPE_MISMATCHES保持为空是刻意的——本轮无需赦免任何存量,门禁对这一类从第一天起就是硬红。🤖 Generated with Claude Code
https://claude.ai/code/session_01MPaVWWMuWeT5LgB1qoXjVB
Generated by Claude Code