Repository navigation
check-node-esm-load.mjs's readTsconfig() destroys any tsconfig whose JSON strings contain /*, so effectiveNoEmit() silently answers from its catch block for packages/auth and packages/fields #5367
Description
Activity
Triage: lands in
scripts/check-node-esm-load.mjs(readTsconfig()string-unaware strip); promotedfinding→pm:queue, type Bug. The gate is currently ratchetingpackages/fieldson a verdict computed from a catch block — that is a gate-machinery defect, not polish. Fix shape is named (JSONC/TS parser + round-trip-every-tsconfig assertion); the one embedded question (whetherfieldsstays in the specifier leg once parsed) is small enough to surface in the PR.Fold ruling: FOLD with #5382 — see the five-gate answer recorded on #5382; this card is the second member, claim goes on the chain head.
Generated by Claude Code
Claim: PM loop round 1
Session:session_014rYHTXpNpNFqsZ6rbtsoDf
Branch:claude/issue-5367-readtsconfig-jsonc
Worktree:objectui-issue-5367
Domain:domain:devx(objectui carries nodomain:*labels — sister-repo normal shape; lane seat is objectstack#10602,domain:devx @ objectui)
File surface:scripts/check-node-esm-load.mjs+ its test file (stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus
Clause-②: no
Serial constraints cleared: chain head #5382 has landed (it shares this exact file) — verified onorigin/main60d452e; no open PR touchesscripts/. Open PRs repo-wide: #5472 (draft, awaiting maintainer merge), #5400 (release), #4958 / #4945 (dependabot). None intersect.Premise re-verified at claim time, on
origin/main60d452eThe triage ruling on this card said FOLD with #5382. #5382 landed without this card folded in — the prior seat wrote a serial dispatch order and publicly corrected itself on #5382. So this is now a standalone remainder, not a fold member, and the fold ruling is moot rather than unexecuted.
Because #5382 rewrote parts of this same file, the premise was re-measured rather than assumed:
readTsconfigis still the three-regex strip at:124-130.effectiveNoEmitstill answers from itscatchat:145-150.parseConfigFileTextToJsoncount: 0. Positive controleffectiveNoEmit: 3 — so the zero is a real zero, not a bad path.- ⭐ check-node-esm-load's comment mask is not line-comment aware, so a
/*inside a//line hides live extensionless specifiers from the gate #5382's own landed code documents this card as its remainder, at:90-91: "readTsconfigbelow is the same class, still open as objectui#5367."
⚠️ Note the--tierreading normally quoted on this line is not available in this repo:scripts/pm/dispatch-gates.mjsis objectstack-resident and objectui has noscripts/pm/. The tier above is this seat's explicit per-card decision, stated as such rather than attributed to a tool reading that does not exist here.
Generated by Claude Code
Claim released — dispatched dev produced nothing, card returned to
pm:queueReleasing my own claim (
5365444185, sessionsession_014rYHTXpNpNFqsZ6rbtsoDf). ⛔ Not a reclaim of anyone else's work.Evidence, 2026-08-21T10:29Z — dispatched 05:07Z, so ~5.4h elapsed:
reading value claude/issue-5367-readtsconfig-jsoncexists, 0 commits of its own (still at 60d452ee, i.e.mainas of dispatch)PR none report comment on this card none — comment count was 2 (triage + my claim) ⚠️ The branch reading alone would not justify this: a dev that correctly stops has zero commits too, and the two are identical undergit. This lane has already burned a full round on exactly that misread. So the deciding reading is the comment count — no third comment exists in any form, which also rules out the sanitizer-ate-the-marker case (that hides a marker, not a whole comment). No report of any kind arrived. That is death, not a considered stop.mainhas moved60d452ee→7d0143c3since dispatch, so the branch is also stale. Delete it or reset it from currentmain; ⛔ do not branch on top of it.Carried forward so the next dispatch does not re-derive it
Verified on
origin/main60d452ee— re-check against currentmain7d0143c3before relying on any of it, but this is where to start:readTsconfigis still the three-regex strip at:124-130;effectiveNoEmitstill answers from itscatchat:145-150.parseConfigFileTextToJsoncount in that file: 0. Positive controleffectiveNoEmit: 3 — the zero is real, not a bad path.- ⭐ check-node-esm-load's comment mask is not line-comment aware, so a
/*inside a//line hides live extensionless specifiers from the gate #5382's landed code names this card as its own remainder, at:90-91: "readTsconfigbelow is the same class, still open as objectui#5367." - ⇒ The triage FOLD-with-check-node-esm-load's comment mask is not line-comment aware, so a
/*inside a//line hides live extensionless specifiers from the gate #5382 ruling is moot, not unexecuted. check-node-esm-load's comment mask is not line-comment aware, so a/*inside a//line hides live extensionless specifiers from the gate #5382 landed alone. Treat this as a standalone remainder; ⛔ do not hunt for a fold partner. - The card's claim that
ts.parseConfigFileTextToJsonadds no new dependency edge (because check-node-esm-load's comment mask is not line-comment aware, so a/*inside a//line hides live extensionless specifiers from the gate #5382 madetypescriptreachable from the shared scanner) was not independently verified by me — verify before relying on it. - Note this file imports from
./check-phantom-dependencies.mjs; shared-helper changes reach further than this gate. - The embedded question stays open and must not be decided silently: parsed correctly,
effectiveNoEmitreturnstrueforpackages/fields, dropping it out of the specifier leg. Hold it in scope explicitly or report the scope change as a named consequence — a silent scope change is the failure this card exists to end.
No gates were run and no code was written, so nothing here is a review verdict.
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 huangyiirene commented
on Aug 21, 2026 CollaboratorMore actionsTriage (three-stream backfill per the 2026-08-21
domain:uiruling): addeddomain:devx—check-node-esm-load.mjsis gate tooling, the engineering face.
Generated by Claude Code
Claim: PM loop round 3
Session:session_01PQ3NihCHE9LUtHoGxo6A9f
Branch:claude/issue-5367-readtsconfig-jsonc-r3
Worktree:objectui-issue-5367-r3
Domain:domain:devx(@ objectui, seat #5748)
File surface:scripts/check-node-esm-load.mjs+ its test file underscripts/__tests__/(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus.⚠️ scripts/pm/dispatch-gates.mjsis objectstack-resident and objectui has noscripts/pm/— this tier is this seat's explicit decision, ⛔ not a tool reading.
Clause-②: no — gate machinery. The gate's verdict on one package may change (see the embedded question), but no contract acceptance behaviour and no public surface moves.
Serial constraints cleared: chain head #5382 landed and its code names this card as its own remainder. No open PR touchesscripts/— checked by head: #5925 (app-shell preview comment), #5922 (layout docs), #5843 (governeddocs/adr), #5774 (.github/workflows/+ci-cd-pipeline.md), #5400 (release), #4958 (dependabot). Concurrent in this batch: #3840 (apps/consoleprobe) and #5458 (eslint-rules/+ app-shell views) — neither touchesscripts/.⛔ Two dead branches precede you. Do not build on either.
This card has been claimed twice and delivered zero times, both times by devs that died silently:
claude/issue-5367-readtsconfig-jsonc— exists on the remote, 0 commits of its own, stale at60d452ee. ⛔ Do not branch from it, do not reset it, do not reuse its name. Branch from currentmainunder the new name above.- The prior claim (
5365444185) was released by its own session with evidence. Nothing is owed to it.
The reason both are recorded rather than swept: a dev that correctly stops also has zero commits, and the two are indistinguishable under
git. The deciding reading was the absent report comment, not the empty branch. Same standard applies to you — if you stop, say so in a report; silence will be read as death.Premise re-verified on
origin/main@2074a59, all four readings⛔ Not inherited. Measured now:
reading result readTsconfigis still the regex stripyes — :124, three.replace()calls over raw text, thenJSON.parseparseConfigFileTextToJsonin that file0 positive control on the same file ( effectiveNoEmit)3 — so the zero above is a real zero, ⛔ not a bad path the two poisoned configs still carry /*inside JSON stringsyes — packages/auth/tsconfig.json:9"@/*": ["src/*"]and:37"**/*.test.ts";packages/fields/tsconfig.json:7(packages/*/srcinside a//comment) and:33And
#5382's landed code still points here, at:91and:404: "is a SEPARATE live instance of the same class — objectui#5367".The triage FOLD-with-#5382 ruling is moot, not unexecuted — #5382 landed alone and named this as its remainder. ⛔ Do not hunt for a fold partner.
The embedded question — ⛔ must not be answered silently
Parsed correctly,
effectiveNoEmitreturns true forpackages/fields, which drops it out of the specifier leg of the ratchet. Today it is inside that leg only because the parse throws and thecatchgrades it "emitting".So the fix necessarily changes a gate's scope. Both of these are acceptable; exactly one thing is not:
- keep
fieldsin scope deliberately, and say in the PR body why and how; - let it drop out, and report that as a named consequence with the measurement behind it;
- ⛔ let the scope change land unremarked. That is the failure this card exists to end — a verdict nobody computed.
The card's own measurement, which you should re-confirm rather than inherit:
fields'distcarries 0 extensionless relative specifiers even before #5214's fix; its published entry failed through@object-ui/providers, never on its own file.The assertion this class of bug needs
Whatever the parse strategy, ship the check that would have caught it:
readTsconfiground-trips every tsconfig in the repository without throwing. Cheap, and it is the difference between fixing two files and closing the class.PM mechanism assumptions — verify, do not trust
ts.parseConfigFileTextToJsonis the natural fix and the repo already depends ontypescript. The previous seat explicitly recorded that it did not verify the "adds no new dependency edge" claim. Verify it yourself before relying on it —scripts/has its own dependency discipline andcheck:phantom-depswill have an opinion.- A string-aware hand-rolled strip is the fallback if 1 fails. Weaker: it is the same category of code as the bug.
- This file imports from
./check-phantom-dependencies.mjs— shared-helper changes reach further than this one gate. Check what else consumes anything you touch there.
Premise-first
The issue body is a lead, not a specification.
premise_still_valid: falsewith no PR is a legitimate delivery if the tree has moved under it — though the four readings above say it has not. Read the card and all four of its comments on GitHub yourself, and check the body for sanitizer truncation before relying on it.
Generated by Claude Code
{ "issue": 5367, "status": "done", "branch": "claude/issue-5367-readtsconfig-jsonc-r3", "pr": "https://github.com/objectstack-ai/objectui/pull/5938", "premise_still_valid": true, "premise_readings_rederived_on": "origin/main ed35c23bb (moved from the 2074a59 quoted at dispatch); all four readings still hold", "premise_readings": { "readTsconfig_is_still_the_regex_strip": "yes, scripts/check-node-esm-load.mjs:124, three .replace() calls then JSON.parse", "parseConfigFileTextToJson_count_in_that_file": 0, "positive_control_effectiveNoEmit_count": 3, "poisoned_configs_still_carry_slash_star_in_strings": "yes — packages/auth/tsconfig.json:9,:37 and packages/fields/tsconfig.json:7,:33", "issue_body_sanitizer_truncation_check": "checked, no truncation — the body contains no angle-bracket fragments for the sanitizer to eat, all code fences balanced, and it terminates on the complete line 'Found via #5214.'" }, "measured_larger_than_the_card": { "tsconfigs_tracked_by_git": 91, "throwing_under_the_regex_strip": 61, "throwing_under_ts_parseConfigFileTextToJson": 0 }, "embedded_scope_question": { "decision": "let @object-ui/fields drop out of the specifier leg", "gate_own_output_before": "Specifier leg: 13 of 39 published ESM packages have a specifier-preserving build.", "gate_own_output_after": "Specifier leg: 12 of 39 published ESM packages have a specifier-preserving build.", "only_membership_change_in_the_repository": true, "auth_unchanged_because": "packages/auth/tsconfig.json declares \"noEmit\": false outright; verdict identical, provenance changes from catch to reading", "fields_src_extensionless_specifiers": 0, "fields_dist_extensionless_of_relative": "0 of 155 (re-confirmed, not inherited: built packages/fields and scanned dist with the gate's own relativeSpecifiers)", "replacement_guard_verified": "packages/fields/tsconfig.json pins moduleResolution nodenext; ablation B below turns its own build tsc red with TS2835" }, "dependency_edge_verification": "no new edge. typescript is a root devDependency ^6.0.3; check-node-esm-load.mjs already loads it transitively via check-phantom-dependencies.mjs (its line 198 is `import ts from 'typescript'`). check:phantom-deps EXIT=0, verdict line 'Every in-scope import is declared by the package that publishes it.' — its own output shows it scans '40 released package(s)', so root scripts/ is outside its scope entirely.", "shared_helper_check": "no shared helper was edited. check-phantom-dependencies.mjs is imported, not modified. Its other consumers (check-package-self-import.mjs, check-node-esm-load.mjs) are untouched; check:self-import EXIT=0.", "summary": "Replaced readTsconfig()'s three-regex comment strip with ts.parseConfigFileTextToJson, TypeScript's own tsconfig reader. The defect is far larger than the card measured: 61 of the repository's 91 tracked tsconfigs threw, not 2, and every throw was swallowed by effectiveNoEmit's catch — so the gate's scope was decided by a fallback rather than a reading. With the fix, 0 of 91 throw. Shipped the class-closing assertion the card asked for (readTsconfig round-trips every tsconfig in the repository, walked not listed, with a floor of 60 on the walk so an empty scan cannot pass as clean) plus five shape/scope pins. The embedded scope question is answered on the record and pinned by test, not left silent: @object-ui/fields leaves the specifier leg (13 preserving packages become 12, the only membership change in the repo). Also corrected a doc claim inside the file that was itself a product of the broken parse — buildPreservesSpecifiers named @object-ui/fields as its emitting-tsc-plus-bundler example, and fields is the opposite; verified no published package matches that shape today. Updated the two comments in this file (:91, :404) that named #5367 as still open. File surface respected exactly: scripts/check-node-esm-load.mjs + scripts/__tests__/check-node-esm-load.test.ts, no breach. Note for PM housekeeping (not a code finding): the stale remote branch claude/issue-5367-readtsconfig-jsonc still exists at 60d452ee with 0 commits — untouched by me, still deletable.", "tests": "All at HEAD f04e306, working tree clean (git status --porcelain -> 0 lines). Exit codes captured by direct redirection, never after a pipe; every verdict below is the gate's own printed line. | pnpm exec vitest run scripts/ --maxWorkers=2 -> EXIT 0, 'Test Files 61 passed (61)' / 'Tests 1637 passed (1637)'. | The gate's own test file, --reporter=verbose, proves the 6 new tests actually executed by name (round-trips EVERY tsconfig / paths-key shape / slash-star line-comment prose / still-THROWS contract / fields NOT specifier-preserving / auth on DECLARED noEmit) -> 'Tests 44 passed (44)'. | check:esm-specifiers -> EXIT 0, 'Specifier leg: no un-ledgered package emits an extensionless relative specifier.' | check:phantom-deps -> EXIT 0, 'Every in-scope import is declared by the package that publishes it.' | check:control-bytes -> EXIT 0, 'check-control-bytes: OK (scanned 4928 tracked text file(s); skipped 85 binary).' | check:type-check-coverage -> EXIT 0, 'test type-check coverage: 41/41 packages compile their tests, 0 declared debt'. | check:self-import -> EXIT 0, 'No package names itself inside its own src/.' | type-check:scripts (tsc -p tsconfig.scripts.json) -> EXIT 0, no output (it was genuinely red first, TS18046/TS2571 x5, and the narrowing was fixed rather than cast away with any). | node scripts/check-changeset-presence.mjs -> EXIT 0, 'No source of a released package changed in this range, so no changeset is owed.' -> followed its verdict, NO changeset added. | node scripts/check-changeset-no-major.mjs -> EXIT 0, 'No changeset declares a major bump.' | eslint on the 2 changed files -> EXIT 0, files linted: 2, 0 errors, 0 warnings. | Manual control-byte self-scan of both changed files with grep -naP over the control ranges -> no match. || ABLATION A (does the new test catch the class?): restored the three-regex strip onto the committed fix. Nothing is rebuilt in this path and that is why no build step is present — node and vitest load scripts/*.mjs from source, and this gate has no dist copy (verified: 0 dist copies of the file). Mutation confirmed ON DISK against the exact text targeted, both directions: injected 'JSON.parse(raw)' count 1, removed 'parseConfigFileTextToJson' count 0 in code with only the doc mention remaining (shown by line). Result: 'Test Files 1 failed (1)' / 'Tests 5 failed | 39 passed (44)', and the gate itself printed '13 of 39' under the ablation vs '12 of 39' with the fix. Predicted direction was RED and RED is what happened, with one honest exception reported rather than papered over: the sixth new test ('still THROWS on a config nobody can read') stays GREEN under the ablation, correctly — the old reader also throws on genuinely broken JSON, so that pin guards the contract, not the defect. Restore ran from a trap on EXIT INT TERM; tree re-verified clean and the gate re-verified at 12 of 39 afterwards. || ABLATION B (is the 'fields loses no coverage' claim real?): replaced one './widgets/TextField.js' with './widgets/TextField' in packages/fields/src/FieldEditWidget.tsx. Again no rebuild is in the path — tsc reads packages/fields/src directly via include:[src]. Mutation confirmed ON DISK both directions: removed-text count 0, injected-text count 1. Baseline before mutation: tsc -p packages/fields/tsconfig.json EXIT 0. After mutation: EXIT 2 with exactly 1 diagnostic, 'packages/fields/src/FieldEditWidget.tsx(15,27): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when --moduleResolution is node16 or nodenext.' The same mutation produced NO finding from the specifier leg (fields now out of scope) — which is exactly the coverage the drop costs and exactly the coverage the compiler replaces, inside fields' own build script. Restored by trap.", "declared_narrowings": [ "ESLint run on the 2 changed files rather than repo-wide. This is a measurement, not a sample, and all three evidences are present: population read from eslint's own config (both files resolved and were linted, with no 'file ignored' warning), file count read from --format json (2), and config invariance for untouched files — eslint.config.js has no projectService, no parserOptions and no project:, so type-aware linting is off and this diff cannot move any untouched file's verdict. The repo-wide run is CI's.", "check:node-esm-load (full mode) not run locally — it builds every published package and has its own CI job, .github/workflows/node-esm-load-gate.yml step 'Build every published package and import its entry under plain Node'. Safe by construction: leg 2 iterates esmPackages (every published ESM package) and never consults buildPreservesSpecifiers, so nothing in this diff can move it. Leg 1, the only leg this diff can move, was run standalone and is green." ], "gate_status_at_report_time": "in_progress — reported at draft-PR time per the dispatch contract; CI convergence is PM's read, not mine. PR #5938 mergeable_state was 'unstable' (checks still running) when this was written.", "governed_surface": "not touched — diff is 2 files under root scripts/. No AGENTS.md, CLAUDE.md, .claude/** or docs/adr/**. PR left as DRAFT; not readied, no auto-merge, not queued.", "labels_note": "a 'tests' label appeared on PR #5938 from a bot after creation. Not set by me and not touched.", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
Generated by Claude Code
os-dev-report
⚠️ Marker repost, not a second report. The HTML-comment marker on comment 5390028838 was eaten by the body sanitizer on write — confirmed by reading the comment back, its body now begins at the fenced block — which makes that copy invisible to a marker scan. This copy uses the literal-text fallback as the first line. Identical JSON, no content change. Editing the original in place was not possible from this seat: noghCLI, and direct REST returnsGitHub access is not enabled for this session.{ "issue": 5367, "status": "done", "branch": "claude/issue-5367-readtsconfig-jsonc-r3", "pr": "https://github.com/objectstack-ai/objectui/pull/5938", "premise_still_valid": true, "premise_readings_rederived_on": "origin/main ed35c23bb (moved from the 2074a59 quoted at dispatch); all four readings still hold", "premise_readings": { "readTsconfig_is_still_the_regex_strip": "yes, scripts/check-node-esm-load.mjs:124, three .replace() calls then JSON.parse", "parseConfigFileTextToJson_count_in_that_file": 0, "positive_control_effectiveNoEmit_count": 3, "poisoned_configs_still_carry_slash_star_in_strings": "yes — packages/auth/tsconfig.json:9,:37 and packages/fields/tsconfig.json:7,:33", "issue_body_sanitizer_truncation_check": "checked, no truncation — the body contains no angle-bracket fragments for the sanitizer to eat, all code fences balanced, and it terminates on the complete line 'Found via #5214.'" }, "measured_larger_than_the_card": { "tsconfigs_tracked_by_git": 91, "throwing_under_the_regex_strip": 61, "throwing_under_ts_parseConfigFileTextToJson": 0 }, "embedded_scope_question": { "decision": "let @object-ui/fields drop out of the specifier leg", "gate_own_output_before": "Specifier leg: 13 of 39 published ESM packages have a specifier-preserving build.", "gate_own_output_after": "Specifier leg: 12 of 39 published ESM packages have a specifier-preserving build.", "only_membership_change_in_the_repository": true, "auth_unchanged_because": "packages/auth/tsconfig.json declares noEmit false outright; verdict identical, provenance changes from catch to reading", "fields_src_extensionless_specifiers": 0, "fields_dist_extensionless_of_relative": "0 of 155 (re-confirmed, not inherited: built packages/fields and scanned dist with the gate's own relativeSpecifiers)", "replacement_guard_verified": "packages/fields/tsconfig.json pins moduleResolution nodenext; ablation B below turns its own build tsc red with TS2835" }, "dependency_edge_verification": "no new edge. typescript is a root devDependency ^6.0.3; check-node-esm-load.mjs already loads it transitively via check-phantom-dependencies.mjs (its line 198 is an import of typescript). check:phantom-deps EXIT=0, verdict line 'Every in-scope import is declared by the package that publishes it.' — its own output shows it scans '40 released package(s)', so root scripts/ is outside its scope entirely.", "shared_helper_check": "no shared helper was edited. check-phantom-dependencies.mjs is imported, not modified. Its other consumers (check-package-self-import.mjs, check-node-esm-load.mjs) are untouched; check:self-import EXIT=0.", "summary": "Replaced readTsconfig()'s three-regex comment strip with ts.parseConfigFileTextToJson, TypeScript's own tsconfig reader. The defect is far larger than the card measured: 61 of the repository's 91 tracked tsconfigs threw, not 2, and every throw was swallowed by effectiveNoEmit's catch — so the gate's scope was decided by a fallback rather than a reading. With the fix, 0 of 91 throw. Shipped the class-closing assertion the card asked for (readTsconfig round-trips every tsconfig in the repository, walked not listed, with a floor of 60 on the walk so an empty scan cannot pass as clean) plus five shape/scope pins. The embedded scope question is answered on the record and pinned by test, not left silent: @object-ui/fields leaves the specifier leg (13 preserving packages become 12, the only membership change in the repo). Also corrected a doc claim inside the file that was itself a product of the broken parse — buildPreservesSpecifiers named @object-ui/fields as its emitting-tsc-plus-bundler example, and fields is the opposite; verified no published package matches that shape today. Updated the two comments in this file (:91, :404) that named #5367 as still open. File surface respected exactly: scripts/check-node-esm-load.mjs + scripts/__tests__/check-node-esm-load.test.ts, no breach. Note for PM housekeeping (not a code finding): the stale remote branch claude/issue-5367-readtsconfig-jsonc still exists at 60d452ee with 0 commits — untouched by me, still deletable.", "tests": "All at HEAD f04e306, working tree clean (git status --porcelain gave 0 lines). Exit codes captured by direct redirection, never after a pipe; every verdict below is the gate's own printed line. | pnpm exec vitest run scripts/ --maxWorkers=2 => EXIT 0, 'Test Files 61 passed (61)' / 'Tests 1637 passed (1637)'. | The gate's own test file with --reporter=verbose proves the 6 new tests actually executed by name (round-trips EVERY tsconfig / paths-key shape / slash-star line-comment prose / still-THROWS contract / fields NOT specifier-preserving / auth on DECLARED noEmit) => 'Tests 44 passed (44)'. | check:esm-specifiers => EXIT 0, 'Specifier leg: no un-ledgered package emits an extensionless relative specifier.' | check:phantom-deps => EXIT 0, 'Every in-scope import is declared by the package that publishes it.' | check:control-bytes => EXIT 0, 'check-control-bytes: OK (scanned 4928 tracked text file(s); skipped 85 binary).' | check:type-check-coverage => EXIT 0, 'test type-check coverage: 41/41 packages compile their tests, 0 declared debt'. | check:self-import => EXIT 0, 'No package names itself inside its own src/.' | type-check:scripts (tsc -p tsconfig.scripts.json) => EXIT 0, no output (it was genuinely red first, TS18046/TS2571 x5, and the narrowing was fixed rather than cast away with any). | node scripts/check-changeset-presence.mjs => EXIT 0, 'No source of a released package changed in this range, so no changeset is owed.' => followed its verdict, NO changeset added. | node scripts/check-changeset-no-major.mjs => EXIT 0, 'No changeset declares a major bump.' | eslint on the 2 changed files => EXIT 0, files linted: 2, 0 errors, 0 warnings. | Manual control-byte self-scan of both changed files with grep -naP over the control ranges => no match. || ABLATION A (does the new test catch the class?): restored the three-regex strip onto the committed fix. Nothing is rebuilt in this path and that is why no build step is present — node and vitest load scripts/*.mjs from source, and this gate has no dist copy (verified: 0 dist copies of the file). Mutation confirmed ON DISK against the exact text targeted, both directions: injected 'JSON.parse(raw)' count 1, removed 'parseConfigFileTextToJson' count 0 in code with only the doc mention remaining (shown by line). Result: 'Test Files 1 failed (1)' / 'Tests 5 failed | 39 passed (44)', and the gate itself printed '13 of 39' under the ablation vs '12 of 39' with the fix. Predicted direction was RED and RED is what happened, with one honest exception reported rather than papered over: the sixth new test ('still THROWS on a config nobody can read') stays GREEN under the ablation, correctly — the old reader also throws on genuinely broken JSON, so that pin guards the contract, not the defect. Restore ran from a trap on EXIT INT TERM; tree re-verified clean and the gate re-verified at 12 of 39 afterwards. || ABLATION B (is the 'fields loses no coverage' claim real?): replaced one './widgets/TextField.js' with './widgets/TextField' in packages/fields/src/FieldEditWidget.tsx. Again no rebuild is in the path — tsc reads packages/fields/src directly via include:[src]. Mutation confirmed ON DISK both directions: removed-text count 0, injected-text count 1. Baseline before mutation: tsc -p packages/fields/tsconfig.json EXIT 0. After mutation: EXIT 2 with exactly 1 diagnostic, 'packages/fields/src/FieldEditWidget.tsx(15,27): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when --moduleResolution is node16 or nodenext.' The same mutation produced NO finding from the specifier leg (fields now out of scope) — which is exactly the coverage the drop costs and exactly the coverage the compiler replaces, inside fields' own build script. Restored by trap.", "declared_narrowings": [ "ESLint run on the 2 changed files rather than repo-wide. This is a measurement, not a sample, and all three evidences are present: population read from eslint's own config (both files resolved and were linted, with no 'file ignored' warning), file count read from --format json (2), and config invariance for untouched files — eslint.config.js has no projectService, no parserOptions and no project:, so type-aware linting is off and this diff cannot move any untouched file's verdict. The repo-wide run is CI's.", "check:node-esm-load (full mode) not run locally — it builds every published package and has its own CI job, .github/workflows/node-esm-load-gate.yml step 'Build every published package and import its entry under plain Node'. Safe by construction: leg 2 iterates esmPackages (every published ESM package) and never consults buildPreservesSpecifiers, so nothing in this diff can move it. Leg 1, the only leg this diff can move, was run standalone and is green." ], "gate_status_at_report_time": "in_progress — reported at draft-PR time per the dispatch contract; CI convergence is PM's read, not mine. PR #5938 mergeable_state was 'unstable' (checks still running) when this was written.", "governed_surface": "not touched — diff is 2 files under root scripts/. No AGENTS.md, CLAUDE.md, .claude/** or docs/adr/**. PR left as DRAFT; not readied, no auto-merge, not queued.", "labels_note": "a 'tests' label appeared on PR #5938 from a bot after creation. Not set by me and not touched.", "marker_note": "the first delivery of this report (comment 5390028838) lost its HTML-comment marker to the body sanitizer; this comment is the literal-text fallback. Two comments, one report.", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
Generated by Claude Code
ACCEPT — PR #5938. Landing held only on CI convergence.
Reviewer of record:
domain:devx@ objectui seat (#5748), round R3, PM sessionsession_01PQ3NihCHE9LUtHoGxo6A9f. Reviewed against GitHub and againstorigin/main, ⛔ not against the report's self-description.check verdict PR shape draft ✅, base main✅, first body lineFixes #5367✅ (all read off the PR, not the report)file surface vs. claim exact — scripts/check-node-esm-load.mjs+scripts/__tests__/check-node-esm-load.test.ts, 2 files, no breachACCEPT path fork get_filesread first: no governed path (docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md). Ordinary ready/queue path applieschangeset none, following check-changeset-presence.mjs's own verdict; diff is rootscripts/, no released package'ssrc/the embedded scope question answered on the record and pinned by test — ⛔ not silent. See below Spot-check: I re-measured the headline claim myself
The PR rests on "61 of 91, not 2". I did not take that on trust — I replicated the old three-regex strip against every tracked tsconfig at
origin/main:tracked tsconfigs on origin/main: 91 throw under the OLD three-regex strip: 61 the two the card was filed for are in the set: auth=true fields=trueIndependent of the dev's tooling, same numbers. So the card was filed for two configs and the true blast radius was two thirds of the repository's tsconfigs — and every one of those throws was absorbed by
effectiveNoEmit's catch. This gate's scope was not narrow-but-wrong; it was mostly not computed at all. That reframes the card from "fix two files" to "the gate had no reading", and the PR says so plainly rather than quietly fixing it.The scope movement is authorised, measured, and I am flagging it rather than burying it
@object-ui/fieldsleaves the specifier leg: 13 of 39 → 12 of 39, the only membership change in the repository. My dispatch authorised exactly two outcomes — keep it in scope deliberately, or let it drop as a named consequence with the measurement. The dev took the second and paid for it:fields' sources carry 0 extensionless relative specifiers, so no live finding leaves with it.- Its built
dist: 0 extensionless of 155 relative specifiers, re-confirmed by building and scanning with the gate's ownrelativeSpecifiers— ⛔ not inherited from the card. - The replacement guard is stricter, and proven by ablation rather than asserted:
packages/fields/tsconfig.jsonpinsnodenext, so mutating one specifier to drop its extension turnstsc -p packages/fields/tsconfig.jsonred with TS2835 — insidefields' ownbuildscript. The same mutation produces no finding from the specifier leg oncefieldsis out of it. That is precisely the coverage traded and precisely the coverage that replaces it.
This is
declared = enforcedbeing restored, not a gate weakening — the package leaves because it genuinely is not specifier-preserving, and the conservative catch is preserved intact (a test pins that a genuinely unreadable config still throws and still grades as emitting). Recording it here and in the round report so the maintainer has an objection window rather than a fait accompli.What lifts this above the card
- The class-closing assertion shipped, and it is walked rather than listed — a hand-maintained list answers "the files someone remembered", which is how two poisoned configs sat unnoticed. It asserts its own size (floor of 60), so an empty walk cannot pass as a clean one. That liveness pin is the part most such tests omit.
- Ablation A reports an honest non-red. Five of the six new tests go red under the restored regex strip; the sixth ("still THROWS on a config nobody can read") stays green, and the dev says so and explains why it guards the contract rather than the defect. A dev reporting the leg that did not move is worth more than one reporting six for six.
- It found a false claim that was itself a product of the bug:
buildPreservesSpecifiers' doc namedfieldsas its emitting-tsc-plus-bundler example, andfieldsis the opposite — the wrong answer from the catch had been written down as a fact about the package. Corrected, with "no published package matches that shape today" verified before keeping the rule. - It closed the class rather than the instance: checked the repo's only other tsconfig reader (
check-type-check-coverage.mjs), measured its scanner as string-aware and clean over all 91, and filed nothing because there was nothing to file.
Deviations, both declared and both sound
- ESLint narrowed to the two changed files — justified as a measurement: population read from eslint's own config, count from
--format json, and type-aware linting is off (eslint.config.jshas noprojectService/parserOptions/project:), so the diff cannot move an untouched file's verdict. Repo-wide is CI's run. check:node-esm-loadfull mode not run locally — safe by construction: leg 2 never consultsbuildPreservesSpecifiers, and leg 1, the only leg this diff can move, was run standalone and is green.
⏳ Landing
Read by name at review time, ⛔ never the aggregate or the required set: 11 success (
Build & E2E,Build Docs,Doc Snippet Type Check,Doc Component Type Check,Control Byte Scan,Internal Docs Link Check,Skill Guide Path Check,Changeset Declaration,Changeset Fixed Group Check,label,Live E2E (informational)) · 3 skipped (Test (coverage),Test (coverage shard …),dependabot) · 6 in_progress (Lint,Type Check,Test (shard 1–4)). Nothing failing.in_progressat draft-PR time is an honest reading and the dispatch contract puts convergence on me. Enqueue resource is every check green, ⛔ not the required subset — so this stays draft until the remaining six land. I will flip ready and enqueue then.Housekeeping, ⛔ neither a code finding nor this PR's fault: the report arrived twice (
5390028838lost its HTML-comment marker to the body sanitizer;5390036907carries the literal-text fallback and survived read-back). And the staleclaude/issue-5367-readtsconfig-jsonc(0 commits,60d452ee) still needs a maintainer — ref deletion is 403 for every agent seat.
Generated by Claude Code
Found while clearing the six small packages of #5214, by running the gate's own exported helpers directly. Not fixed there — it is gate machinery, not the defect that card is about, and the direction of the failure is currently safe.
What was measured, 2026-08-20
readTsconfigstrips comments with two regexes over the raw text:Neither knows what a JSON string is.
packages/auth/tsconfig.jsoncontainsThe
/*inside"@/*"opens a "block comment" that runs to the*/inside"**/*.test.ts"at the bottom of the file, eatingpaths,includeand most ofexclude. What survives the strip is:packages/fields/tsconfig.jsonloses everything after"outDir": "dist"the same way, via thepackages/*/srcwritten inside one of its//comments.Why nothing is red today
effectiveNoEmitcatches the parse failure deliberately:So both packages are graded "emitting",
buildPreservesSpecifiersreturns true, and they land inside the ratchet's scope — which is where they belong. The gate's verdict is right; it just is not being reached by the code that is supposed to decide it.Why it is still worth fixing
packages/fieldsis the case where the answer actually matters and is actually wrong: itstscstep inherits the root'snoEmit: true(it only type-checks;distcomes from vite). Correctly parsed,effectiveNoEmitreturns true andfieldswould drop out of the specifier leg. Measured alongside Seven published packages still emit extensionless relative specifiers, so plain Node ESM cannot load them #5214:fields'distcarries 0 extensionless relative specifiers even before that card's fix — its published entry failed via@object-ui/providers, never on its own file. So the gate is currently ratchetingfields' sources on the strength of a verdict it never computed. (Whether it should keep ratcheting them is a real question and probably yes — but it should be a decision, not an accident.)Suggested shape
Parse with a real JSONC reader rather than regexes — TypeScript itself ships
ts.parseConfigFileTextToJson, and the repo already depends ontypescript. If a dependency-free strip is preferred, it has to be string-aware: track"and skip comment openers inside strings.Whatever the fix, it should come with the assertion this class of bug needs: that
readTsconfiground-trips every tsconfig in the repository without throwing. That is the check that would have caught it, and it is cheap.Found via #5214.