Repository navigation
finding: 没有门禁核对包内 README 的自包导入名 vs 真实导出面 —— #5002 家族 7 个包的漂移全靠人工巡查发现(附已验证的检查器原型) #5043
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentation
on Aug 17, 2026 在 #5012(plugin-gantt,本族第三席)的实施里把这两档都用了一遍。不另立卡 —— 按 #4949 的去重纪律,命中在途卡就写在卡上。带回三条对原型规格有直接影响的读数。
1. 「多行块仍按单行匹配」这个下限可以彻底消掉:别用正则抓 import,用 TS parser 抓
本单第一版照楼上的规格写了「一条跨行正则 + 先剥注释再按逗号切」。它在 plugin-gantt 上直接报了 5 个假名:
weeks/title/selection/target/dataSource,而真正的两个虚构名ganttComponents、GanttSchema一个都没报出来。根因是这一族 README 的固定形状:第一处是副作用导入(
import '@object-ui/plugin-gantt';,无from),于是懒惰量词从它开始一路吃到下一个from '@object-ui/plugin-gantt'(隔了 20 行散文和一个 schema 字面量),把中间整段 markdown 当成了 import clause。剥注释、按逗号切都发生在这段被污染的文本上,于是散文里的词成了「虚构导入名」。换成先抽 fenced 代码块、每块喂
ts.createSourceFile、走ImportDeclaration的 AST,楼上记的两个坑就不再是需要各自防守的规则,而是结构上不可能发生:- 多行块本来就是一个
ImportDeclaration节点,任何 fence 或散文都不可能被吃进 import clause; - 行尾
//注释对 parser 是 trivia,永远不可能贡献一个名字(假阳性消失); A as B的 AST 直接给出propertyName = A(导出名)与name = B(本地别名),判 A;楼上第三版记的 alias 误判也随之消失。
实测(plugin-gantt,修复前):导出面 35 个名字取自构建产物
dist/index.d.ts的checker.getExportsOfModule;README 侧 4 个绑定,MISSING COUNT: 2——ganttComponents@:108、GanttSchema@:407,零假阳性。修复后 39 个绑定全 OK,读数 0。顺带印证楼上第 1 条:
GanttSchema用裸 grep 在packages/types/src上是「有命中」的 —— 但那 6 处全是ObjectGanttSchema的子串;加词边界\bGanttSchema\b才是零命中。所以要机制化的话,取名集合这一步必须走 symbol,任何 grep 近似都会在这个方向上骗人(卡面 #5012 的复核命令就写成了裸 grep)。2. 自证做成「往多行块塞假名」之外还要塞一条「注释里的假名」
反向验证如果只塞假名,只验证了召回,验不到楼上第二版栽的那个假阳性。本单的自证一次planted 四处,预判先写、四条都符:
变异 预判 实测 行尾注释改成含非导出词( // ganttComponents was never real)不得报 未报 多行值导入里加 ganttThings as Things报,且按导出名 ganttThings报 ganttThings多行类型导入里 GanttTask改成GanttTask as Task仍 OK OK 多行类型导入中段插 GanttSchema报 报 MISSING COUNT0 → 2(恰为{ganttThings, GanttSchema}),还原后回 0。变异全做在 README 的scratch 副本上(检查器加了--readme <path>),工作树一次都没被改过,不存在「临时改动误入 commit」的窗口。3. 「更强的一档」的一个具体加强:文档里的 interface 块要双向钉,否则它绿得没有意义
楼上把「抽块喂 tsc」列为可选上界。本单做了,并且在 plugin-gantt 上正是它抓到第三处漂移:
GanttTask是真导出(名集合核对全绿),但 README 的 "Task Structure" 参考块把它写成name+ ISO 字符串日期,真身是title+Date。一个坑值得写进规格:文档里那种独立的
interface GanttTask { … }块,照原样喂 tsc 是永远绿的 —— 它只是一个与真类型无关的本地声明,内容再假也编译通过。要让它有检查力,得在块后追加双向赋值把它钉在真身上:declare const real: RealGanttTask; export const docFromReal: GanttTask = real; // real -> documented declare const doc: GanttTask; export const realFromDoc: RealGanttTask = doc; // documented -> real
反向验证(旧 README 回填,预判先写)按预判转红,两向各报一条:
TS2741 Property 'name' is missing in type '…GanttTask' but required in type 'GanttTask'与TS2741 Property 'title' is missing in type 'GanttTask' but required in type '…GanttTask'。单向只钉一半:只写 real → doc 时,文档块多写的可选键不会被发现;只写 doc → real 时,文档块漏写的必填键不会被发现。预判不符的一处,如实记:同时预判旧的
const task: GanttTask = { name: …, start: '2024-01-01' }会报name/title,实测只报了两条TS2322 Type 'string' is not assignable to type 'Date'—— present 属性的类型不符会抢在 missing-property 的详述之前短路。也就是说name/title这个事实只由上面的 pin 块承载,typed-example 那一侧靠不住;规格里若只抽 typed example 不做 pin,会漏掉键名重命名这一整类。另外两条工程细节,给写脚本的人省一次弯路:
- 一份 README 里多个块常各自
const schema = { … }。它们都没有 import,于是都是全局 script,一次 tsc 会报重复标识符。每块补一行export {};变模块即可,只改作用域,不影响块内检查。 - 这一族 README 有一部分块是故意不合法的形状速写(裸
{ type, tasks: …, … }属性列表、裸dependencies: [ … ]片段)。它们必须显式跳过并在输出里列出理由,不能静默丢 —— 否则「跳过了几块」这件事没人看得见。按内容签名(块首行正则)分类比按块序号稳,同一套机器才能既跑当前 README 又跑改前 README(两者块序号不同)。
关于入场价
strict下把 plugin-gantt 全部 15 个可编译块跑一遍,修完 #5012 之后仍余rc=2:两条TS7006,来自onTaskClick: (task) => …写在无类型标注的const schema = { … }里。它们不是 #5012 的漏修,而是那一族虚构 schema 键的一部分(已立 #5057)。给这道门定入场价时值得预期到:名集合核对可以要求全仓零,而抽块编译在现状下会带一批与虚构键面纠缠的既有红,得先分诊再决定基线,不宜一上来就要求全绿。原型仍是 scratch、未入库,与楼上一致。若做成
scripts/check-readme-exports.mjs,建议第一档(名集合核对,AST 版)先上并要求零,第二档(抽块编译 + 双向 pin)带 baseline 跟进。
Generated by Claude Code
- 多行块本来就是一个
给「更强的一档」补一条实测边界:抽块喂 tsc 抓不到「假 schema 键」这一类(发现于 #5058 / PR #5072 的实施,顺带复测 #5057;不另立卡,附在本卡里)
本卡「更强的一档」写的是「把 README 的 TypeScript 代码块抽出来喂 tsc」,并说它「能抓住『名字全真、类型不对』的漂移」。这一点成立,但有一个结构性盲区值得写进本卡,否则实现出来会给人虚假的安全感:授权 schema 的键面它一个都抓不到。
根因是契约那一端两层都对未知键放行:
BaseSchema带索引签名[key: string]: any(packages/types/src/base.ts:318),所有ObjectXxxSchema继承它;- 其 Zod 对应物是
.passthrough()(packages/types/src/zod/base.zod.ts:154,注释里明写)。
实测(gantt,基线
82a94170c):把 README 的 Basic 块type先改对、只留卡面判为虚构的键面(tasks/viewMode/onTaskClick/fields/nameField/startField/endField),标注ObjectGanttSchema编译 → exit=0,零报错。也就是说TS2353(多余属性)在这一族类型上不可能出现,不管标注得多严。抽块喂 tsc 在这一族上真能钉住的只有三处:
type字面量 →TS2322: Type '"gantt"' is not assignable to type '"object-gantt"';- 必填键缺失 →
TS2741: Property 'objectName' is missing; - 运行时类型的字面量(不继承
BaseSchema、没有索引签名的那些)—— 例如GanttTask:8 处TS2322: Type 'string' is not assignable to type 'Date',以及TS2353: … 'name' does not exist in type 'GanttTask'。第 3 类正是本卡「更强的一档」在 plugin-calendar README 教的calendarComponents手动注册 API 不存在 —— 照抄即抛 #5010 上命中CalendarEvent的那类,能力真实存在,只是局限在运行时类型上。
再补一条给实现者的读数陷阱:同一个字面量里若同时有多余属性和属性级赋值错误(如
name:+ 字符串日期),TS 只报后者、跳过 freshness 检查,多余属性看不见红。所以这档检查器若按「一次编译看全部」来判,会系统性漏报多余键;要看见,必须像我那样把类型错误先修对再单独测。推论,供本卡定范围时取舍:名集合核对(第一档)管的是导入名,抽块编译(第二档)管的是运行时类型,而授权 JSON 的键面两档都不管 —— 它需要第三种东西(要么收紧契约让未知键可拒,要么按读取点做 grep 类核对)。#5057 就是这个盲区的用户可见后果(整片 schema 键面渲染器不读,照抄即空图),#5074 是同一盲区在渲染器侧的镜像(未声明键被
as any读进来,半接线)。
Generated by Claude Code
os-support-ai commented
on Aug 19, 2026 CollaboratorMore actionsTriage (first-touch, triage seat, session
session_01HXzdkKx5WjwwCTT3c7U5kB): promotedfinding→pm:queue, labels +tooling; type corrected Bug → Task (this card is the gate/mechanization work; the user-visible defects of the family were filed separately as #5010–#5016).Scope adopted = tier 1 only:
scripts/check-readme-exports.mjs+check:readme-exports, built per the two follow-up comments on this card (which supersede the body's regex sketch): AST-based — fenced blocks →ts.createSourceFile→ImportDeclarationwalk (judge the export namepropertyName, not the alias), export sets from the package's builtdist/index.d.tsviachecker.getExportsOfModule(symbol-level, no grep), three-state verdict (real / fabricated / wrong-path), require zero repo-wide. Include the planted-mutation self-test described in the comments (fake name in a multi-line block AND a fake name in a trailing comment).Tier 2 (extract-blocks-and-compile + bidirectional pins) is explicitly OUT of this card — it needs a baseline decision first (known pre-existing reds, see the entry-price note); file a follow-up if wanted. The comments also record that neither tier sees authorable-JSON key surfaces (BaseSchema passthrough) — that bound is documented, not solved here.
Size/model suggestion: M,
mode:subagent.
Generated by Claude Code
- addeddomain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repoobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
on Aug 21, 2026 - added a commit that references this issue
on Aug 23, 2026 yinlianghui-tw commented
on Aug 25, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 5043, "status": "done", "branch": "claude/issue-5043-readme-exports-gate", "pr": "https://github.com/objectstack-ai/objectui/pull/6212", "premise_still_valid": true, "summary": "Tier 1 only, as triage scoped it: scripts/check-readme-exports.mjs + `pnpm check:readme-exports`, over every README under packages/, requiring zero repo-wide. Fenced blocks -> ts.createSourceFile -> ImportDeclaration walk, judging the EXPORT name (propertyName) not the local alias; export sets from each package's DECLARED type entry via checker.getExportsOfModule, symbol-level, aliases resolved before the value/type flags are read; three-state verdict real / fabricated / wrong-path. A package whose declared type entry is not on disk is a FAILURE (unbuilt / no-type-entry), never a silent skip and never 'exports nothing' - scoped to the case where the missing exports would have changed a verdict; today 37 read, 0 unbuilt, 3 declare no types and none of those three has a self-import. Six FLOORS make a collapsed walk fail, and the two walks (READMEs, packages) breach independently so the verdict names which one collapsed. New blocking workflow readme-exports.yml -> 'README Export Check', unfiltered pull_request + push + merge_group + workflow_dispatch, classified in REQUIRED_CONTEXTS (which is what puts it inside the #6160-derived merge_group floor); it is NOT a step in ci.yml because every ci.yml job short-circuits on a diff excluding **/*.md, so a README-only PR - one of the two shapes that introduce this drift - would skip it. Two design points the fixtures forced rather than the prompt: the wrong-path owner universe is the whole workspace (deriving it from the README walk would report a name owned by a README-less package as 'fabricated', telling the reader to delete a real export), and nested READMEs are kept and resolved to their publishing package via packageDirOf rather than dropped by tightening the pathspec - the one real drift found came from a nested one. Also filed: #6213 (the rest of the core/adapters README is stale beyond its imports) and #6214 (tier 2, blocked on a baseline ruling).", "tests": "All at 2db373fab, the final commit; exit codes captured by redirect BEFORE any pipe; each verdict quoted from the line the gate itself printed. GATES: check:readme-exports EXIT=0 '<check> check-readme-exports: OK (43 README(s) under packages/ (0 outside any package), 396 fenced block(s) (301 parsed as code, 6 untagged); 495 import binding(s), 374 of them self-imports judged (374 real, 0 wrong-path, 0 fabricated); 3239 export symbol(s) read from 37 of 40 package(s) (39 carry a README) (0 unbuilt, 3 declare no types); 17 side-effect import(s), 0 namespace, 11 deep self-path, 93 to other packages)'; check:control-bytes EXIT=0 'scanned 5125 tracked text file(s)'; check:pre-install-import-graph EXIT=0 '15 pre-install step(s) in 14 job(s) run 15 scripts/ gate(s)'; check:vi-mock-specifiers EXIT=0; check:doc-fences EXIT=0 'every TypeScript block in 223 document(s)'; check:doc-types EXIT=0 'Every documented component type is registered.'; check:skills-paths EXIT=0 '93/94 stated path(s) resolve'; check-doc-links.mjs EXIT=0; check-doc-snippet-types.mjs EXIT=0 'Every covered documentation snippet compiles against the built types.'; check-changeset-presence.mjs EXIT=0 '1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'; check-changeset-fixed.mjs EXIT=0; check-lint-coverage.mjs EXIT=0 '46/46 packages linted'; check-type-check-coverage.mjs EXIT=0 '45/46 via type-check'; pnpm type-check:scripts EXIT=0; pnpm lint:root EXIT=0 '28 problems (0 errors, 28 warnings)'. TESTS: vitest --project unit scripts/__tests__ EXIT=0 'Test Files 74 passed (74) | Tests 2040 passed (2040)', including the new 44-test scripts/__tests__/check-readme-exports.test.ts. The first run of that directory went RED on ci-cd-pipeline-doc.test.ts ('gives every workflow in .github/workflows/ its own section') - a tooling diff is not automatically a tooling-only diff; fixed by adding the section and inventory row to content/docs/guide/ci-cd-pipeline.md, then re-run green. LINT SCOPE, stated not assumed: every code file in this diff is under scripts/, entirely inside `pnpm lint:root`'s population, so this is the full run for this diff and not a narrowing; population read from eslint's own config via --format json = 192 files linted, 0 errors, 28 warnings, all 28 pre-existing (the one warning this PR introduced was removed); the config enables no type-aware linting (no projectService, no parserOptions.project), so this diff cannot move any untouched file's verdict; no package source changed - packages/core/src/adapters/README.md is markdown, which eslint does not lint. TEST SCOPE, derived from disk: grep -rln for readers of every changed path across *.test.ts/*.test.tsx returns only scripts/__tests__ (the single hit outside it, packages/auth/src/__tests__/reserved-auth-features.test.ts, mentions ci-cd-pipeline-doc.test.ts in a prose comment and reads none of these files); the whole directory was run rather than the new file alone. Gate list derived from package.json's check:* scripts and .github/workflows/, not from the dispatch note. PLANTED-MUTATION SELF-TEST: predictions written to disk BEFORE any run; mutations made on a SCRATCH COPY via the --readme <readmePath>=<path> flag, so `git status --porcelain` showed only the two intended tree entries throughout; injection proved on disk by ANCHORED counts, not by an editor exit code (ganttThings 0->1, 'ganttWidgets as Widgets' 0->1, ganttComponents 0->1, CalendarViewSchema 0->1, GanttSchema 1->2). Six directions, all matching prediction: (1) fake name in a MULTI-LINE value block -> reported, fabricated ganttThings @ :133; (2) fake name in a TRAILING // COMMENT -> NOT reported, real stayed 374 (the false-positive direction a recall-only test cannot see); (3) `ganttWidgets as Widgets` -> reported as ganttWidgets, the EXPORT name, @ :134; (4) fabricated name MID-BLOCK in a multi-line TYPE import -> reported, fabricated GanttSchema @ :152; (5) a real name owned by another package -> wrong-path naming @object-ui/types @ :135, NOT fabricated; (6) side-effect import + 20 lines of prose (the shape that made the regex prototype report 5 false names) -> zero judged names, no prose word reported. ONE PREDICTION DID NOT MATCH, and it found a real defect: the first run reported every binding of a multi-line block at the `import {` line, because the line came from the ImportDeclaration node - on a 14-name block that hands the reader the wrong line. Fixed to take the line from the narrowest node carrying the binding (the ImportSpecifier), which is what produced :133/:134/:135/:152 above; pinned by 'names the README LINE of the specifier, not of the import { above it'. All of it is now re-run every CI run in check-readme-exports.test.ts against a fixture tree with hand-written .d.ts files - the test shards run `pnpm install` then `pnpm test` and NEVER build, so a suite scanning this repo for its verdicts would assert nothing in CI while passing locally; the repo-state assertions hold in both states and BOTH branches assert (on an unbuilt tree the gate must FAIL, never report OK). BUILD: `turbo run build --filter='./packages/*'` cold cache, concurrency 2, on a contended container = 2m42s for all 39 packages - the measurement behind wiring this blocking per-PR rather than nightly. All heavy runs went through the shared verify lock. NOT RUN, and why: no test on disk reads packages/core/src/adapters/README.md (derived above), and no package source changed, so no per-package `pnpm test` was run; CI runs the farm regardless and PM reads the real job conclusions.", "open_questions": [ { "question": "Should README Export Check be a BLOCKING per-PR context (as built), or a nightly + push-to-main gate like this repo's three other build-requiring gates? It is the one judgement call in this PR and it is a pipeline-budget decision, not a technical one.", "options": [ "A - blocking per-PR, unfiltered (as built): catches the drift before it ships; costs pnpm install + a 2m42s cold build on every PR", "B - nightly + push-to-main, matching published-dist-gate.yml / node-esm-load-gate.yml / spec-range-floors.yml: near-zero PR cost, but drift merges green and is caught the next night", "C - blocking with a `paths` filter on packages/**: barely cheaper than A, since most PRs touch packages/**, and it moves the check into OPTIONAL_CONTEXTS" ], "recommendation": "A, as built. Real business need: the READMEs are in each package's `files` and ship in the npm tarball, so the defect reaches paying readers, and one manual sweep already hit seven packages and called that a lower bound. Long-term soundness: the three nightly precedents all scan the PUBLISHED artifact, which only exists after a release - catching a bad README the night after it merges still lets it into the next publish, so B is a weaker instrument for this particular failure, not merely a cheaper one. Making AI-written code hard to get wrong: a copied fabricated import is precisely the shape an AI reader reproduces, and blocking is what stops it at authoring time rather than after. Startup scope discipline: the cost is bounded and measured (2m42s, same order as ci.yml's own Type Check, an order below Build & E2E) rather than estimated. Demoting to B is a two-line change to the trigger block plus moving the name out of REQUIRED_CONTEXTS, so this is cheap to reverse if the budget call goes the other way - flagged explicitly in the PR body for exactly that reason." }, { "question": "CLAUDE.md requires assign + a claim comment with session ID and branch before writing code; the dispatch says the issue is already claimed by PM and not to touch the assignee. #5043 was in fact UNASSIGNED with no claim comment when this started. Recorded, not acted on: the assignee was left alone per dispatch, and no other in-flight work on it was found (no branch, no PR, no claim comment).", "options": [ "A - leave as is; the dispatch contract governs and PM owns the claim", "B - the dev seat should post a claim comment (session ID + branch) even when told not to touch the assignee" ], "recommendation": "A for this run - the conflict is between two standing documents and picking sides quietly is worse than reporting it. But B is worth a ruling: the claim comment is the only thing that distinguishes MY claim from anyone else's under a shared GitHub identity, and it costs nothing that touching the assignee would." } ], "out_of_scope_findings": [ "filed as #6213: packages/core/src/adapters/README.md documents an ObjectStack adapter that no longer lives in that directory at all - the two import paths were repaired in this PR (unambiguous: both names are real and both live in @object-ui/data-objectstack), but the headings, the filter-operator and query-parameter tables, and the one-entry 'Available Adapters' list still describe a package that moved, while the four adapters actually in the directory are named nowhere. Placement question with three defensible answers, so not repaired here.", "filed as #6214: tier 2 of this card (extract-blocks-and-compile + bidirectional pins for documented interface blocks), carrying the already-measured design points so nobody re-derives them - a documented interface block compiles green as-is and needs both pin directions; the typed-example half cannot carry the key-rename class because a property-level type error short-circuits the missing-property detail. Blocked on a baseline ruling, not on effort: after #5012 landed, plugin-gantt still exits rc=2 with two TS7006 entangled with #5057's fabricated-key surface." ] }Draft PR: #6212 · Reported at the draft-PR moment; CI convergence is PM's read, not waited on here.
Generated by Claude Code
yinlianghui-tw commented
on Aug 25, 2026 CollaboratorMore actionsErratum to the report above — not a second report, deliberately unmarked so a marker scan still finds exactly one.
Read-back of the report comment shows GitHub's body sanitizer ate two short angle-bracket fragments after the comment landed. The JSON still parses and every measured number is intact; one prose sentence in the
testsfield lost its meaning. Correcting it here rather than editing the comment, because this session has no edit-comment path.As posted: "mutations made on a SCRATCH COPY via the
--readme =flag"As written, and as the flag is actually spelled (angle brackets removed so it survives):
node scripts/check-readme-exports.mjs --readme packages/plugin-gantt/README.md=/tmp/scratch.mdi.e.
--readme readmePath=standInPath, repeatable. It substitutes a stand-in file for one README while the rest of the scan runs normally, which is what let the four planted mutations run with the working tree untouched —git status --porcelainshowed only the two intended entries throughout.Also eaten: a leading check-mark glyph in the quoted
check:readme-exportsverdict line, which now reads as two leading spaces. The line itself is unchanged and the exit code (EXIT=0) is the load-bearing part.Noting the mechanism for the next person on this card: the sanitizer removes short
<…>spans after the body is stored, and it does so inside fenced code blocks and backticks too — so a placeholder written in angle brackets cannot be protected by quoting it, only by not using angle brackets.
Generated by Claude Code
yinlianghui-tw commented
on Aug 25, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 5043, "status": "done", "branch": "claude/issue-5043-readme-exports-gate", "pr": "https://github.com/objectstack-ai/objectui/pull/6212", "premise_still_valid": true, "summary": "FIX ROUND (supersedes the earlier report on this card; the gate script, workflow and REQUIRED_CONTEXTS wiring are untouched as instructed). One test fixed, plus a clean merge of origin/main. The red `Test (shard 2/4)` was my own test: `breaches the README-side floors alone when only that walk collapses` called scan(repoRoot, {readmes: []}), so its verdict depended on whether the checkout was built. Its claim needs the PACKAGE-side walk healthy while the README walk collapses; against repoRoot that precondition is a property of the machine, and the shards run pnpm install then pnpm test and never build, so on CI every packages/* type entry is absent, packagesRead and exportSymbols breach too, and the exact equality failed. Moved onto the same fixture tree the rest of the suite uses, with fixture-scale floors, split into three legs: a control leg (both walks healthy, vacuous empty - without it a green could mean the fixture is broken too), the collapse leg (exact equality kept, every breached value asserted 0), and a package-side leg (packagesRead and exportSymbols unchanged from the healthy leg, pinned at 2 and 4). The equality stays EXACT: a containment check would pass on a built tree and an unbuilt one alike, asserting that four counters breached without asserting the other two did not, which is the entire independence claim. Swept all nine repoRoot reads in the file - one was the defect, one (the summarise shape assertion) passed in both states but still walked the real packages for a pure-formatting assertion and was narrowed to override both walks, and the rest read only tracked source or are the deliberate both-states repo-state block where both branches assert. No second bug. Also merged origin/main as a merge commit, never a rebase or force-push.", "tests": "TWO-STATE READINGS, each on the tree it names, same command (npx vitest run --project unit --maxWorkers=2 scripts/__tests__/check-readme-exports.test.ts), root vitest only per objectui#3378, exit codes captured by redirect BEFORE any pipe. (0) RED REPRODUCED FIRST, on an UNBUILT checkout (ls packages/*/dist/index.d.ts = 0): EXIT=1, 'Test Files 1 failed (1) | Tests 1 failed | 43 passed (44)', failing test 'breaches the README-side floors alone when only that walk collapses' - the exact CI assertion. (1) UNBUILT, after the fix (dist count 0): EXIT=0, 'Test Files 1 passed (1) | Tests 46 passed (46)'. (2) BUILT, after the fix (dist count 37): EXIT=0, 'Test Files 1 passed (1) | Tests 46 passed (46)'. WHOLE DIRECTORY from the repo root, on the merged built tree: EXIT=0, 'Test Files 76 passed (76) | Tests 2094 passed (2094)' - up from 74/2040 because the origin/main merge brought two new suites in; ci-cd-pipeline-doc.test.ts is inside that green, so the merge did not drop my workflow section. GATE UNION re-run at 68c3a07bf, the merged head, because the merge changed this gate's own inputs (packages/components/README.md, packages/data-objectstack/src/index.ts +133, and the vite.config.ts dts wiring across 20 packages), so the tree was REBUILT first (turbo run build --filter='./packages/*', 2m57s, 39/39 successful): check:readme-exports EXIT=0 'check-readme-exports: OK (43 README(s) under packages/ (0 outside any package), 396 fenced block(s) (301 parsed as code, 6 untagged); 495 import binding(s), 374 of them self-imports judged (374 real, 0 wrong-path, 0 fabricated); 3242 export symbol(s) read from 37 of 40 package(s) (39 carry a README) (0 unbuilt, 3 declare no types); 17 side-effect import(s), 0 namespace, 11 deep self-path, 93 to other packages)' - exportSymbols moved 3239 to 3242 from main's data-objectstack additions, drift stayed zero; check:control-bytes EXIT=0; check:pre-install-import-graph EXIT=0; check:vi-mock-specifiers EXIT=0; check:doc-fences EXIT=0; check:doc-types EXIT=0; check:skills-paths EXIT=0; check-doc-links.mjs EXIT=0; check-doc-snippet-types.mjs EXIT=0; check-changeset-presence.mjs EXIT=0; check-changeset-fixed.mjs EXIT=0; check-lint-coverage.mjs EXIT=0; check-type-check-coverage.mjs EXIT=0; pnpm type-check:scripts EXIT=0; pnpm lint:root EXIT=0 '28 problems (0 errors, 28 warnings)', all pre-existing. REPOROOT SWEEP, per assertion: packageDirOf x2 reads package.json files (tracked source, never build output) - stable, unchanged; scan(repoRoot, {readmes: [], packageDirs: []}) overrides both walks so nothing is read off disk and every counter is 0 by construction - stable, unchanged; summarise(scan(repoRoot, {readmes: []})) asserted only the line's SHAPE so it passed in both states, but still walked the real packages for a formatting assertion - narrowed this round to override both walks; scan(repoRoot) in the 'repo state' describe is the deliberate both-states design (built = packagesUnbuilt === 0, and BOTH branches assert - on an unbuilt tree the gate must FAIL, never report OK) - kept as is; scan(repoRoot, {readmes: []}) in the independence test was THE DEFECT, moved to the fixture tree; path.join(repoRoot, '.github/workflows') and path.join(repoRoot, 'package.json') read tracked files - stable, unchanged. Files changed this round: scripts/__tests__/check-readme-exports.test.ts only, plus the origin/main merge commit. Pushed to the same branch; no new PR.", "open_questions": [ { "question": "The fix round told me PR #6216 had landed and to reconcile its three colliding anchors. It has NOT landed: #6216 reads state open, draft true, merged false, mergeable_state behind, and none of its files are on main at 0aed1aa9a - no shell-escape-residue.yml, no 'Shell Escape Residue Scan' in REQUIRED_CONTEXTS, no check:shell-escape-residue script. The origin/main merge was therefore CLEAN, zero conflicts, and I did not hand-apply #6216's insertions.", "options": [ "A - as done: merge main clean, do not touch #6216's content; whichever PR lands second reconciles the three anchors, which on current state is #6216", "B - pre-apply #6216's three insertions to this branch so the union exists before either merges" ], "recommendation": "A, and I acted on A. B would duplicate another branch's work on a branch its author does not control, and would CREATE the conflict rather than resolve it - two branches each adding 'Shell Escape Residue Scan' to REQUIRED_CONTEXTS is a textual conflict plus a duplicate array entry that dependabot-merge-gate.test.ts's exact partition would red on. The three anchors are ordinary adjacent-insertion conflicts and the second merger resolves them in minutes. Flagging the premise mismatch rather than complying quietly, per the standing instruction to argue instead." }, { "question": "MUST_SUBSCRIBE_MERGE_GROUP entry in merge-queue-reporting.test.ts: the correction says it is a hand-kept subset extra, not a requirement, and to leave it this round.", "options": [ "A - leave it (as instructed and as done)", "B - remove it for consistency with #6216, which omitted its own" ], "recommendation": "A, done, and I agree with the correction on the merits. Since #6160 the floor DERIVES from REQUIRED_CONTEXTS, so README Export Check entered it the moment it was classified there; the map entry passes both honesty checks and is a valid subset member, but it is not what satisfies the contract. Removing it would widen a fix round that should stay at one test plus a merge. Recorded in the PR body so the next gate author copies #6216's omission rather than my entry." }, { "question": "Unchanged from the first report and still open: should README Export Check be a BLOCKING per-PR context (as built) or a nightly + push-to-main gate like this repo's three other build-requiring gates?", "options": [ "A - blocking per-PR, unfiltered (as built)", "B - nightly + push-to-main, matching published-dist-gate.yml / node-esm-load-gate.yml / spec-range-floors.yml", "C - blocking with a paths filter on packages/**" ], "recommendation": "A, as built, and CI has now measured the real cost rather than my estimate: the job's first run came in at 2m30s end to end, against my local 2m42s cold-build measurement - same order as ci.yml's own Type Check, an order below Build & E2E. The four axes are unchanged: the READMEs ship in the npm tarball so the defect reaches readers; the three nightly precedents all scan the PUBLISHED artifact, which only exists after a release, so B would still let a bad README into the next publish; a copied fabricated import is exactly the shape an AI reader reproduces, and blocking stops it at authoring time; and the cost is bounded and now measured twice. Demoting is a two-line trigger change plus moving the name out of REQUIRED_CONTEXTS." } ], "out_of_scope_findings": [ "filed as #6213 (unchanged): packages/core/src/adapters/README.md documents an ObjectStack adapter that no longer lives in that directory at all - the two import paths were repaired in this PR, but the headings, the filter-operator and query-parameter tables, and the one-entry 'Available Adapters' list still describe a package that moved.", "filed as #6214 (unchanged): tier 2 of this card (extract-blocks-and-compile + bidirectional pins), carrying the already-measured design points. Blocked on a baseline ruling, not on effort.", "not filed, reported here instead because it is about another agent's in-flight branch and not a defect: PR #6216 and this PR insert at three identical anchors (scripts/dependabot-merge-gate.mjs docstring line and REQUIRED_CONTEXTS entry, package.json check:* script, content/docs/guide/ci-cd-pipeline.md inventory row and section). Both are correct; whichever lands second resolves to the union. Filing an issue for a two-branch merge ordering would be noise." ] }Branch head
68c3a07bf(merge commit2884e06bf+ the test fix). Draft PR #6212 body updated with the fix-round section, the two-state readings and the fullrepoRootsweep table. CI convergence is PM's read, not waited on here.
Generated by Claude Code
yinlianghui-tw commented
on Aug 25, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 5043, "status": "done", "branch": "claude/issue-5043-readme-exports-gate", "pr": "https://github.com/objectstack-ai/objectui/pull/6212", "premise_still_valid": true, "summary": "CONFLICT ROUND (supersedes the two earlier reports on this card). The coordinator's premise checked out on disk this time, exactly as stated: origin/main is f66072d1b, #6216 landed as 0ce6c8a0c, and PR #6212 was open / draft false / mergeable_state dirty with base still 0aed1aa9a. Merged origin/main as a MERGE COMMIT (dc79e7029) - never a rebase, never a force-push. Three anchors conflicted, precisely the three predicted, each an adjacent-insertion resolved to the UNION: scripts/dependabot-merge-gate.mjs keeps both the docstring inventory line and the REQUIRED_CONTEXTS entry for Shell Escape Residue Scan AND README Export Check; package.json keeps both check:shell-escape-residue and check:readme-exports; content/docs/guide/ci-cd-pipeline.md keeps both inventory rows and both sections. #6216's text is byte-identical to what landed, verified mechanically rather than by eye - every line on origin/main matching shell-escape-residue or Shell Escape Residue was checked for a verbatim grep -qxF match in the resolved file and all are present. The table is not reordered: pre-install-import-graph and vi-mock-specifiers still precede the two new rows and performance-budget still follows them, with #6216's row placed first as the incumbent and mine appended; the two sections are ordered to match and both still precede ## Link Checking. No file outside those three was touched. PR #6227 was NOT adapted to and packages/core/src/adapters/README.md was not touched beyond the two-line fix already in this PR; verified on disk that #6227 has not landed (origin/main's copy still carries the ObjectStack half, 6 ObjectStackAdapter mentions) and the merge produced no conflict there. Auto-merge NOT re-armed, per instruction.", "tests": "All at dc79e7029, the merged head, exit codes captured by redirect BEFORE any pipe, each verdict quoted from the line the gate itself printed. The merge touched this gate's inputs again so the tree was REBUILT first (turbo run build --filter='./packages/*', 1m08s, 39/39 successful, 37 dist type entries present). GATE UNION: check:readme-exports EXIT=0 'check-readme-exports: OK (43 README(s) under packages/ (0 outside any package), 396 fenced block(s) (301 parsed as code, 6 untagged); 495 import binding(s), 374 of them self-imports judged (374 real, 0 wrong-path, 0 fabricated); 3242 export symbol(s) read from 37 of 40 package(s) (39 carry a README) (0 unbuilt, 3 declare no types); 17 side-effect import(s), 0 namespace, 11 deep self-path, 93 to other packages)' - census unchanged from the previous round, drift still zero; check:shell-escape-residue EXIT=0 'check-shell-escape-residue: OK (4/4 root(s) resolved -- AGENTS.md: 1 file(s), 12 fence(s); CLAUDE.md: 1 file(s), 2 fence(s); skills: 18 file(s), 235 fence(s); content/docs: 184 file(s), 1056 fence(s); 204 file(s) and 1305 fenced block(s) examined in total; 0 occurrence(s) outside a fence (counted, not judged))' - #6216's own gate green on my merged tree, and its content/docs fence count is 1056, IDENTICAL to what #6216 measured, so my new doc section added no fenced block; check:control-bytes EXIT=0; check:pre-install-import-graph EXIT=0; check:vi-mock-specifiers EXIT=0; check:doc-fences EXIT=0; check:doc-types EXIT=0; check:skills-paths EXIT=0; check-doc-links.mjs EXIT=0; check-doc-snippet-types.mjs EXIT=0; check-changeset-presence.mjs EXIT=0; check-changeset-fixed.mjs EXIT=0; check-lint-coverage.mjs EXIT=0; check-type-check-coverage.mjs EXIT=0; pnpm type-check:scripts EXIT=0; pnpm lint:root EXIT=0 '28 problems (0 errors, 28 warnings)', all pre-existing. WHOLE DIRECTORY from the repo root, root vitest only per objectui#3378: EXIT=0, 'Test Files 77 passed (77) | Tests 2170 passed (2170)' - up from 76/2094 because this merge brought another suite in. THE FOUR SUITES THE CONFLICT ROUND NAMED, run explicitly with --reporter=verbose for per-file counts since the default reporter does not list passing files: EXIT=0, 'Test Files 5 passed (5) | Tests 164 passed (164)', broken down as check-readme-exports.test.ts 46 passed, check-shell-escape-residue.test.ts 33 passed (#6216's, unmodified), ci-cd-pipeline-doc.test.ts 32 passed (the one that reds if either section is dropped), dependabot-merge-gate.test.ts 35 passed (the exact three-bucket partition, now over a REQUIRED_CONTEXTS carrying two more names), merge-queue-reporting.test.ts 18 passed (the derived merge_group floor reading that same list). Files changed this round: the three conflicted files only, all resolved to the union, plus the merge commit. Pushed to the same branch (68c3a07bf..dc79e7029, fast-forward, no force); PR base now reads f66072d1b and it stays non-draft.", "open_questions": [ { "question": "Ordering within the two unions: #6216's entry placed FIRST (incumbent, byte-identical, in the position it occupies on main) and README Export Check appended after it, in all four places (the dependabot docstring inventory, REQUIRED_CONTEXTS, the package.json script block, and the ci-cd-pipeline.md table plus section order). Recording the choice because nothing in the repo pins the order and the instruction only forbade reordering or reflowing #6216's text.", "options": [ "A - as done: incumbent first, new entry appended, consistently across all four sites", "B - alphabetical, which would put readme-exports before shell-escape-residue and move #6216's row" ], "recommendation": "A. B would move #6216's landed row and section, which is exactly what the instruction forbade, and neither list is alphabetical today (REQUIRED_CONTEXTS is grouped by workflow in the order gates were added). Append-after-incumbent is what every previous gate addition in this file did, and it keeps the diff against main minimal - the only lines this PR adds are its own." }, { "question": "Unchanged and still open after three rounds: should README Export Check be a BLOCKING per-PR context (as built) or a nightly + push-to-main gate like this repo's three other build-requiring gates?", "options": [ "A - blocking per-PR, unfiltered (as built)", "B - nightly + push-to-main, matching published-dist-gate.yml / node-esm-load-gate.yml / spec-range-floors.yml", "C - blocking with a paths filter on packages/**" ], "recommendation": "A, as built. CI has measured the real cost rather than my estimate: 2m30s end to end on its first run, against a 2m42s local cold build - same order as ci.yml's own Type Check, an order below Build & E2E. One thing this round adds to the case for A: this PR's gate is what validates PR #6227's rewrite of the same README, and #6227's dev had to fetch the script off this branch to run the census at all. A gate that only runs nightly could not have served that purpose. Demoting is still a two-line trigger change plus moving the name out of REQUIRED_CONTEXTS." } ], "out_of_scope_findings": [ "filed as #6213 (unchanged, and now being repaired by PR #6227): packages/core/src/adapters/README.md documents an ObjectStack adapter that no longer lives in that directory at all. This PR repaired only the two import paths; #6227 deletes the ObjectStack half wholesale and lands after this one.", "filed as #6214 (unchanged): tier 2 of this card (extract-blocks-and-compile + bidirectional pins), carrying the already-measured design points. Blocked on a baseline ruling, not on effort.", "not filed, resolved instead: the #6216 collision reported in the previous round as a coming conflict materialised exactly as predicted, at exactly the three anchors named, and is now resolved to the union in this branch. Nothing outstanding." ] }Branch head
dc79e7029(merge commit resolving three anchors to the union). PR #6212 base now readsf66072d1b, non-draft, auto-merge deliberately not re-armed. Body updated: the previous round's "#6216 has not landed" section is replaced with the resolution, and the verification block is restated at the new head. CI convergence is PM's read.
Generated by Claude Code
yinlianghui-tw commented
on Aug 25, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 5043, "status": "done", "branch": "claude/issue-5043-readme-exports-gate", "pr": "https://github.com/objectstack-ai/objectui/pull/6212", "premise_still_valid": true, "summary": "FINAL RESOLUTION ROUND (supersedes the three earlier reports on this card). The premise checked out on disk exactly as stated: origin/main is 0409b766d, #6227 landed as a26b9e4f8, and the conflict was the single one described. Merged origin/main as a MERGE COMMIT (653000878) - never a rebase, never a force-push, the third such merge on this branch. ONE conflict, packages/core/src/adapters/README.md, resolved by taking #6227's version WHOLESALE; the two import paths this PR had repaired no longer exist on the page, so they were NOT re-applied. Verified byte-identity rather than eyeballing it: `git diff origin/main -- packages/core/src/adapters/README.md` is EMPTY, so this PR now contributes nothing to that file. Checked rather than assumed that the three anchors resolved last round (dependabot-merge-gate.mjs, package.json, ci-cd-pipeline.md) did not re-conflict - none did, consistent with #6228 adding no gate to REQUIRED_CONTEXTS. THEN THE CHANGESET: re-read the remaining diff and DROPPED the @object-ui/core patch changeset. Every remaining path is tooling, CI, the docs-site guide page, or the private root manifest - zero published source of any released package - and #6227's own changeset already carries a @object-ui/core plus @object-ui/data-objectstack patch for exactly the fix mine described. Keeping it would have published a release note for a fix this diff does not contain and double-counted the bump. This PR is now pure tooling. Auto-merge NOT re-armed, per instruction.", "tests": "All at c9496f043, the final merged head, exit codes captured by redirect BEFORE any pipe, each verdict quoted from the line the gate itself printed. The merge moved README content between packages, which is exactly what this gate weighs, so the tree was REBUILT first (turbo run build --filter='./packages/*', 3m18s, 39/39 successful, 37 dist type entries). FRESH CENSUS, and it MOVED as predicted: check:readme-exports EXIT=0 'check-readme-exports: OK (43 README(s) under packages/ (0 outside any package), 399 fenced block(s) (304 parsed as code, 6 untagged); 497 import binding(s), 378 of them self-imports judged (378 real, 0 wrong-path, 0 fabricated); 3242 export symbol(s) read from 37 of 40 package(s) (39 carry a README) (0 unbuilt, 3 declare no types); 17 side-effect import(s), 0 namespace, 11 deep self-path, 91 to other packages)'. Deltas across this round: fenced blocks 396 to 399, parsed 301 to 304, bindings 495 to 497, self-imports judged 374 to 378, and 'to other packages' 93 DOWN to 91. DRIFT IS STILL ZERO - 0 fabricated, 0 wrong-path. Confirmed the movement is #6227's relocation and not something else, by reading --list rather than inferring: createObjectStackAdapter now reads 'real' as a SELF-import at packages/data-objectstack/README.md:24, the package that actually exports it, and packages/core/src/adapters/README.md now carries 4 self-imports naming ApiDataSource, ValueDataSource, resolveDataSource and runBatchTransaction - the exports #6227 documented. The drop of exactly two in 'to other packages' is this PR's two former wrong-path lines becoming self-imports elsewhere. REST OF THE UNION: check:shell-escape-residue EXIT=0; check:control-bytes EXIT=0; check:pre-install-import-graph EXIT=0; check:vi-mock-specifiers EXIT=0; check:doc-fences EXIT=0; check:doc-types EXIT=0; check:skills-paths EXIT=0; check-doc-links.mjs EXIT=0; check-doc-snippet-types.mjs EXIT=0; check-changeset-presence.mjs EXIT=0; check-changeset-fixed.mjs EXIT=0; check-changeset-no-major.mjs EXIT=0; check-lint-coverage.mjs EXIT=0; check-type-check-coverage.mjs EXIT=0; pnpm type-check:scripts EXIT=0; pnpm lint:root EXIT=0 '28 problems (0 errors, 28 warnings)', all pre-existing. WHOLE DIRECTORY from the repo root, root vitest only per objectui#3378: EXIT=0, 'Test Files 77 passed (77) | Tests 2187 passed (2187)' - up from 77/2170 as this merge brought more cases in. THE CHANGESET DECISION, measured both ways: with the stale changeset present the presence gate reported '8 file(s) changed, 0 of them published source of a package the release covers, 0 under a package changesets ignores, 1 changeset(s) added' and EXIT=0; with it dropped, '7 file(s) changed, 0 of them published source ... 0 changeset(s) added' and EXIT=0, verdict line 'No source of a released package changed in this range, so no changeset is owed.' The gate exits 0 EITHER WAY, so nothing mechanical would have caught the false release note - that is why the instruction not to leave it as-is was the load-bearing part, and I re-read the diff instead of trusting the green.", "open_questions": [ { "question": "Recording the changeset call explicitly since it is the one judgement this round asked for and no gate can check it: the @object-ui/core patch changeset was DELETED rather than rewritten.", "options": [ "A - delete it (as done): nothing in the remaining diff ships from any released package", "B - rewrite it to describe the tooling change", "C - replace it with an empty-frontmatter changeset declaring 'this releases nothing'" ], "recommendation": "A. B is wrong because scripts/, .github/workflows/, content/docs and the private root manifest are not published by any package, so there is no package to score and no release note to write. C is the convention where a gate DEMANDS a declaration, and this one does not: check-changeset-presence explicitly reports 'no changeset is owed' at 0 published source files, and PR #6216 - the closest precedent, a tooling-only gate PR that landed tonight - carried no changeset at all for the same reason. Deleting also removes the double-count: #6227's changeset already bumps @object-ui/core for precisely the README fix mine described." }, { "question": "Unchanged and still open after four rounds: should README Export Check be a BLOCKING per-PR context (as built) or a nightly + push-to-main gate like this repo's three other build-requiring gates?", "options": [ "A - blocking per-PR, unfiltered (as built)", "B - nightly + push-to-main, matching published-dist-gate.yml / node-esm-load-gate.yml / spec-range-floors.yml", "C - blocking with a paths filter on packages/**" ], "recommendation": "A, as built, and this round strengthened the case with evidence rather than argument. #6227 exists because this gate found the defect; its author ran the script off this branch to measure the census; and the post-#6227 census above is what confirms the repair landed correctly, with createObjectStackAdapter now homed in the package that exports it. A gate running only nightly could not have served any of those three roles. Cost is measured twice - 2m30s in CI, 2m42s local cold build, same order as ci.yml's own Type Check and an order below Build & E2E. Demoting remains a two-line trigger change plus moving the name out of REQUIRED_CONTEXTS." } ], "out_of_scope_findings": [ "#6213 is CLOSED OUT, not outstanding: filed by me this session, repaired by PR #6227 (a26b9e4f8) while this PR sat in the queue. Its fix is now on main and this branch takes it wholesale.", "filed as #6214 (unchanged): tier 2 of this card (extract-blocks-and-compile + bidirectional pins), carrying the already-measured design points. Blocked on a baseline ruling, not on effort.", "nothing new found this round. All three collisions this card predicted across four rounds - #6216's three anchors and #6227's README rewrite - have now materialised and been resolved; nothing is outstanding against this branch." ] }Branch head
c9496f043. Merge commit653000878(one conflict, resolved to #6227's version wholesale) plus the changeset removal. PR base now reads0409b766d, non-draft, auto-merge deliberately not re-armed. Body updated: the drift section now records the supersession, the census is restated with the moved numbers, and a "No changeset" section explains the deletion and notes the gate is green either way. CI convergence is PM's read.
Generated by Claude Code
发现于 #5010 的实施(plugin-calendar README)。Filed unassigned, not claiming.
现象
packages/*/README.md里指向本包自己的import { X } from '@object-ui/[本包名]'—— 没有任何门禁核对X是否真的被本包导出。现存两个 doc 门禁都看不到这一层:scripts/check-doc-links.mjs—— 只解析链接,不看代码块里的标识符;scripts/check-doc-component-types.mjs—— 只扫content/docs,不进packages/*/README.md。后果不是假设:#5002 家族一次巡查就在 7 个包命中(calendar #5010、form #5011、gantt、grid、view 的
*Components手动注册虚构;dashboard #5015、report #5016 的类型导入虚构),每一处照抄都是TypeError或 TS2305/TS2724。这些 README 都在各包package.json的files里,随 npm 包发布给用户。为什么值得机制化(而不是继续逐包人工修)
#5010 楼上立卡人的方法学备注已经记了三次方法迭代,每一次都因为近似而漏报:
src/有没有出现」—— 被注释里的名字放过(plugin-dashboard README 的 TypeScript 一节导入DashboardSchema/MetricCardSchema,两个名字在全仓都不存在 #5015 的DashboardSchema只出现在注释里),也会把export * as X from …误报成虚构(plugin-chatbot 的AIElements实为真导出);也就是说 7 个包是下限,不是全集。人工方法每迭代一次就发现上一版漏了一类,这正是该交给脚本的形状。
已验证的检查器原型(#5010 实施中写成并用过)
思路:不做文本近似,直接问编译器。
checker.getExportsOfModule),并解析 alias 后再判 value/type —— 注意export { ObjectCalendar }是 Alias 符号、不带 Value flag,按 alias 判会把每个 re-export 误标成 type(原型第一版就踩了这个);//与/* */注释再按逗号切(不剥的话多行块里的行尾注释会被粘到前一个标识符上,报成虚构名 —— 原型第二版的假阳性就是这个);calendarComponents手动注册 API 不存在 —— 照抄即抛 #5010 的CalendarViewSchema就是这一类,修法是改路径而非删节,与虚构名不同)。在 plugin-calendar 上的实测读数(修复前):
calendarComponents→ 虚构;CalendarViewSchema→ 路径错(真身在@object-ui/types);CalendarEvent→ 真。反向验证:往多行块里各塞一个假名,两个都被抓出(单行 grep 看不到的正是这一类)。原型是 #5010 的一次性 scratch,没有入库。要机制化的话建议做成
scripts/check-readme-exports.mjs+ 一条check:readme-exports,对全部packages/*/README.md跑。更强的一档(可选,成本更高)
真正的上界是把 README 的 TypeScript 代码块抽出来喂 tsc(类似 docs 的 doctest)。#5010 里正是这么发现第三处漂移的:
CalendarViewSchema路径改对之后示例仍然不编译 —— 同一个例子里的CalendarEvent是真导出,但是CalendarView组件的运行时类型(id: string | number、start: Date),不是CalendarViewSchema.events要的授权 JSON 类型(id: string、start: string | Date),ISO 字符串字面量对不上,TS2322×3。名集合核对看不见这一类(名字全真),只有真编译看得见。详见 #5044。两档可以叠:名集合核对便宜、可全仓跑;抽块编译贵,但能抓住「名字全真、类型不对」的漂移。
分级
交 PM 分诊。挂
finding:门禁缺失本身不是用户今天直接撞到的 bug,它的用户可见后果已经分别立卡(#5010–#5016);这一单是那一族的根因 + 复发预防。