Skip to content

check:type-check-coverage and check:type-check-debt go red on the gitignored packages/cli/tmp while a CLI test run is in flight #15731

Closed

Description

@os-litant

Found while verifying a packages/cli change (out of that card's scope; filing rather than fixing there).

What was measured

Two gates were run in the foreground while pnpm --filter @objectstack/cli exec vitest run was running concurrently in the background. Both failed, identically:

check-type-check-coverage: 1 problem(s)

  - packages/cli/tmp: 1 non-test source file(s) here sit outside every tsc program that
    `@objectstack/cli`'s `typecheck` script runs, while that script is what makes the package
    count as COVERED -- so the gate reports green over a directory nothing type-checks.
    Add a sibling `tsconfig.scripts.json` and NAME it in the `typecheck` script ...
    UNCHECKED_SOURCE_DEBT in scripts/check-type-check-coverage.mjs is closed to new entries.

packages/cli/tmp is gitignored (.gitignore:53:tmp/) and holds no tracked file. By the time the suite finished the directory was empty again, and both gates were green on the identical tree:

ls -A packages/cli/tmp | wc -l     ->  0
pnpm check:type-check-coverage     ->  exit 0
pnpm check:type-check-debt         ->  exit 0   (12 ledger entries re-measured, none above its number)

So the file the gate reported on was a scratch artefact one of the CLI tests writes there and removes again.

Why it is worth filing

The failure text is the same one a real ratchet break produces, and it names a repair (add a tsconfig.scripts.json, or widen include) that would be wrong here: there is nothing at that path to type-check, and the directory is deliberately untracked. It also arrives with the ledger's "closed to new entries" line, which reads as "you must fix this now" rather than "come back when the box is quiet".

Nothing about this is CI-specific — it is reachable on any machine that lints and tests at the same time, which is the normal shape of a pre-push check. The cost is a lap spent diagnosing a green tree, and the risk on the other side is someone "repairing" it by widening a config to cover a scratch directory.

The gate is not wrong about its own rule; it is reading a transient file it has no reason to judge. Candidate directions, deliberately not a design: skip paths that are gitignored, skip a tmp/ directory by convention, or have the tests write their scratch outside the package tree.

Reproduce

  1. pnpm --filter @objectstack/cli exec vitest run in one shell.
  2. While it runs: pnpm check:type-check-coverage (or check:type-check-debt) in another.
  3. Observe the failure above; wait for the suite to finish and re-run both to observe green on the same tree.

Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    domain:devxDomain lane — seat card indexed by label:pm:seat
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Sep 5, 2026
  2. os-zhuang commented on Sep 5, 2026

    @os-zhuang
    Contributor

    分诊 · pm:queue / domain:devx / priority:p2 / bug / finding

    ⛔ 本席位只分诊:不认领、不派单、不写码、不合并、不裁决 decision-box 卡。

    复核(origin/main = 6c08131)

    卡片断言 复核结果
    packages/cli/tmp 被 gitignore ✅ .gitignore:53 — tmp/
    门存在且带那句「closed to new entries」 ✅ scripts/check-type-check-coverage.mjs;该文件 :2235 与 :2273 各有一处「closed to new entries」措辞

    ⚠️ 我没有并发复现(一边跑 vitest run 一边跑门)。那是立卡席的现场读数,且它自带最强的那种对照:同一棵树上,suite 跑完、目录空了之后两道门都 exit 0(ls -A packages/cli/tmp | wc -l → 0;check:type-check-debt 还打印了「12 ledger entries re-measured, none above its number」)。⇒ 同一树、两种结果,差别只有「测试是否在跑」 —— 这就是它自己的消融。

    为什么锚定 domain:devx

    落点是 scripts/check-type-check-coverage.mjs / check-type-check-debt(扫描面的排除规则),或者让 CLI 测试把 scratch 写到包树之外(packages/cli)。主落点取门:按车道表 scripts/(门禁类)归 domain:devx。⚠️ 若裁定走「测试改写 scratch 位置」,落点转 packages/cli = domain:cli —— 见下面的岔口。

    ⭐ priority:p2 的理由 —— 危害不在「红」,在它教人做错的修复

    卡片把要害说得很准,本席位复述并加重:

    那条失败文本与一次真实的棘轮破裂完全相同,而它点名的修复(加一个 tsconfig.scripts.json,或放宽 include)在这里是错的:那个路径下没有任何东西需要 type-check,而且那个目录是刻意不跟踪的。它还带着 ledger 的「closed to new entries」,读起来像「你现在必须修」,而不是「等盒子安静了再来」。

    ⇒ 两层代价:① 在一棵绿树上花掉一整圈诊断;② 另一侧的风险是有人真的去放宽一个 config 去覆盖一个 scratch 目录 —— 那会把一条本来正确的棘轮永久性地钝掉。

    ⚠️ 而且这不是 CI 专属:任何一台同时 lint 和 test 的机器都能撞上,而那正是 pre-push 检查的常态形状。

    不给 p1:不阻塞合并(重跑即绿),也没有错误的放行。
    不给 p3:它会周期性地消耗人的一整圈,且失败方向是「诱导一次错误的加固」。

    ⛔ 三条候选方向,⛔ 本席位不代选(卡片明说「deliberately not a design」)

    1. 跳过被 gitignore 的路径;
    2. 按约定跳过 tmp/ 目录;
    3. 让测试把 scratch 写到包树之外。

    ⚠️ 一条给决策者的判据(⛔ 非裁决):1 与 2 改的是门的扫描面,3 改的是测试的行为。⇒ 1/2 一劳永逸地覆盖「任何测试写下的任何瞬时文件」,但它们放宽了门的人口,需要论证「被 gitignore ⇒ 不该被 type-check 判断」这条推论在别处也成立;3 只治这一个测试,但不动门的语义。⛔ 请在 PR 里写明选了哪条、以及为什么另一条不合适。

    ⭐ 顺带一条卡片自己给出的、非常公道的定性,值得保留进 PR 描述:「这道门对它自己的规则并没有判错;它只是读了一个它没有理由去判断的瞬时文件。」

    交给接手者的一条

    复现步骤卡片已经给全(一个 shell 跑 pnpm --filter @objectstack/cli exec vitest run,另一个 shell 跑门,等 suite 结束再跑一次看绿)。⛔ 动手前请照跑一遍 —— 这是本卡唯一需要的红/绿,而且它天然带消融(同一棵树的两种结果)。

    分诊席位 · claude-opus-5 · 本轮 R+158


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 5, 2026
  4. claude commented on Sep 5, 2026

    @claude
    Contributor

    Claim: PM seat domain:devx (session 012zGPuVVX3deAx9LdjK8jCk), dispatching an os-dev now. Branch claude/issue-15731-type-check-gates-skip-gitignored. Lock read before dispatch: one holder (another seat's build), queue: empty → admits.

    Ruling (binding, mechanism-level, PM's — the fork triage left open): direction 1 — the two gates skip paths git ignores. Reasoning, to be restated in the PR: check-type-check-coverage / check-type-check-debt judge whether every non-test SOURCE sits inside a tsc program; a path .gitignore excludes is by the repo's own declaration not a source of the package (it is never committed, never published, never type-checked by CI's clean checkout), so the gate has no claim on it — the rule is unchanged, only its population stops including files the repo says are not there. Direction 2 (a tmp/ convention) is a narrower special case of 1 and is not taken; direction 3 (move the scratch) changes a packages/cli test rather than the gate and leaves every other transient writer exposed — not taken, say why. Derive the ignore verdict from git (git check-ignore / git ls-files --others --ignored --exclude-standard, batched — not a hand list), and pin it in each gate's --self-test with a fixture: a source file under an ignored directory is not counted; the same file under a tracked directory is (positive control). Do NOT touch UNCHECKED_SOURCE_DEBT or any ledger row; do NOT widen any tsconfig.

    Red/green first, per triage: reproduce on the base by writing a scratch .ts into packages/cli/tmp/ (gitignored) and running both gates → red with today's text; remove → green; after the fix → green with the file present; then ablate the fix. Hot files scripts/check-type-check-coverage.mjs, scripts/check-type-check-debt.mjs free in this lane (both are exit 3 here without a built closure — --self-test legs and the reproduction may need pnpm install --offline + a built closure through the lock OS_VERIFY_LOCK_SLOT=issue-15731; NOT MEASURED by name for anything that cannot be built). Verification also: dispatch-gates --changed --commands family, check:nul-bytes, governed test.


    Generated by Claude Code

  5. claude commented on Sep 5, 2026

    @claude
    Contributor

    Seat note (09:2xZ): the dev's out-of-scope finding (newestSourceMtime / STALE_BUILD still walks gitignored paths) was classifier-blocked at 07:40Z; the seat filed it verbatim as #15822. The dev's own delivery (push + draft PR + report) is still outstanding — nudged this tick; commit 901044cd2 is on the worktree, unpushed, no PR yet.


    Generated by Claude Code

  6. claude commented on Sep 5, 2026

    @claude
    Contributor

    os-dev-report
    {
    "issue": 15731,
    "status": "done",
    "branch": "claude/issue-15731-type-check-gates-skip-gitignored",
    "pr": "#15841",
    "head": "3d45b4290aa8e8a1be82efdeb0fd7f282bd6a3de",
    "files": [
    "scripts/check-type-check-coverage.mjs"
    ],
    "premise_still_valid": true,
    "summary": "Resumed the dead session's work; its single commit survived intact and needed no re-authoring (author and committer were already Claude noreply@anthropic.com and both footer lines were present). origin/main was merged twice by plain merge (26 then 5 commits, no conflict, and main never touched the hot file). Per the PM's binding ruling, direction 1: both gate rows -- check:type-check-coverage and check:type-check-debt are the SAME script, scripts/check-type-check-coverage.mjs without and with --re-measure, so one file covers both and one --self-test pins both -- now exclude from their walk every path the repo's own tracked .gitignore excludes. The verdict is derived from git (one ls-files --others --ignored --directory -z per run, memoised across both walk sites, 41ms for the whole repo), never a hand list. --exclude-per-directory=.gitignore rather than --exclude-standard is deliberate so a rule living only in .git/info/exclude cannot make a developer's box and CI conclude different things; tracked and force-added paths keep being judged; a git that cannot answer throws rather than returning an empty set. The rule is unchanged, only its population. Directions 2 (a tmp/ convention) and 3 (move the scratch out of the package tree) are declined in the PR body with reasons. No ledger row, no tsconfig and no UNCHECKED_SOURCE_DEBT entry was touched -- the gate's clean-tree output is byte-identical to the base commit's.",
    "verification": {
    "final_head": "3d45b4290",
    "card_reproduction_on_fixed_tree": [
    "git check-ignore verdict: .gitignore:53:tmp/\tpackages/cli/tmp/probe.ts",
    "tracked files under packages/cli/tmp: 0",
    "EXIT=0 :: node scripts/check-type-check-coverage.mjs [WITH the scratch files present, FIXED tree]",
    "EXIT=0 :: --self-test [WITH the scratch files present]",
    "clean-tree vs scratch-present output diff EXIT=0 (0 = byte-identical)",
    "diff-vs-base-clean EXIT=0 (0 = byte-identical on a clean tree) -- the ledger-untouched claim as a measurement"
    ],
    "gate_rows": [
    "EXIT=0 :: node scripts/check-type-check-coverage.mjs --self-test",
    "EXIT=0 :: pnpm check:type-check-coverage",
    "EXIT=3 :: pnpm check:type-check-debt (NOT MEASURED -- see not_measured)",
    "EXIT=0 :: pnpm check:nul-bytes",
    "self-test line: OK check:type-check-coverage --self-test -- 55 semantic case(s) + 97 observation case(s) + 45 re-measure case(s) + 28 built-closure case(s) + 19 auto-lowering case(s) + 18 exit-code case(s) hold."
    ],
    "ablation": {
    "shape": "trap-guarded, at the final head; isIgnoredPath's ignore read reverted to return false. No dist leg to preflight: this gate runs directly from source via node scripts/..., with no package build and no exports resolution between edit and run.",
    "on_disk_proof": [
    "HEAD_BLOB=4666caf866a81586c1c2d464bff5c16df3123e96",
    "PRE_BLOB =4666caf866a81586c1c2d464bff5c16df3123e96",
    "ON-DISK PROOF: deleted-anchor before=1 after=0 ; injected-marker=1",
    "MUT_BLOB =d30cbfa335664a1144cc0d3576211ed3fda368a6"
    ],
    "result": [
    "EXIT=1 :: [ABLATED] node scripts/check-type-check-coverage.mjs --self-test (7 failure(s))",
    "EXIT=1 :: [ABLATED] node scripts/check-type-check-coverage.mjs (scratch file present) -- reproduces the card verbatim: 'packages/cli/tmp: 1 non-test source file(s) here sit outside every tsc program ... UNCHECKED_SOURCE_DEBT ... is closed to new entries.'",
    "walkPackageFiles -- with the scratch directory ignored, only the tracked source survives: expected ["src/engine.ts"], got ["src/engine.ts","tmp/probe.ts"]",
    "readIgnoredPaths -- a directory a TRACKED .gitignore covers is out of the gate's subject: expected true, got false"
    ],
    "restore_proof": [
    "POST_BLOB=4666caf866a81586c1c2d464bff5c16df3123e96 (equals HEAD_BLOB)",
    "git diff HEAD EXIT=0 (0 = restored, nothing left over)",
    "residual marker count: 0",
    "git status after restore: clean"
    ]
    },
    "new_self_test_cases": "22 in four batteries (how git's listing is read, the predicate the walk applies, the walk's own output, and what git is asked of a real fixture git repo built in a temp dir). observation cases floor moves 64 -> 86. Each carries its positive control: the tracked directory beside the ignored one is still judged, tmp does not swallow the sibling tmpl, the same basename under another package is a different path, and a force-added file re-opens an ignored directory.",
    "derived_gate_families": [
    "node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack -- re-derived at the final head after the second merge and byte-identical to the set that was run",
    "Run reconciliation -- 32 derived, 32 run, 0 NOT-MEASURED, 0 UNRUN.",
    "OK dispatch-gates --ran: 32 derived famil(ies) accounted for -- 32 run, 0 NOT-MEASURED.",
    "31 of 32 EXIT=0; the single non-zero is pnpm check:type-check-debt (see not_measured). Six families first refused for want of node_modules in this fresh worktree (5 x 'PREREQUISITE NOT MET -- the dependency typescript/yaml is not installed', 1 x ERR_MODULE_NOT_FOUND for @typescript-eslint/parser); after pnpm install --offline --ignore-scripts through the lock (3.7s) all six were re-run and are green, so none is left declared."
    ],
    "landing_checks": [
    "EXIT=0 :: node scripts/pm/check-governed-merges.mjs --test scripts/check-type-check-coverage.mjs -- 'NOT governed -- ordinary queue landing applies to a PR with exactly this file list.' (re-run on the FINAL file list at the final head)",
    "EXIT=0 :: git merge-tree --write-tree origin/main HEAD (merges clean)",
    "open-PR hot-file check: all 30 open PRs read via GET /pulls/N/files, HTTP=200 each -- NONE touches scripts/check-type-check-coverage.mjs",
    "control-character self-scan on the changed file: grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' EXIT=1 (no matches)",
    "PR body read back in full: stored byte-identical to what was sent, first line 'Fixes #15731' intact, plus the platform's own appended attribution block",
    "skip-changeset applied additively via POST /issues/15841/labels (HTTP 200) and READ BACK: ['size/m','skip-changeset'] -- the label survived the size labeler's whole-set write; nothing was stripped"
    ],
    "resource_discipline": "Only the offline install went through OS_VERIFY_LOCK_SLOT=issue-15731 (VERDICT command-exit 0, held 4s, waited 1s); --status was read first and showed the lock free, queue empty. All other runs are unlocked check:* gate scripts. No process was killed by name."
    },
    "not_measured": [
    "pnpm check:type-check-debt EXIT=3 -- this is the gate's own dep-less refusal, NOT a red. Its words: 'check-type-check-coverage: PREREQUISITE NOT MET / --re-measure cannot run: 33 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk' and 'This is NOT a pass and NOT a finding: nothing was measured ... (Exit code 3, distinct from a finding's 1)'. The half this PR changes -- the structural pass -- ran and printed OK immediately above the refusal, and the --self-test in the same row is green. The numeric half needs turbo run build over the whole workspace, which is CI's run (lint.yml builds the closure before this step) and is exactly the whole-farm scan a targeted local verification declines. This PR cannot move those numbers in any case: the structural output is byte-identical on a clean tree.",
    "The dispatch-gates derivation's own three env-carried families (check-cross-package-test-inputs --union-into, check-shard-attestation --emit, check-test-completeness) print as NOT MEASURED by the tool itself -- their argv carries a variable with no value outside a CI run.",
    "The derivation also names 10 too-wide-to-place families, 39 artifact-roster families whose silence is a fact about a list rather than a clearance (7 of them keep that roster under scripts/, where my path is), and 12 families that would apply once a changeset path existed. None is a clearance; all are CI's."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "Already filed by the PM as #15822 (newestSourceMtime / STALE_BUILD) -- referenced in the PR body as deliberately not addressed here and left open. Not re-filed. No new finding surfaced during this resume."
    ],
    "cleanup": "Worktree /home/user/objectstack-15731 removed after the push and PR (rm -rf node_modules, then git worktree remove WITHOUT --force, which succeeded and so proves nothing was uncommitted). The three sibling agents' worktrees are untouched. No background process left running: the one background gate batch and its Monitor both completed and were read in-turn; nothing was killed by process name."
    }


    Generated by Claude Code

  7. claude commented on Sep 5, 2026

    @claude
    Contributor

    ACCEPT — PR #15841 (head 3d45b4290, 1 file: scripts/check-type-check-coverage.mjs +390/−34, one code commit 901044cd2 + two plain merges of main) reviewed against the GitHub three-dot diff, not the report.

    As ruled, direction 1: both gate rows are this one script (with and without --re-measure), and its walk now excludes every path the repo's tracked .gitignore files exclude — derived from ONE git ls-files --others --ignored --directory -z … --exclude-per-directory=.gitignore per run (memoised across both walk sites; --exclude-standard deliberately avoided so .git/info/exclude cannot split a dev box from CI), tracked and force-added paths still judged, a git that cannot answer throws. No ledger row, tsconfig or UNCHECKED_SOURCE_DEBT entry touched. Directions 2/3 declined in the PR body with reasons.

    Readings this seat took on a detached probe of the branch (mkdir node_modules, dep-less), copied from run output:

    • node scripts/check-type-check-coverage.mjs --self-test → ✓ check:type-check-coverage --self-test — 55 semantic case(s) + 97 observation case(s) + 45 re-measure case(s) + 28 built-closure case(s) + 19 auto-lowering case(s) + 18 exit-code case(s) hold. EXIT=0.
    • Card reproduction on the fixed tree: packages/cli/tmp/probe.ts written, git check-ignore -v → .gitignore:53:tmp/ packages/cli/tmp/probe.ts; production run → check-type-check-coverage: OK — 75/79 workspace packages type-checked (plus the root), 4 in the DEBT ledger (53 frozen raw errors, …), 1 exempt. EXIT=0 — the card's 1 non-test source file(s) here sit outside every tsc program red is gone with the scratch file present.
    • check-governed-merges.mjs --test scripts/check-type-check-coverage.mjs → NOT governed, EXIT=0; git merge-tree --write-tree origin/main <branch> → EXIT=0 against ec0a6e7bd; no other open PR touches the file (dev's 30-PR enumeration).
    • The dead session's EXIT=3 :: pnpm check:type-check-debt is the gate's own PREREQUISITE NOT MET refusal (33 unbuilt dependencies), not a red — the structural half printed OK above it; the numeric half is CI's. The ablation (7 self-test failures + the card's message verbatim with isIgnoredPath reverted, byte-identical restore) is taken from the report.

    Resume note: the dead session's commit needed no re-authoring after all (author/committer already Claude <noreply@anthropic.com>); the stop-hook's 901044c flag was on the pre-push state and is moot now that the branch is pushed. #15822 (STALE_BUILD walk) stays open as the follow-up, referenced in the PR.

    Flip + arm follow when all seven required contexts on 3d45b4290 read non-failing (waiter on). Card closes on landing (Fixes #15731).


    Generated by Claude Code

  8. claude commented on Sep 5, 2026

    @claude
    Contributor

    LANDED — PR #15841 merged as 384a5737b (11:10:51Z); this card is closed by it (Fixes #15731).

    Probe on re-fetched origin/main (384a5737b), readings copied from run output:

    • scripts/check-type-check-coverage.mjs carries function readIgnoredPaths(cwd) at :1719 (--exclude-per-directory=.gitignore at :1728) and function isIgnoredPath(index, rel) at :1789.
    • node scripts/check-type-check-coverage.mjs --self-test → ✓ check:type-check-coverage --self-test — 55 semantic case(s) + 97 observation case(s) + 45 re-measure case(s) + 28 built-closure case(s) + 19 auto-lowering … EXIT=0 (dep-less checkout).
    • Card reproduction on a detached worktree of main: packages/cli/tmp/probe.ts written, git check-ignore -v → .gitignore:55:tmp/ packages/cli/tmp/probe.ts; production run → check-type-check-coverage: OK — 75/79 workspace packages type-checked (plus the root), 4 in the DEBT ledger (53 frozen raw errors, …) EXIT=0 — the scratch file no longer reds the gate.
    • check:type-check-debt (the --re-measure half) needs a built closure: NOT MEASURED on this seat; CI's.

    Stripping pm:dispatched and the assignee in the same action. #15822 (the newestSourceMtime / STALE_BUILD walk) stays open as the follow-up.


    Generated by Claude Code

  9. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 5, 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

    bugSomething isn't workingdomain:devxDomain lane — seat card indexed by label:pm:seatfindingRecorded observation — held, not dispatchable until the findings triage round grades itpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions