Skip to content

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

@os-support-ai

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

import { readTsconfig, effectiveNoEmit } from './scripts/check-node-esm-load.mjs';
readTsconfig('packages/auth/tsconfig.json')
//   SyntaxError: Expected ':' after property name in JSON at position 182
readTsconfig('packages/fields/tsconfig.json')
//   SyntaxError: Expected ',' or '}' after property value in JSON at position 87

readTsconfig strips comments with two regexes over the raw text:

.replace(/\/\*[\s\S]*?\*\//g, '')      // block comments
.replace(/^[ \t]*\/\/.*$/gm, '')       // whole-line comments

Neither knows what a JSON string is. packages/auth/tsconfig.json contains

"paths": { "@/*": ["src/*"] },
...
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"]

The /* inside "@/*" opens a "block comment" that runs to the */ inside "**/*.test.ts" at the bottom of the file, eating paths, include and most of exclude. What survives the strip is:

  "paths": {
    "@*.test.ts", "**/*.test.tsx"]
}

packages/fields/tsconfig.json loses everything after "outDir": "dist" the same way, via the packages/*/src written inside one of its // comments.

Why nothing is red today

effectiveNoEmit catches the parse failure deliberately:

} catch {
  // An unreadable config is graded as emitting, so a parse failure widens the
  // scan rather than silently dropping a package out of it.
  return false;
}

So both packages are graded "emitting", buildPreservesSpecifiers returns 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

  • The conservative catch is the documented fallback for an unreadable config. It is silently absorbing a readable one, so the safety margin the comment describes is already spent, and nobody can see it.
  • packages/fields is the case where the answer actually matters and is actually wrong: its tsc step inherits the root's noEmit: true (it only type-checks; dist comes from vite). Correctly parsed, effectiveNoEmit returns true and fields would 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' dist carries 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 ratcheting fields' 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.)
  • Any future package whose tsconfig genuinely should be excluded will be included instead, with no signal.

Suggested shape

Parse with a real JSONC reader rather than regexes — TypeScript itself ships ts.parseConfigFileTextToJson, and the repo already depends on typescript. 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 readTsconfig round-trips every tsconfig in the repository without throwing. That is the check that would have caught it, and it is cheap.

Found via #5214.

Activity

  1. added theissue type on Aug 20, 2026
  2. os-zhuang commented on Aug 20, 2026

    @os-zhuang
    Contributor

    Triage: lands in scripts/check-node-esm-load.mjs (readTsconfig() string-unaware strip); promoted finding → pm:queue, type Bug. The gate is currently ratcheting packages/fields on 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 (whether fields stays 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

  3. self-assigned this
    on Aug 21, 2026
  4. os-warren commented on Aug 21, 2026

    @os-warren
    Collaborator

    Claim: PM loop round 1
    Session: session_014rYHTXpNpNFqsZ6rbtsoDf
    Branch: claude/issue-5367-readtsconfig-jsonc
    Worktree: objectui-issue-5367
    Domain: domain:devx (objectui carries no domain:* 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 on origin/main 60d452e; no open PR touches scripts/. Open PRs repo-wide: #5472 (draft, awaiting maintainer merge), #5400 (release), #4958 / #4945 (dependabot). None intersect.

    Premise re-verified at claim time, on origin/main 60d452e

    The 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:

    ⚠️ Note the --tier reading normally quoted on this line is not available in this repo: scripts/pm/dispatch-gates.mjs is objectstack-resident and objectui has no scripts/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

  5. removed their assignment
    on Aug 21, 2026
  6. os-warren commented on Aug 21, 2026

    @os-warren
    Collaborator

    Claim released — dispatched dev produced nothing, card returned to pm:queue

    Releasing my own claim (5365444185, session session_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-jsonc exists, 0 commits of its own (still at 60d452ee, i.e. main as 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 under git. 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.

    main has moved 60d452ee → 7d0143c3 since dispatch, so the branch is also stale. Delete it or reset it from current main; ⛔ do not branch on top of it.

    Carried forward so the next dispatch does not re-derive it

    Verified on origin/main 60d452ee — re-check against current main 7d0143c3 before relying on any of it, but this is where to start:

    No gates were run and no code was written, so nothing here is a review verdict.


    Generated by Claude Code

  7. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    on Aug 21, 2026
  8. huangyiirene commented on Aug 21, 2026

    @huangyiirene
    Collaborator

    Triage (three-stream backfill per the 2026-08-21 domain:ui ruling): added domain:devx — check-node-esm-load.mjs is gate tooling, the engineering face.


    Generated by Claude Code

  9. self-assigned this
    on Aug 24, 2026
  10. os-warren commented on Aug 24, 2026

    @os-warren
    Collaborator

    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 under scripts/__tests__/ (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus. ⚠️ scripts/pm/dispatch-gates.mjs is objectstack-resident and objectui has no scripts/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 touches scripts/ — checked by head: #5925 (app-shell preview comment), #5922 (layout docs), #5843 (governed docs/adr), #5774 (.github/workflows/ + ci-cd-pipeline.md), #5400 (release), #4958 (dependabot). Concurrent in this batch: #3840 (apps/console probe) and #5458 (eslint-rules/ + app-shell views) — neither touches scripts/.

    ⛔ 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 at 60d452ee. ⛔ Do not branch from it, do not reset it, do not reuse its name. Branch from current main under 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
    readTsconfig is still the regex strip yes — :124, three .replace() calls over raw text, then JSON.parse
    parseConfigFileTextToJson in that file 0
    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 strings yes — packages/auth/tsconfig.json:9 "@/*": ["src/*"] and :37 "**/*.test.ts"; packages/fields/tsconfig.json:7 (packages/*/src inside a // comment) and :33

    And #5382's landed code still points here, at :91 and :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, effectiveNoEmit returns true for packages/fields, which drops it out of the specifier leg of the ratchet. Today it is inside that leg only because the parse throws and the catch grades it "emitting".

    So the fix necessarily changes a gate's scope. Both of these are acceptable; exactly one thing is not:

    • keep fields in 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' dist carries 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: readTsconfig round-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

    1. ts.parseConfigFileTextToJson is the natural fix and the repo already depends on typescript. 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 and check:phantom-deps will have an opinion.
    2. A string-aware hand-rolled strip is the fallback if 1 fails. Weaker: it is the same category of code as the bug.
    3. 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: false with 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

  11. os-warren commented on Aug 24, 2026

    @os-warren
    Collaborator
    {
      "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

  12. os-warren commented on Aug 24, 2026

    @os-warren
    Collaborator

    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: no gh CLI, and direct REST returns GitHub 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

  13. os-warren commented on Aug 24, 2026

    @os-warren
    Collaborator

    ACCEPT — PR #5938. Landing held only on CI convergence.

    Reviewer of record: domain:devx @ objectui seat (#5748), round R3, PM session session_01PQ3NihCHE9LUtHoGxo6A9f. Reviewed against GitHub and against origin/main, ⛔ not against the report's self-description.

    check verdict
    PR shape draft ✅, base main ✅, first body line Fixes #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 breach
    ACCEPT path fork get_files read first: no governed path (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md). Ordinary ready/queue path applies
    changeset none, following check-changeset-presence.mjs's own verdict; diff is root scripts/, no released package's src/
    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=true
    

    Independent 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/fields leaves 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 own relativeSpecifiers — ⛔ not inherited from the card.
    • The replacement guard is stricter, and proven by ablation rather than asserted: packages/fields/tsconfig.json pins nodenext, so mutating one specifier to drop its extension turns tsc -p packages/fields/tsconfig.json red with TS2835 — inside fields' own build script. The same mutation produces no finding from the specifier leg once fields is out of it. That is precisely the coverage traded and precisely the coverage that replaces it.

    This is declared = enforced being 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 named fields as its emitting-tsc-plus-bundler example, and fields is 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.js has no projectService / parserOptions / project:), so the diff cannot move an untouched file's verdict. Repo-wide is CI's run.
    • check:node-esm-load full mode not run locally — safe by construction: leg 2 never consults buildPreservesSpecifiers, 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_progress at 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 (5390028838 lost its HTML-comment marker to the body sanitizer; 5390036907 carries the literal-text fallback and survived read-back). And the stale claude/issue-5367-readtsconfig-jsonc (0 commits, 60d452ee) still needs a maintainer — ref deletion is 403 for every agent seat.


    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

Labels

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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions