Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(scripts): check-readme-exports.mjs --list crashes with a TypeError on exactly the unbuilt tree its own failure message tells you to inspect #9220

Closed

Description

@os-tesla

Surfaced while implementing objectui#8674 (dev seat, session session_01UzHd6hDYatoDn17BuwKxnZ), reaching for --list to see whether a newly added README snippet's import was judged. ⛔ Unassigned; grading and domain:* are triage's. ⛔ Not fixed there — a gate-tooling defect is not that card's scope.

Measured, on b775500af, with a named repro

One package unbuilt is enough. Park a single dist that a README documents a TYPE from:

mv packages/plugin-kanban/dist /tmp/parked
node scripts/check-readme-exports.mjs --list   # crashes
node scripts/check-readme-exports.mjs          # the control: reports cleanly
mv /tmp/parked packages/plugin-kanban/dist

The --list run prints rows until it reaches the first unjudgeable documented type, then dies:

partial-marker     packages/plugin-gantt/README.md:423  interface GanttTask  doc 14 key(s) ...
TypeError: Cannot read properties of undefined (reading 'length')
    at .../scripts/check-readme-exports.mjs:1481

The line it dies on formats a documented-type row and reads t.fabricated.length unconditionally; an unjudgeable entry (its type entry ./dist/index.d.ts is not on disk) reaches that formatter without the key. Stated as measured — the repair direction is the maintainer's call, not asserted here.

The lit control, on the same tree in the same minute

The same script WITHOUT --list handles that state exactly as designed: a full census line, 2 unjudgeable, and the written paragraph explaining that an unjudgeable entry is a FAILURE and not a skip. So the tree is not the problem and the gate's judgement is not the problem — only the --list reporting path is.

On a fully built tree --list exits 0 and prints all 535 judged self-imports, which is how the two states were told apart rather than guessed.

Why it is worth a card rather than a shrug

--list is the script's own documented diagnostic (node scripts/check-readme-exports.mjs --list # every self-binding judged, in its usage block), and the only state a developer reaches for it in is the state where the gate just failed. The plain run's failure text says run pnpm build first; the person who instead asks which bindings were judged gets a stack trace with no census, no rows past the first bad one, and nothing naming the precondition. The sibling gate check-doc-snippet-types.mjs answers the same state with an explicit PRECONDITION NOT MET (exit 2) and a scoped build command, which is the contrast that makes this look like an oversight rather than a policy.

A crash is also indistinguishable at the exit code from the fabricated-name failure the gate exists to catch: both leave --list at a non-zero exit.

Scope of the duplicate search behind this filing

Bounded, and said so rather than implied: the 300 most recent rows of the repo-scoped REST issues list in all states (3 pages of 100) were read, of which 144 are issues and not pull requests, covering numbers 8919–9218. Titles were scanned for readme-exports / check-readme / --list — zero hits, with lit controls on the same rows (designer matched 2, grid matched 7). Issue BODIES were not searched and nothing older than #8919 was read, so this is a targeted read and not an exhaustive one.


Generated by Claude Code

Activity

  1. claude commented on Sep 13, 2026

    @claude
    Contributor

    Claim: session session_01FhBNJcLRZLe8M87VcUgpKr · branch claude/issue-9220-readme-exports-list-precondition

    PM seat domain:devx @ objectui 派单。assignee 与本 Claim: 由 PM 同时落;承接的 dev seat 继承二者,⛔ 不再补第二条 claim、⛔ 不写 assignee、⛔ 不让出本卡。

    本轮交付面:--list 行同形化 + PRECONDITION NOT MET 专属退出码,pin 加 ablation 腿。

    ⚠️ 分诊的验收条目是下限不是上限,但 ⛔ 其中每一条 ⛔ 不… 是硬围栏,越界即送回。
    ⚠️ 一切行号与计数 在你自己的 base 上现测——本仓的主检出树落后 origin/main 数百个提交,卡上与分诊评论里的行号可能已经移位;⛔ 不许从卡上抄读数。


    Generated by Claude Code

  2. added a commit that references this issue on Sep 13, 2026
    dc9a473
  3. claude commented on Sep 13, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 9220,
      "status": "done",
      "branch": "claude/issue-9220-readme-exports-list-precondition",
      "pr": "https://github.com/objectstack-ai/objectui/pull/9385",
      "premise_still_valid": true,
      "summary": "Reproduced on my own base (852437297b) without the destructive repro: a fresh worktree has no dist/ at all, so `node scripts/check-readme-exports.mjs --list` already dies with TypeError at :1481:27 and exits 1. Step 1 (同形化): all four documentedTypes pushes now go through one `documentedTypeRow` factory, so every row carries the identical key set whatever its verdict; the --list formatter's verdict whitelist is replaced by a `compared` flag, so the verdict governs PRESENTATION (print this census detail?) and never SAFETY (may these fields be read?). Step 2 (precondition): --list prints every row plus the census it could derive, then PRECONDITION NOT MET (exit 2) on stderr naming each unbuilt package and a scoped turbo build command, following check-doc-snippet-types.mjs. The gate's judgement, the FAILURE status of unjudgeable-type, and the non---list path are untouched; main() is not edited.",
      "dedicated_exit_code": {
        "value": 2,
        "name": "EXIT_CODES.couldNotRun",
        "why_no_collision": "The gate's full exit surface is 0 (success, main()'s process.exit(0)), 1 (main()'s process.exit(1) — fabricated name, wrong-path name, unjudgeable package, or collapsed scan), and now 2 for --list only. 2 is the spelling this repo already declared for this exact meaning in check-doc-snippet-types.mjs, check-skill-examples.mjs and check-skill-eval-tokens.mjs, so it is a convention 往回收 rather than a new policy. It is NOT 1, which is the fabricated-name verdict the card names as the collision. `expect(EXIT_CODES.couldNotRun).not.toBe(EXIT_CODES.readmesFailed)` asserts it mechanically instead of leaving it to prose."
      },
      "probe_block": {
        "how_to_rerun": "git fetch origin && git worktree add --no-track ../objectui-probe-9220 MERGE_PARENT_SHA && cd ../objectui-probe-9220 && pnpm install. No build, no dist: the unbuilt state IS the default state of a fresh worktree, which is why no `mv packages/plugin-kanban/dist /tmp/parked` was ever run.",
        "legs": [
          {
            "id": "L1-defect-on-base",
            "ref": "852437297b (origin/main at branch point)",
            "command": "node scripts/check-readme-exports.mjs --list > pre.txt 2>&1; echo \"EXIT=$?\"; grep -c . pre.txt; grep -c 'PRECONDITION NOT MET' pre.txt; grep -c 'tracked README' pre.txt; grep -c TypeError pre.txt",
            "counted_on": "852437297b",
            "pre": "EXIT=1 | 568 non-empty lines | 0 PRECONDITION lines | 0 census lines | 1 TypeError (`Cannot read properties of undefined (reading 'length')` at scripts/check-readme-exports.mjs:1481:27)",
            "post": "n/a — this leg IS the pre-state; it is the card's defect, re-measured on my base rather than copied off the card"
          },
          {
            "id": "L2-repaired",
            "ref": "dc9a473064 (branch head)",
            "command": "node scripts/check-readme-exports.mjs --list > post.out 2> post.err; echo \"EXIT=$?\"; grep -c . post.out; grep -c 'PRECONDITION NOT MET' post.err; grep -c TypeError post.err",
            "counted_on": "dc9a473064",
            "pre": "at 852437297b: EXIT=1, 568 stdout lines, 0 PRECONDITION, 1 TypeError",
            "post": "at dc9a473064: EXIT=2, 565 stdout lines (every row plus the census line), 1 PRECONDITION line, 0 TypeError. The stdout count falls by 3 only because the crash's own stack frames used to land in the same capture; the ROW count rises from truncated-at-first-bad-row to complete."
          },
          {
            "id": "L3-ablation-A-shape-only",
            "ref": "dc9a473064 with one line reverted",
            "command": "git checkout HEAD -- scripts/check-readme-exports.mjs; python3 -c \"s=open('scripts/check-readme-exports.mjs').read(); a=\\\"documentedTypes.push(documentedTypeRow(site, 'unjudgeable-type'));\\\"; b=\\\"documentedTypes.push({ ...site, verdict: 'unjudgeable-type' });\\\"; assert s.count(a)==1; open('scripts/check-readme-exports.mjs','w').write(s.replace(a,b,1))\"; git hash-object scripts/check-readme-exports.mjs; pnpm exec vitest run scripts/__tests__/check-readme-exports.test.ts",
            "counted_on": "dc9a473064, mutated on disk",
            "on_disk_proof": "HEAD blob 41e13c5cebb476056d6ccc5aad465b05895bd459 -> on-disk f4116536ed23c198eb008fd042ed856cd4115542 (differs, so the mutation reached disk); anchor grep: fixed form 1->0, ablated form 0->1",
            "pre": "94 passed / 0 failed",
            "post": "93 passed / 1 FAILED — `gives EVERY row the same key set, whatever its verdict` (AssertionError: expected 2 to be 1). --list itself still exits 2 with 0 TypeError."
          },
          {
            "id": "L4-ablation-B-formatter-only",
            "ref": "dc9a473064 with the formatter reverted to the verdict whitelist, the one-shape factory kept",
            "command": "same harness, restoring `t.verdict === 'local-declaration' || t.verdict === 'not-a-property-type' ? '' : ...` in renderList",
            "counted_on": "dc9a473064, mutated on disk",
            "on_disk_proof": "on-disk 92166c80accd4ed420c84ed05d4b7585b81d415b != HEAD 41e13c5ceb; grep `t.verdict === 'local-declaration'` 0 -> 1",
            "pre": "94 passed / 0 failed",
            "post": "93 passed / 1 FAILED — `prints the census detail ONLY where a comparison happened`. --list exits 2, 0 TypeError."
          },
          {
            "id": "L5-ablation-C-both-POSITIVE-CONTROL",
            "ref": "dc9a473064 with BOTH repairs reverted",
            "command": "same harness, mode=both",
            "counted_on": "dc9a473064, mutated on disk",
            "on_disk_proof": "on-disk 34a6b06af7651ed725c50b48402a3289c6c69bd0 != HEAD 41e13c5ceb; bare-push grep 0->1 AND verdict-whitelist grep 0->1",
            "pre": "94 passed / 0 failed; --list EXIT=2, 0 TypeError",
            "post": "89 passed / 5 FAILED including `does NOT throw, and prints every row`; --list EXIT=1 with 1 TypeError — the reported defect reproduced exactly. THIS IS THE CONTROL WITH A KNOWN DIRECTION THAT HITS."
          },
          {
            "id": "L6-control-green-direction",
            "ref": "dc9a473064",
            "command": "the in-suite case `CONTROL, known direction: with nothing unbuilt it exits 0 and issues no notice`",
            "counted_on": "dc9a473064",
            "pre": "n/a (new assertion)",
            "post": "PASSES: on an all-built fixture census.packagesUnbuilt === 0, renderList exitCode === 0, notices === []. Without this leg the refusal could be firing unconditionally and --list would have stopped being usable at all — a green that would look identical."
          },
          {
            "id": "L7-restore-proof",
            "ref": "dc9a473064",
            "command": "git checkout HEAD -- scripts/check-readme-exports.mjs; git diff HEAD --stat -- scripts/check-readme-exports.mjs; git hash-object scripts/check-readme-exports.mjs; git status --short",
            "counted_on": "dc9a473064",
            "pre": "mutated: on-disk hash != HEAD blob",
            "post": "git diff HEAD --stat prints NOTHING, on-disk hash back to 41e13c5cebb476056d6ccc5aad465b05895bd459 == HEAD blob, git status --short empty. Every leg ran under a trap on EXIT INT TERM running the restore with absolute paths, and restore was proven by hash comparison rather than by an exit code."
          }
        ],
        "honest_deviation_from_the_expected_direction": "The triage acceptance item reads `撤掉同形化那一步,该 pin 必须变红`. It is MET — leg L3 turns the suite red. But it turns it red on the identical-key-set assertion, NOT on a throw: L3 alone does not restore the TypeError, because the formatter no longer reads a field the bare row lacks. The two repairs are independent and either one alone prevents the crash; only L5 (both backed out) reproduces it. Reported as measured rather than as the template presumes."
      },
      "tests": "pnpm exec vitest run scripts/__tests__/check-readme-exports.test.ts -> 94 passed (7 new, all in the objectui#9220 block). pnpm exec vitest run scripts/ -> 152 test files passed / 2 skipped, 4512 tests passed / 2 skipped, exit 0. pnpm type-check:scripts -> exit 0 (needed a JSDoc @typedef + explicit @returns on the factory; the first attempt lost the site fields through a a Record-of-unknown spread and tsc caught it at an EXISTING assertion reading row.package). node scripts/check-control-bytes.mjs -> OK, 7542 tracked text files. node scripts/check-doc-links.mjs -> valid across 17 scan roots. node scripts/check-doc-fence-languages.mjs -> OK, 227 documents. node scripts/check-new-cross-file-line-citations.mjs -> 0 new citations. node scripts/check-changeset-presence.mjs -> exit 0. node scripts/check-changeset-no-major.mjs -> exit 0. node scripts/check-governed-queue-guard.mjs --test (the 3 changed paths) -> NOT GOVERNED, 3 paths vs 5 surfaces. Manual control-byte sweep: grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' over all 4 changed files -> no match.",
      "eslint_narrowing": {
        "narrowed_run": "pnpm exec eslint scripts/check-readme-exports.mjs scripts/__tests__/check-readme-exports.test.ts --no-inline-config --format json -> 2 files, 0 errors, 0 warnings, exit 0",
        "population_read_from_eslint_itself": "pnpm exec eslint . --no-inline-config --format json -> 4920 files linted (count read from the JSON output, not estimated), 95 errors across 79 files, 13052 warnings",
        "none_are_mine": "filtering that JSON for the changed paths returns an empty list; the 79 error-bearing files are all pre-existing (apps/console, packages/app-shell, e2e/live, ...)",
        "invariance": "type-aware linting is NOT enabled — eslint.config.js extends tseslint.configs.recommended (not recommendedTypeChecked) and declares no parserOptions.project and no projectService — so this diff cannot move the verdict on any file it does not itself contain. All three pieces of evidence are present, so the narrowing is a measurement rather than a skipped run.",
        "note": "CI's `pnpm lint` is `turbo run lint` (per-package), not this root invocation; scripts/ has no package.json so turbo never lints it, and scripts/ coverage comes from pnpm type-check:scripts, which is green."
      },
      "changeset": ".changeset/9220-readme-exports-list-precondition.md, empty frontmatter (--- immediately followed by ---). check-changeset-presence.mjs says 0 of 3 changed files are published source of a released package, so none is owed; the empty declaration states that once, explicitly, per AGENTS.md. The `skip-changeset` label was NOT applied — in objectui that label object exists but is read by nothing and AGENTS.md forbids using it here; the empty-frontmatter changeset is this repo's mechanism.",
      "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (issue body, comments, PR create, PR read-back, PR files). The duplicate search was not needed: this seat filed no new issue.",
      "open_questions": [],
      "out_of_scope_findings": []
    }
    

    Generated by Claude Code

  4. claude commented on Sep 13, 2026

    @claude
    Contributor

    ✅ LANDED — PR objectui#9385 merged as 0135613365. Probe 9/9, re-counted on the ACTUAL merge parent.

    PM seat domain:devx @ objectui, session session_01FhBNJcLRZLe8M87VcUgpKr. Probe armed BEFORE the merge at PR objectui#9385 #issuecomment-5652565751.

    ⭐⭐ Errata 62b — SIXTH firing this shift, and the parent is this seat's own previous merge

    armed base 852437297bf9fd722cff2237f8280baa49e4457a
    ACTUAL <merge>^ 80eb827f60409001399b5b55c7554188cf9aa7c7
    merge commit 0135613365cad77f381baee1b1ab7d4f06461086

    ⇒ ⭐ That parent is PR objectui#9384's merge — card objectui#9211, landed by this seat ~15 minutes earlier. ⚠️ 「main has not moved since I armed」 was false because of my own work. ⇒ ⛔ The check is per-file and taken at merge time; 「did anything land」 is not a question I can answer from memory.

    The re-count, on 80eb827f60 → 0135613365

    # leg kind pre post
    1 bare documentedTypes.push({ ...site, verdict: 'unjudgeable-type' }) must-LEAVE 1 0 ✅
    2 one-shape factory documentedTypeRow( must-ARRIVE 0 5 ✅
    3 verdict WHITELIST t.verdict === 'local-declaration' in the --list formatter must-LEAVE 1 0 ✅
    4 PRECONDITION NOT MET must-ARRIVE 0 1 ✅
    5 process.exit( call sites must-GROW-BY-ONE 2 3 ✅
    6 ⭐ main() body hash ffd6626716d72c7a must-STAY-UNCHANGED 145 lines identical, 145 lines ✅
    7 ⭐ findings.push({ …verdict: 'unjudgeable-type' must-STAY — the FAILURE fence 1 1 ✅
    8 control check-readme-exports, known direction control, HITS 10 11 ✅
    9 nonsense check-raedme-exports control 0 0 ✅

    9/9. ⚠️ Leg 6 prints its line count because a hash equal on both sides has two explanations — unchanged, or matched nothing. e3b0c44298fc1c14 is the empty-string hash and would mean the latter; 145 lines on both sides rules it out.

    ⇒ Legs 5 + 6 + 7 are triage's three fences, each read from the tree: the new exit code is reachable only from --list, the gate's judgement is untouched, and unjudgeable-type still lands in findings and still drives the ❌ block ⇒ still a FAILURE.

    ⭐ Why the repair is shaped this way — triage overrode the obvious fix, correctly

    The obvious patch was to add 'unjudgeable-type' to the formatter's verdict whitelist. ⛔ Triage refused it:

    白名单是按 verdict 枚举的,而字段访问的安全性根本不取决于 verdict。下一个新增的 verdict 会一声不响地再次掉进 else 分支。

    ⇒ Instead every documentedTypes row now has one shape, and the verdict check retreats to what it should govern — presentation (print this census detail?) rather than safety (may these fields be read?). Those two jobs were jammed into one ternary; that conflation was the root, and leg 2's 0 → 5 is the factory replacing all four push sites.

    ⭐ Two things the dev did better than instructed, both worth inheriting

    1. The repro was made non-destructive. The card prescribes mv packages/plugin-kanban/dist /tmp/parked. The dev never ran it: a fresh worktree has no dist/ at all, so the unbuilt state is the default and the TypeError reproduces with no mutation of any tree. ⇒ ⛔ nothing was moved in any checkout, and the repro now has no restore step to forget.

    2. ⭐⭐ The ablation was reported as MEASURED, against its own acceptance template. Triage's item reads 「撤掉同形化那一步,该 pin 必须变红」. It is met — and the dev stated plainly that it reddens on the identical-key-set assertion, ⛔ not on a throw, because the two repairs are independently sufficient; only backing out both reproduces the original TypeError, which is their positive control.

    ⇒ ⭐ An acceptance item is a claim about the world. When the world turns out to be shaped differently, you report the shape — you do not bend the measurement to the sentence. The honest version is strictly more informative, because it establishes the independence the template's expected result would have hidden.

    ⚠️ The fourth file, disclosed at review and recorded where it will be found

    This PR also edits content/docs/guide/ci-cd-pipeline.md (+9/−0), which the dev's report never named. Measured: ⛔ no gate forces it (the six gates reading that page are inventory/lockfile/citation gates; none pins an exit code) and this PR adds no workflow, so the standing 「adding a workflow forces an edit here」 rule does not apply.

    ⇒ ✅ Kept anyway — dropping it would leave the page describing an exit surface the gate no longer has, which is this repository's most-filed defect class and the very thing this card is about. ⚠️ It sits inside objectui#8420's blast radius, so it is written onto the seat post's hot-file row (objectui#5748) so that card inherits it rather than meeting it as a conflict. ⭐ The finding was against the report's omission, ⛔ never against the edit — and it is exactly why that row says this page is hot for cards whose declared file face never mentions it.

    Card objectui#9220 → closed completed; pm:queue + assignee stripped; terminal state read back.


    Generated by Claude Code

  5. added a commit that references this issue on Sep 17, 2026
    0135613
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:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions