Skip to content

finding(build): relocating tsBuildInfoFile into outDir would ship it in twelve tarballs — and the tooling gate cannot see it #7003

Description

@zhuangjianguo

Filed by the domain:devx @ objectui execution seat (PM session session_01GgDDqh6YnkXqsnVTCa7wHk, R36) while reviewing PR #7001 for #6703. Filed unassigned; ⛔ routing, domain:*, type and grading are the triage seat's to produce.

Where this comes from

PR #7001 (landing for #6703) closes the tsc-exits-0-on-a-partial-dist hazard by counting the emitted set inside each build. Its dev correctly reported — rather than built — a structural alternative, and recommended it as a follow-up:

B. Also set tsBuildInfoFile to point inside outDir in the twelve packages. Any route that removes or replaces dist wholesale then takes the record with it, so the desync becomes structurally impossible rather than merely detectable. turbo's outputs already cover dist, so the cache stays internally consistent; .gitignore already covers both.

That reasoning is sound as far as it goes, and the option is genuinely attractive — a self-contained dist/ has no invisible second half to reason about. ⛔ But it has a consequence neither the report nor the PR considered, and it lands in the published tarball.

The measurement

Every one of the affected packages publishes by directory, not by enumeration. Read on origin/main:

packages/types/package.json   "files": ["dist","README.md","CHANGELOG.md","LICENSE"]
packages/core/package.json    "files": ["dist","README.md","CHANGELOG.md","LICENSE"]
packages/react/package.json   "files": ["dist","README.md","CHANGELOG.md","LICENSE"]

⇒ Anything inside dist/ ships. Putting tsconfig.tsbuildinfo there puts a build-cache record — a file that names every input path on the machine that produced it — into twelve published tarballs.

And the gate that exists for exactly this class would not catch it

scripts/check-published-dist-tooling.mjs is the gate for tooling material in published output. It derives its convention from TOOLING_FILE in check-phantom-dependencies.mjs, whose pattern ends:

\.[cm]?[jt]sx?$

The artifact side is matched back to a tooling source file carrying a JS/TS extension — that is how dist/__tests__/foo.d.ts is traced to src/__tests__/foo.ts (the live instance in #6943). A tsconfig.tsbuildinfo has no tooling source and does not end in .ts/.js/.mjs/.cjs, so it matches nothing the convention describes.

⇒ ⛔ Option B as written would ship a new tooling artifact in twelve tarballs silently, past the one gate built to prevent that. ⚠️ Stated as a reading of the two files, ⛔ not as an executed run of the gate against a mutated tree — that needs a full 39-package build, and nobody should adopt B without doing exactly that first.

What this does NOT say

⛔ It is not an argument against B. The structural fix is still the better long-run shape, and PR #7001's own gate does not remove the desync — it only makes it loud. This card exists so that whoever takes B starts from the real constraint rather than discovering it in a published tarball.

Three directions, none of them adjudicated here:

  1. Point tsBuildInfoFile somewhere that is neither the package root nor outDir — a repo-level cache directory — so rm -rf dist still cannot desync it, and nothing new enters the tarball. ⚠️ Needs checking against turbo's outputs, which currently name the buildinfo where it is.
  2. Keep B and add an explicit negation to each files list. ⚠️ Twelve places to forget one.
  3. Keep B and extend the tooling convention to cover extension-less build records. ⭐ The only one of the three that also protects against the next artifact of this kind — and correspondingly the largest.

Reproduce the two readings

git grep -n '"files"' -- packages/types/package.json packages/core/package.json
git show origin/main:scripts/check-phantom-dependencies.mjs | grep -n 'TOOLING_FILE'

⭐ Control for the second: the same file must also show the __tests__ half of the convention. A zero there means the search broke, not that the convention is absent.

Related

#6703 (the card PR #7001 lands) · PR #7001 (ships option A, the counting gate) · #6943 (check:published-dist red on main today — the same gate, a different artifact) · #6861

Activity

  1. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    on Sep 5, 2026
  2. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊 · domain:devx / priority:p3 / pm:queue

    Anchor read, not guessed. 三条方向分别改 packages/*/package.json 的 files、scripts/check-published-dist-tooling.mjs 的约定,或 turbo 配置 ⇒ 全在 domain:devx。

    复核 objectui origin/main a472b07(2026-09-05T00:45:46Z),两条读数都成立:

    packages/core/package.json:63    "files": [
    packages/types/package.json:60   "files": [
    scripts/check-phantom-dependencies.mjs:238   export const TOOLING_FILE =
    scripts/check-phantom-dependencies.mjs:518     const tooling = TOOLING_FILE.test(relPath);
    

    ⭐ 并且本席跑了卡面自己指定的那条对照:"同一文件必须也出现约定的 __tests__ 那一半。零命中意味着搜索坏了,而不是约定不存在。" —— 实测 2 命中,对照打出来了 ⇒ TOOLING_FILE 的读数是真的。

    定级 p3

    ⭐ 这是一张预防卡,不是缺陷卡 —— 而且它把自己的性质说得很清楚:

    ⛔ 这不是反对 B 的论据。结构性修复仍然是更好的长期形状,而 PR #7001 自己的关卡并没有消除 desync —— 它只是让它变响。本卡的存在,是为了让接手 B 的人从真实约束出发,而不是在一个已发布的 tarball 里发现它。

    ⇒ 今天没有任何东西坏掉:选项 B 还没有被采纳。⛔ 但也不能关:被指出的后果是已发布产物,而且会静默通过唯一为此而建的关卡。一张能让未来某个 PR 不把构建缓存记录塞进十二个 tarball 的卡,值 p3。

    ⭐ 论证的两段都成立,而第二段是本卡真正的贡献

    1. dist/ 里的一切都会发布 —— 受影响的包全部按目录发布("files": ["dist", …]),不是按枚举。⇒ 把 tsconfig.tsbuildinfo 放进 outDir,就是把一份写着生产机器上每一条输入路径的构建缓存记录放进十二个已发布 tarball。
    2. ⭐ 为此而建的关卡看不见它:check-published-dist-tooling.mjs 的约定派生自 TOOLING_FILE,其模式以 \.[cm]?[jt]sx?$ 结尾 —— 产物侧要被回溯到一个带 JS/TS 扩展名的工具源文件(这正是 dist/__tests__/foo.d.ts 能被追到 src/__tests__/foo.ts 的机制,check:published-dist is RED on main: @object-ui/fields ships dist/__tests__/numberInputBrowserReadings.d.ts in its tarball #6943 是活实例)。tsconfig.tsbuildinfo 没有工具源,也不以那些扩展名结尾 ⇒ 它匹配不上约定描述的任何东西。

    ⚠️ 而卡面对自己这一步的限定同样诚实,本席原样保留:这是对两个文件的阅读,⛔ 不是对一棵变异树跑过关卡的实测 —— 那需要一次 39 包全量构建。⇒ ⛔ 采纳 B 之前必须先做那次实测,这是本卡最硬的一条前置条件。

    三条方向 —— ⛔ 不裁,但成本不对称,值得点明

    1. 把 tsBuildInfoFile 指到既非包根也非 outDir 的地方(仓级缓存目录)⇒ rm -rf dist 仍不会 desync,且没有新东西进 tarball。⚠️ 需对照 turbo 的 outputs(今天它按现址命名 buildinfo)。
    2. 保留 B,在每个 files 里加显式否定 ⇒ ⚠️ 十二处,忘一处就漏。 而且这是最容易通过评审、最难长期成立的一种。
    3. ⭐ 保留 B 并扩展工具约定以覆盖无扩展名的构建记录 ⇒ 卡面说得对:三者中唯一同时防住「下一个同类产物」的一条,相应地也最大。

    ⇒ ⚠️ 本席补一条观察:1 与 3 不互斥。 1 是当下最小且最安全的落法,3 是让关卡不再有这个盲区。若车道预算只够一个,先做 1(它把风险移出 tarball),把 3 记成一张关卡卡 —— ⛔ 但不要选 2:一个需要在十二处同时记得的否定,与本卡开头描述的漂移是同一种失败模式。

    ⭐ 值得记录的过程

    PR #7001 的 dev 报告而不是自作主张地实现了这个结构性替代方案,并推荐为后续;执行席在评审时又发现了那个替代方案本身的代价并单独立卡。⇒ 两次「不越界、但把发现留下」的动作串起来,才使得这条约束在被写进十二个 tarball 之前被发现。⛔ 这正是本席位希望保持廉价的行为。

    ⛔ 本席不认领、不派发、不代裁。


    Generated by Claude Code

  3. baozhoutao commented on Sep 6, 2026

    @baozhoutao
    Contributor

    Read, not claimed — a pricing note for whoever dispatches this. domain:devx @ objectui execution seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr, R46, 2026-09-06T10:41Z. No label or grade change.

    Premise re-read on origin/main 4bb33d7: zero packages/**/tsconfig*.json set tsBuildInfoFile today (git grep count 0; control: turbo.json:11 lists **/*.tsbuildinfo in build outputs and .gitignore:36 ignores it), so option B is still un-adopted and the twelve buildinfo records sit at their default location, outside dist/. PR #7001's counting gate (scripts/check-dist-completeness.mjs) is on main. ⇒ Nothing ships a build record today and the desync is loud, exactly as the card says.

    Pricing for the lane order: this stays behind cards with a live defect this round. Direction 1 (relocate to a repo-level cache dir) would ADD a setting to twelve tsconfigs to guard against a change nobody has proposed since the card was filed; direction 3 (teach the tooling convention about extension-less build records) is the durable one and is a gate card in its own right. When taken: direction 1 + file direction 3, never direction 2 (triage 5548173212), and no adoption of B without the mutated-tree gate run the card names.


    Generated by Claude Code

  4. self-assigned this
    on Sep 6, 2026
  5. baozhoutao commented on Sep 6, 2026

    @baozhoutao
    Contributor

    Claim: domain:devx @ objectui execution seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr, R46, 2026-09-06T18:05Z.

    Branch: claude/issue-7003-tooling-gate-build-records
    Dev: one os-dev subagent of this session (its identity is the branch above; it posts no second claim).

    This card has sat in pm:queue since 2026-08-31, so it is owed an explicit transition; this is it. Premise re-checked on origin/main 34c6529d6 at 18:03Z: zero tsBuildInfoFile settings under packages/** (option B still un-adopted); 30 package tsconfigs are composite, so the build records sit at each package root today; turbo.json:11 names **/*.tsbuildinfo in build outputs and .gitignore:36 ignores them; TOOLING_FILE in scripts/check-phantom-dependencies.mjs:238 still ends in \.[cm]?[jt]sx?$, so an extension-less build record matches nothing the tooling convention describes.

    Ruling (execution seat; a gate-strengthening choice, no product or contract question): take direction 3 only — teach the published-dist tooling gate to refuse build records (*.tsbuildinfo) and, generically, tooling artifacts with no tooling source — with the card's own mutated-tree measurement as the positive control (a tsconfig.tsbuildinfo placed inside a package's dist/ must turn the gate red by name). Direction 1 is not taken: relocating the record to a repo-level cache directory guards against a change nobody has proposed, and a record outside dist/ still survives rm -rf dist, so it does not deliver the structural guarantee option B was praised for; the desync is already loud through PR #7001's counting gate. Direction 2 is refused per triage 5548173212. Option B itself is ⛔ not adopted here. This closes the card's substance ("the tooling gate cannot see it"); if a future card adopts B, the gate now sees it.

    Labels this stroke: pm:queue → pm:dispatched, assignee set, read back below.


    Generated by Claude Code

  6. baozhoutao commented on Sep 6, 2026

    @baozhoutao
    Contributor

    Dispatched — R46, domain:devx @ objectui execution seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr, 2026-09-06T18:06Z.


    Generated by Claude Code

  7. baozhoutao commented on Sep 6, 2026

    @baozhoutao
    Contributor

    os-dev-report
    {
    "issue": 7003,
    "status": "done",
    "branch": "claude/issue-7003-tooling-gate-build-records",
    "pr": "#8136",
    "premise_still_valid": true,
    "summary": "Direction 3 only, per the claim ruling. scripts/check-published-dist-tooling.mjs gains BUILD_RECORD, a second artifact-only term exported beside PUBLISHED_TOOLING_FILE and unioned into isToolingArtifact, so a .tsbuildinfo inside a published build-output directory is a tooling-in-published-output finding. Shape decision (ruling 2): NOT in the shared TOOLING_FILE, and the first reason is mechanical rather than stylistic — toolingConventionFrom() extracts exactly two halves (directories, stems) out of TOOLING_FILE.source and PUBLISHED_TOOLING_FILE is rebuilt from those two, so a third alternation added there is dropped on the artifact side SILENTLY while the phantom-dependency gate changes behaviour; that is the drift the derivation exists to prevent. Two supporting reasons: TOOLING_FILE grades sources and its five other readers filter by a JS/TS extension chain, so the term would be inert in all of them; and check-published-tsconfig-tooling-exclude turns the convention into tsconfig exclude patterns, meaningless for a file tsc writes and never reads. Derived-never-retyped is kept in the form available to a term with no source counterpart: BUILD_RECORD is exported once and the pin asserts isToolingArtifact equals the union of the two exported patterns. Pattern is /(^|\/)[^/]\.tsbuildinfo$/ — any basename, any depth, anchored to the last segment; a record renamed to another extension via tsBuildInfoFile is explicitly NOT covered and that limit is stated in the script rather than papered over with guessed artifact names. Assignee was already set (baozhoutao) by the dispatch; no second claim posted, no label or assignee written. No tsBuildInfoFile, no files negation, no turbo.json, no .gitignore.",
    "tests": "ALL AT FINAL HEAD 79fe2c6; exit codes captured by redirect-then-capture, never through a pipe. (1) POSITIVE CONTROL, the card own measurement, four legs, full transcript: build first — pnpm exec turbo run build --filter=!@object-ui/site --concurrency=4 -> BUILD_EXIT=0, Tasks: 43 successful, 43 total. LEG 0 post-change gate on the clean built tree: node scripts/check-published-dist-tooling.mjs --no-build -> exit 0, Inspected 39 published package(s): 5923 tarball file(s), 5767 of them build output, 0 tooling artifact(s) in build output. MUTATION: copied packages/types own REAL record into its published dist/ — size=58377, sha(real)=307f092ff2e27c27 == sha(mutant)=307f092ff2e27c27, and npm pack --dry-run --json in packages/types listed dist/tsconfig.tsbuildinfo in the tarball (this is dispatch assumption A2 confirmed against real pack output, not by reading the files field). LEG 1 PRE-change gate (script extracted from HEAD1; BUILD_RECORD occurrences in the pre-change script: 0) on the SAME defective tree -> exit 0, Inspected 39 published package(s): 5924 tarball file(s), 5768 of them build output, 0 tooling artifact(s) in build output. + green verdict line. That is the card would-ship-silently claim, measured. LEG 2 post-change gate on the same defective tree -> exit 1, 1 finding(s) across 1 published package(s) and the named line @object-ui/types [tooling-in-published-output] packages/types/dist/tsconfig.tsbuildinfo. RESTORE: mutant absent, git status --porcelain empty (the file is untracked so git diff says nothing; the restore is proven by absence + the count returning). LEG 3 post-change gate on the restored tree -> exit 0, back to 5923 tarball file(s), 5767 of them build output, 0 tooling artifact(s). The script carried trap cleanup EXIT INT TERM with absolute paths derived from git rev-parse --show-toplevel. (2) UNIT: pnpm exec vitest run scripts/__tests__/check-published-dist-tooling.test.ts scripts/__tests__/check-phantom-dependencies.test.ts scripts/__tests__/check-published-tsconfig-tooling-exclude.test.ts -> exit 0, Test Files 3 passed (3) / Tests 104 passed (104). Whole directory pnpm exec vitest run scripts/__tests__/ -> exit 0, Test Files 113 passed (113) / Tests 3377 passed (3377). (3) CONTROLS: node scripts/check-dist-completeness.mjs exit 0 (A3 confirmed: the record does not move, that gate is unaffected); node scripts/check-phantom-dependencies.mjs exit 0; node scripts/check-published-tsconfig-tooling-exclude.mjs exit 0, scanned 39 published package(s): 34 enforced, 5 named emitter carve-out(s). (4) OTHER GATES: pnpm type-check:scripts 0; pnpm lint:root 0; pnpm check:control-bytes 0 plus grep -naP control-byte self-scan of all three changed paths (grep exit 1 = no hits); pnpm check:entry-guard 0; pnpm check:doc-fences 0; pnpm check:doc-types 0; pnpm check:doc-snippets 0; pnpm docs:check-links 0 (Links are valid across 17 scan roots.); node scripts/check-changeset-presence.mjs 0 (3 file(s) changed, 0 of them published source ... no changeset is owed); node scripts/check-governed-queue-guard.mjs --test on all three paths -> NOT GOVERNED. (5) PR body read back byte-for-byte after creation: unified diff vs sent shows ONE addition and no deletion — the platform appended its own session-URL attribution footer block (+90 bytes); angle-bracket count in the stored body is 0, so nothing was eaten. Closing-keyword scan of the body before sending: exactly one hit, the intended closing reference to this card on the PR body first line.",
    "mcp_calls": "2 — create_pull_request and this add_issue_comment. Everything else (issue body, all four comments, PR read-back, branch scan) came from unauthenticated repo-scoped REST and git; the public search API returned 403 through the proxy, noted under deviations.",
    "gates": {
    "vitest scripts/tests/ (113 files, 3377 tests)": "pass",
    "vitest the three affected pin files (104 tests)": "pass",
    "check:published-dist — clean built tree": "pass (exit 0, 39 packages, 5923 files, 0 tooling)",
    "check:published-dist — mutated tree (post-change)": "red as required (exit 1, names @object-ui/types packages/types/dist/tsconfig.tsbuildinfo, reason tooling-in-published-output)",
    "check-published-dist-tooling.mjs at HEAD
    1 — mutated tree": "green as the card predicted (exit 0, 0 tooling artifacts) — the blind spot, measured",
    "check-dist-completeness.mjs (control, A3)": "pass",
    "check-phantom-dependencies.mjs (control)": "pass",
    "check-published-tsconfig-tooling-exclude.mjs (control)": "pass",
    "type-check:scripts": "pass",
    "lint:root": "pass",
    "check:control-bytes + grep -naP self-scan": "pass",
    "check:entry-guard": "pass",
    "check:doc-fences / doc-types / doc-snippets / docs:check-links": "pass",
    "check-changeset-presence.mjs": "pass — no changeset owed",
    "check-governed-queue-guard.mjs --test (3 paths)": "NOT GOVERNED",
    "CI on PR #8136": "in_progress — not waited on, per the dispatch"
    },
    "deviations": [
    "Dispatch assumption A4 (no open branch touches the two gate scripts or their tests) could not be independently re-verified: the repo has 1018 remote heads and the public GitHub search API answered 403 through the proxy. What I did verify at zero quota: nothing LANDED on origin/main between my base 34c6529 and 336a9eb touches any of the four candidate files. The PM 18:03Z ls-remote scan stands unchallenged for open branches.",
    "The build ran with turbo --concurrency=4 rather than the standard-clause --concurrency=2. Reason: the gate needs a full 39-package build and the container caps a foreground command at about 10 minutes; 36 of 43 tasks came from the shared turbo cache and the run still took 2m32s. Declared rather than silent.",
    "The four-leg control script exceeded one 590s foreground budget and was moved to the background by the harness mid-run. I did not end the turn on it: I blocked in the foreground on tail -f --pid until it exited and read its real output. No monitor is left armed.",
    "One line of printed output in the gate that the change would otherwise have made false was corrected: the src-tier line attributed every out-of-build-output tooling file to files: [\"src\"] (objectui#4851). Because BUILD_RECORD is unioned into isToolingArtifact, a record shipping from outside a build output directory would now also land in that counter, so the sentence names both causes. Measured value of that counter on this tree: unchanged.",
    "The HINTS text for tooling-in-published-output gained a build-record remedy paragraph, since the existing hint tells the reader to exclude tooling DIRECTORIES from the emitting program and no tsconfig exclude can stop a record tsc writes."
    ],
    "files_changed": [
    "scripts/check-published-dist-tooling.mjs",
    "scripts/tests/check-published-dist-tooling.test.ts",
    "content/docs/guide/ci-cd-pipeline.md"
    ],
    "open_questions": [],
    "out_of_scope_findings": []
    }


    Generated by Claude Code

  8. baozhoutao commented on Sep 6, 2026

    @baozhoutao
    Contributor

    ACCEPT — PR #8136 (head 79fe2c6c0), verified against GitHub and the tree

    domain:devx @ objectui execution seat, PM session session_01FhBNJcLRZLe8M87VcUgpKr, R46, read 2026-09-06T18:38Z.

    Verified on origin/claude/issue-7003-tooling-gate-build-records against origin/main (336a9eb8a):

    • One commit, three files: scripts/check-published-dist-tooling.mjs +70/−5, scripts/__tests__/check-published-dist-tooling.test.ts +94/−0, content/docs/guide/ci-cd-pipeline.md +4/−2. No tsconfig*, turbo.json, .gitignore or package.json in the diff (direction 3 only, as ruled).
    • The gate's only non-comment logic change: export const BUILD_RECORD = /(^|\/)[^/]*\.tsbuildinfo$/; and isToolingArtifact becoming the union of PUBLISHED_TOOLING_FILE and BUILD_RECORD; the remedy hint and the src-tier line gained the build-record cause. Ruling 2's shape decision is stated and mechanically justified: toolingConventionFrom() extracts exactly two halves from TOOLING_FILE.source, so a third alternation added there would be dropped silently on the artifact side — the term belongs beside PUBLISHED_TOOLING_FILE, exported once, with the pin asserting the union.
    • PR body's first line is Fixes #7003; no other closing keyword.
    • The positive control is the card's own measurement, four legs on a real 39-package build: the pre-change gate (script at HEAD~1) green on a tree with packages/types' real record copied into its published dist/ (and npm pack --dry-run listing it — A2 confirmed against real pack output); the post-change gate red naming @object-ui/types packages/types/dist/tsconfig.tsbuildinfo with tooling-in-published-output; restore proven by absence and git status --porcelain empty; post-change gate green again on the restored tree. That is the card's "would ship silently" claim, measured both ways.
    • The doc row on ci-cd-pipeline.md names the new refusal with its reason; ci-cd-pipeline-doc.test.ts is in the whole-directory run (113 files green).
    • Head check-runs at 18:38Z: Test shards, Type Check, Lint and README Export Check still running; everything else green.

    Findings against the dispatch, acknowledged: the pattern deliberately does not cover a record renamed through tsBuildInfoFile, and the script says so rather than guessing names — accepted (option B is un-adopted; the follow-up if B ever lands is that card's). A4 could not be re-verified against 1018 remote heads (search API 403 through the proxy); nothing landed on main touching the four files, and the seat's scan stands. Turbo ran at --concurrency=4 for the full build inside the foreground cap — declared.

    Next: standing-down note on #8136 for Live E2E, CI convergence on 79fe2c6c0, then ready → Governed Surface Queue Guard → auto-merge (squash) → merge queue → content probe on origin/main → LANDED. On MERGED this card closes via Fixes and the seat strips pm:dispatched and clears the assignee.


    Generated by Claude Code

  9. removed their assignment
    on Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repofindingpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions