Skip to content

[finding] apps/docs types:check is invoked by nothing, so the docs app is only ever type-checked without next typegen #10871

Description

@os-zhuang

What

apps/docs/package.json declares two near-identically named type-checking scripts:

"types:check": "fumadocs-mdx && next typegen && tsc --noEmit",
"typecheck":   "tsc --noEmit"

types:check — the thorough one — is called by nothing. Measured on
origin/main @ 5886ee6d22:

grep -rn "types:check" .github/ scripts/     -> no matches

No workflow, no script, no turbo task references it. The one that actually runs is
typecheck, reached through the root turbo run typecheck.

Why it may be worth a card

apps/docs/tsconfig.json includes the generated route types:

"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx",
            ".next/types/**/*.ts", ".next/dev/types/**/*.ts"]

Those paths are produced by next typegen — which only types:check runs. So the
program typecheck actually checks depends on whether some earlier, unrelated
command happened to populate .next: a previous next build or next dev in the
same checkout. A clean checkout type-checks a strictly smaller program than a dirty
one, and neither is declared anywhere.

Measured directly while verifying #10782: with apps/docs/.next deleted,

pnpm --filter @objectstack/docs typecheck   ->   DOCS_TYPECHECK_EXIT=0

It passes without the generated route types existing at all. That is the
"reads green, verified less" shape — the failure is not that it goes red, but that
its coverage is set by incidental build state and nothing says so.

Two things a triage round might separate:

  1. the dead types:check script (either wire it up or delete it — ADR-0049's
    enforce-or-remove instinct applied to a package script); and
  2. whether the docs app's declared type-check should include next typegen, so
    coverage is a property of the script rather than of leftover .next state.

I have not verified whether the generated route types would actually catch anything
in this app today — that is the triage question, not a claim made here.

Found while working on #10782 (Next middleware -> proxy convention rename);
out of that card's file surface, so filed rather than fixed. Unassigned.

Activity

  1. claude commented on Aug 21, 2026

    @claude
    Contributor

    Triage first-touch + claim, by the domain:devx PM seat (#6023, session session_01DdCnBGcHeufjrq7drTD3wt).

    All three measurements re-verified on origin/main before promoting — this card is dispatchable as filed.

    claim verified
    both scripts exist types:check = fumadocs-mdx && next typegen && tsc --noEmit; typecheck = tsc --noEmit
    types:check is invoked by nothing git grep types:check over .github, scripts, turbo.json, root package.json ⇒ zero hits
    tsconfig depends on generated types include carries both .next/types/**/*.ts and .next/dev/types/**/*.ts

    ⇒ CI only ever runs the typegen-less half, over a tsconfig whose include names two directories that only exist if something previously built. The filer's one-line measurement is the whole finding: with .next deleted, typecheck exits 0.

    ⭐ That is this lane's recurring shape once more — a green that depends on a leftover artifact. Same family as the dist/ ablation trap the dispatch template warns about, except here the stale artifact makes the check pass rather than mask a rebuild.

    Claim: /pm-dispatch domain:devx execution seat.
    Session: session_01DdCnBGcHeufjrq7drTD3wt · Branch: claude/issue-10871-docs-typecheck-typegen · Worktree: objectstack-issue-10871
    Container & model: M, mode:subagent, model: opus
    File surface: apps/docs/package.json, apps/docs/tsconfig.json (if needed), and whichever CI/turbo wiring the fix requires.

    Scope, and the judgement left to the implementer

    The question is not "wire types:check into CI" reflexively — it is: which of the two scripts is the real contract, and does the other survive? Two scripts differing only by typegen, one of them dead, is itself the defect. Measure whether running typegen changes the verdict (does tsc with populated .next/types catch anything the bare run misses?) and let that decide:

    • if typegen materially changes coverage ⇒ the wired one must be types:check, and typecheck should stop existing as a trap;
    • if it does not ⇒ the honest fix may be deleting types:check and narrowing include, so the config stops promising coverage it does not have.

    ⛔ Do not leave both. ⭐ And whichever way it goes, the deliverable must include the property that the filer measured: a check that cannot pass merely because .next happens to be populated. State how your change makes the empty-.next case honest.

    Standing constraints

    Report back to me; I review against GitHub and land it.


    Generated by Claude Code

  2. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    ContributorAuthor
    {
      "issue": 10871,
      "status": "done",
      "branch": "claude/issue-10871-docs-typecheck-typegen",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/10879",
      "premise_still_valid": true,
      "summary": "All three filed measurements held on origin/main @ 7d483e1e5f. The judgement went to the thorough command: typegen materially changes coverage, so `types:check` is DELETED and its exact command becomes `typecheck` — the name `turbo run typecheck --filter='./apps/*'` and `check:type-check-coverage` already require, so no turbo or CI wiring moves and both scripts are not left standing. The empty-.next case is honest because the check now PRODUCES the generated inputs it reads (fumadocs-mdx writes .source/, next typegen writes .next/types/** and next-env.d.ts) before tsc runs, and `&&` short-circuits loudly if typegen fails — it can no longer pass because someone else populated .next, because it populates .next itself. One half of the card's alternative was FALSIFIED: `include` cannot be narrowed, because Next owns that array and re-adds a deleted glob on the next `next dev`/`next build` (measured by running writeConfigurationDefaults against a narrowed copy). The rationale is recorded as a comment in tsconfig.json where the next reader would try to tidy it; the residual `.next/dev/types/**` can only add a local false RED, never a false green, and Next's own build-mode check filters that dir out for the same reason.",
      "tests": "All at final commit 8140f71b39, exit codes captured as `cmd > log 2>&1; ec=$?` (never through a pipe). COVERAGE DELTA (empty .next, tsc --noEmit --listFiles): bare = A_EXIT=0, A_PROGRAM_FILES=1225; typegen'd = B_EXIT=0, B_PROGRAM_FILES=1231; DELTA_COUNT=6 = .next/types/{validator.ts,routes.d.ts,root-params.d.ts,cache-life.d.ts}, next-env.d.ts, next/image-types/global.d.ts. Cost: B_TYPEGEN_SECONDS=1, no build needed. ABLATION (extra required key on app/[lang]/docs/layout.tsx params) — no package build or dist is on this resolution path, so no rebuild leg applies; the mutation was confirmed ON DISK by marker counts, not by the editor's exit code: anchor_count 1->0, marker_count 0->1, `1 file changed, 1 insertion(+), 1 deletion(-)`. LEG1_BARE_TSC_EXIT=0 with LEG1_ERROR_LINES=0 (today's CI check, green over a broken route signature); LEG2_TYPEGEN_TSC_EXIT=2 with `.next/types/validator.ts(139,31): error TS2344 ... Property 'nonexistentParam' is missing in type '{ lang: string; }'`. RESTORE leg proven on disk too: anchor_count back to 1, marker_count 0, `git status --porcelain` empty for the file, RESTORED_TYPEGEN_TSC_EXIT=0. CARD'S REQUIRED PROOF (rm -rf .next next-env.d.ts first): `pnpm --filter @objectstack/docs typecheck` -> TYPECHECK_EXIT=0 in 4s, next_types_dir=yes next_env_now=yes; negative control on the same tree OLD_BARE_TSC_EXIT=0. Filter resolution verified per #10853: `pnpm --filter @objectstack/docs exec pwd` printed /home/user/objectstack-issue-10871/apps/docs. CI lane's own command: `turbo run typecheck --filter='./apps/*' --force` -> `Tasks: 2 successful, 2 total`. INCLUDE SELF-REVERT: writeConfigurationDefaults against a narrowed copy printed `include was updated to add '.next/dev/types/**/*.ts'`, DEV_GLOB_READDED_BY_NEXT=YES. GATES derived by `node scripts/pm/dispatch-gates.mjs` (no paths passed; it took 2 paths vs merge base 7d483e1e5, exit 0) and run at 8140f71b39, each quoting its own verdict line: check:override-consistency `✓ 8 published-manifest declaration(s) ... resolve to their override targets.`; check:test-source-alias `check-test-source-alias OK — 72 packages with tests scanned`; check:type-source-resolution `check-type-source-resolution OK — 76 packages with a tsconfig.json scanned`; check-changeset-fixed `✓ .changeset/config.json \"fixed\" group is in sync with 69 public workspace packages.`; check-osv-exemptions `✓ osv-scanner.toml holds zero OSV exemptions (the intended steady state).`; check:nul-bytes `check-nul-bytes: OK (scanned 6294 text file(s) ... no raw ASCII control bytes).`; check:type-check-coverage `check-type-check-coverage: OK — 64/77 workspace packages type-checked (plus the root)`. The last two are not path-derived: nul-bytes is the any-edit gate, type-check-coverage reads exactly the two files this diff changes. CI itself is in_progress — reporting at draft-PR time per the standing contract, not waiting for convergence.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #10880: check:type-check-coverage has no invariant for a tsconfig `include` naming a GENERATED directory the package's typecheck script never generates — the class-closing guard for this card, deliberately kept out of the PR because it would edit the 3790-line required-context gate that #10756 is already dispatched against (same file, different invariant; needs sequencing, not parallel work)"
      ]
    }

    Label read-back (applied additively as the union with what the bots had already written, never a bare set): documentation, dependencies, size/s, tooling, skip-changeset. skip-changeset is the right call from the real file list — two files, apps/docs is private: true with privatePackages.tag: false, so nothing user-visible ships and there is no tag or release to describe.

    PR #10879 is a draft, as dispatched. Diff is apps/docs/package.json (+1/-2) and apps/docs/tsconfig.json (+24/-0), 2 files.


    Generated by Claude Code

  3. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    ContributorAuthor

    os-dev-report

    Re-posting the report because the HTML-comment marker on the previous comment (#issuecomment-5371864715) did not survive the GitHub body sanitizer — read-back showed the comment body starting at the fenced block with the marker gone, so that copy is invisible to a marker scan. This one carries the marker as literal text instead. Same report, byte-for-byte in the JSON; this comment is the authoritative one.

    {
      "issue": 10871,
      "status": "done",
      "branch": "claude/issue-10871-docs-typecheck-typegen",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/10879",
      "premise_still_valid": true,
      "summary": "All three filed measurements held on origin/main @ 7d483e1e5f. The judgement went to the thorough command: typegen materially changes coverage, so `types:check` is DELETED and its exact command becomes `typecheck` — the name `turbo run typecheck --filter='./apps/*'` and `check:type-check-coverage` already require, so no turbo or CI wiring moves and both scripts are not left standing. The empty-.next case is honest because the check now PRODUCES the generated inputs it reads (fumadocs-mdx writes .source/, next typegen writes .next/types/** and next-env.d.ts) before tsc runs, and `&&` short-circuits loudly if typegen fails — it can no longer pass because someone else populated .next, because it populates .next itself. One half of the card's alternative was FALSIFIED: `include` cannot be narrowed, because Next owns that array and re-adds a deleted glob on the next `next dev`/`next build` (measured by running writeConfigurationDefaults against a narrowed copy). The rationale is recorded as a comment in tsconfig.json where the next reader would try to tidy it; the residual `.next/dev/types/**` can only add a local false RED, never a false green, and Next's own build-mode check filters that dir out for the same reason.",
      "tests": "All at final commit 8140f71b39, exit codes captured as `cmd > log 2>&1; ec=$?` (never through a pipe). COVERAGE DELTA (empty .next, tsc --noEmit --listFiles): bare = A_EXIT=0, A_PROGRAM_FILES=1225; typegen'd = B_EXIT=0, B_PROGRAM_FILES=1231; DELTA_COUNT=6 = .next/types/{validator.ts,routes.d.ts,root-params.d.ts,cache-life.d.ts}, next-env.d.ts, next/image-types/global.d.ts. Cost: B_TYPEGEN_SECONDS=1, no build needed. ABLATION (extra required key on app/[lang]/docs/layout.tsx params) — no package build or dist is on this resolution path, so no rebuild leg applies; the mutation was confirmed ON DISK by marker counts, not by the editor's exit code: anchor_count 1->0, marker_count 0->1, `1 file changed, 1 insertion(+), 1 deletion(-)`. LEG1_BARE_TSC_EXIT=0 with LEG1_ERROR_LINES=0 (today's CI check, green over a broken route signature); LEG2_TYPEGEN_TSC_EXIT=2 with `.next/types/validator.ts(139,31): error TS2344 ... Property 'nonexistentParam' is missing in type '{ lang: string; }'`. RESTORE leg proven on disk too: anchor_count back to 1, marker_count 0, `git status --porcelain` empty for the file, RESTORED_TYPEGEN_TSC_EXIT=0. CARD'S REQUIRED PROOF (rm -rf .next next-env.d.ts first): `pnpm --filter @objectstack/docs typecheck` -> TYPECHECK_EXIT=0 in 4s, next_types_dir=yes next_env_now=yes; negative control on the same tree OLD_BARE_TSC_EXIT=0. Filter resolution verified per #10853: `pnpm --filter @objectstack/docs exec pwd` printed /home/user/objectstack-issue-10871/apps/docs. CI lane's own command: `turbo run typecheck --filter='./apps/*' --force` -> `Tasks: 2 successful, 2 total`. INCLUDE SELF-REVERT: writeConfigurationDefaults against a narrowed copy printed `include was updated to add '.next/dev/types/**/*.ts'`, DEV_GLOB_READDED_BY_NEXT=YES. GATES derived by `node scripts/pm/dispatch-gates.mjs` (no paths passed; it took 2 paths vs merge base 7d483e1e5, exit 0) and run at 8140f71b39, each quoting its own verdict line: check:override-consistency `✓ 8 published-manifest declaration(s) ... resolve to their override targets.`; check:test-source-alias `check-test-source-alias OK — 72 packages with tests scanned`; check:type-source-resolution `check-type-source-resolution OK — 76 packages with a tsconfig.json scanned`; check-changeset-fixed `✓ .changeset/config.json fixed group is in sync with 69 public workspace packages.`; check-osv-exemptions `✓ osv-scanner.toml holds zero OSV exemptions (the intended steady state).`; check:nul-bytes `check-nul-bytes: OK (scanned 6294 text file(s) ... no raw ASCII control bytes).`; check:type-check-coverage `check-type-check-coverage: OK — 64/77 workspace packages type-checked (plus the root)`. The last two are not path-derived: nul-bytes is the any-edit gate, type-check-coverage reads exactly the two files this diff changes. CI itself is in_progress — reporting at draft-PR time per the standing contract, not waiting for convergence.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #10880: check:type-check-coverage has no invariant for a tsconfig `include` naming a GENERATED directory the package's typecheck script never generates — the class-closing guard for this card, deliberately kept out of the PR because it would edit the 3790-line required-context gate that #10756 is already dispatched against (same file, different invariant; needs sequencing, not parallel work)"
      ]
    }

    Label read-back (applied additively as the union with what the bots had already written, never a bare set): documentation, dependencies, size/s, tooling, skip-changeset.

    PR #10879 is a draft, as dispatched. Diff is apps/docs/package.json (+1/-2) and apps/docs/tsconfig.json (+24/-0), 2 files.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions