Repository navigation
finding(tooling): js-comment-mask still opens a phantom regex on a self-closing JSX tag and on a slash in JSX text #7882
Description
Measured while implementing #6891, on cf7f2af6e. Filed unassigned, not fixed there: #6891 closes the LT SLASH half with a one-token rule; this half provably cannot be closed the same way, so it is its own decision.
The defect
scanSource opens a REGEX when the character before a / is not a value (IDENT_CHAR, ), ]). } and GT are not values, so both of these open a phantom regex that runs to end of line:
- a self-closing JSX tag whose
/follows a brace —Foo bar={x} SLASH GT,Foo {...p} SLASH GT - a
/in JSX text —{a}/{b}, orcode OS_PAGE_REACT=off /code SLASH code x /code
A self-closing tag whose / follows a value (br SLASH GT, Foo SLASH GT, Foo x="1" SLASH GT) was never affected — the discriminator is the preceding byte, not the tag shape.
Measured
Over all 4,322 tracked source files, with #6891's fix applied and both flag arrays diffed byte for byte against the pre-fix scanner: 145 bytes across 28 files are still flagged literal by this shape. They were invisible before only because the closing tag's larger phantom started earlier on the same line and covered them.
It reaches the FABRICATES direction too — the phantom swallows a comment opener that follows it on the same line, so genuinely commented-out text comes back to a caller as live code:
const c = Foo bar={x} SLASH GT ; // dead = 1;
maskComments leaves dead = 1; standing. That is the direction this module's header calls worse than no verifier at all.
Why #6891 did not take it
There is no one-token rule for it, and the two obvious ones are both wrong:
/followed byGTis a tag closer. That is exactly how a regex matching aGTis spelled —str.replace(/GT/g, '>')— and this tree writes that shape.}andGTare values. Both are genuinely ambiguous in JavaScript:function f(){} /re/is a regex, anda GT /re/.test(b)parses.
Separating them needs to know whether the scanner is inside a JSX element, which is a parser. js-comment-mask.mjs is deliberately a cheap language-level masker, and turning it into a parser is a different card.
Already pinned, so this cannot be closed silently
scripts/__tests__/js-comment-mask-jsx-6891.test.ts carries a KNOWN LIMITS block asserting today's behaviour for all four shapes. Whoever closes this will see those pins fail and must retire them deliberately — the same two-way discipline that surfaced #6891 itself.
Shape of a fix, for triage rather than as a decision
- Leave it. The residue is 145 bytes, it is pinned, and no gate's verdict moves today. Cheapest, and honest as long as the pins stay.
- Port objectstack's corpus sweep (
scripts/check-comment-mask-corpus.mjs, named in this module's header as NOT ported here) and decide against a measurement over an independent parser rather than against cases. This is the route finding(tooling):js-comment-maskreads a JSX closing tag as a regex literal — 7 measured call sites become undelimitable #6891's own triage comment named as the only one that answers "which direction does it fail in". - A JSX-aware mode, opt-in per caller. Largest, and it changes what this module is.
Generated by Claude Code
Activity
os-zhuang commentedon Sep 6, 2026
分诊:
domain:devx/tooling+finding/needs-user-decision/priority:p3⛔
needs-user-decision不与pm:*并存。落点核实(
origin/main)⇒ #6891 的
LT SLASH规则已落地(source[i - 1] !== '<'),而判别式仍是「前一字节是不是一个值」:IDENT_CHAR/)/]是值,⇒}与>都不是 ⇒ 卡片描述的两种幻影(自闭合标签的/跟在}后、JSX 文本里的/)在这条判别式下确实仍然开启。⇒ 那一块 pin 确实在,⇒ 卡片那句「已被钉住,因此不能被静默关闭」成立。
⭐ 卡片最重要的一段:它到达了 FABRICATES 方向
⇒ ⭐ 这不是「漏报」,是「捏造」——一段被注释掉的代码被交还给调用方当作活代码。⛔ 这条把本卡与一个普通的覆盖面缺口区分开了,而它正是该模块自己的档头判定为「比没有验证器更糟」的那个方向。
为什么定 p3(尽管方向危险)
为什么是决定箱
卡片列了三支,并且证明了没有便宜的一票解:
⇒ ⭐ 两条显而易见的规则各自被一个树内反例否掉 —— 这不是「还没想到办法」,是已经证明了廉价规则不存在。⇒ 决定箱成立。⛔ 本席不裁决(本会话
claude-opus-5,CONTRACT_REVIEW_TIER硬门要求 fable)。四facet
① 事实(已核)
js-comment-mask.mjs:302的判别式以「前一字节是否为值」决定是否开启正则;}与>不在值集里 ⇒ 自闭合 JSX 标签(/跟在}后)与 JSX 文本里的/仍开启一个跑到行尾的幻影。实测残留 145 字节 / 28 文件,且到达 FABRICATES 方向。四种形状已被js-comment-mask-jsx-6891.test.ts:178的KNOWN LIMITS钉住。② 分叉
scripts/check-comment-mask-corpus.mjs,本模块档头明说尚未移植到这里),对着一个独立的 parser 做测量再决定,而不是对着用例决定。③ 各支的代价
js-comment-maskreads a JSX closing tag as a regex literal — 7 measured call sites become undelimitable #6891 自己的分诊已经点名这是唯一能回答「它往哪个方向失败」的路线 ⇒ 它不是三支之一,它是回答另外两支所需的前置测量。js-comment-mask.mjs刻意是一个廉价的语言级 masker,把它变成 parser 是另一张卡)。④ 需要裁决者提供的东西
一句话:
js-comment-mask是否应当保持为一个廉价的语言级 masker? 答「是」⇒ 1 或 2(且 2 只是把边界测清楚,不改性质);答「否」⇒ 3,但那是重新定义这个模块。⭐ 一条不可省的约束
⇒ ⛔ 任何一支(包括「不动」)都不得在不处理那四条 pin 的情况下关闭本卡。 ⭐ 而「双向纪律」正是本轮同批 #7883 里那条反着写的钉子所示范的东西——同一个模块族,同一条纪律,第二次生效。
Related:#6891(已落地的
LT SLASH那一半)· #7883(因 #6891 而冗余的那个下游变通,本轮已判pm:queue/ p3)· objectstack 的scripts/check-comment-mask-corpus.mjs(路线 2 的来源)。⛔ 本席为 triage 席位:不认领、不派单、不写码、不合并、不裁决 decision-box。
Generated by Claude Code
os-zhuang commentedon Sep 7, 2026
Ruling recorded — option 2, with a standing principle (director seat, decision batch #72, 2026-09-07)
Maintainer reply, verbatim: 「7749 已加入队列,其他同意」 — this card was presented with option 2 recommended and is covered by 「其他同意」.
Ruling:
js-comment-maskstays a cheap, language-level masker. ⛔ Option 3 (a JSX-aware mode) is ruled out: it changes what the module is, and a parser is a different card if one is ever wanted.scripts/check-comment-mask-corpus.mjs, named in this module's own header as not yet ported) and measure the residue against an independent parser — which direction it fails in, and which gates' verdicts it can move. That measurement is the deliverable; it turns "145 bytes / 28 files" into a reading a decision can be made on.KNOWN LIMITSpins inscripts/__tests__/js-comment-mask-jsx-6891.test.tsstay until a fix retires them deliberately. ⛔ Nothing closes this card silently.After the sweep: if it shows the FABRICATES direction actually moves a gate's verdict, the card comes back to the decision box with that reading; if it shows no gate is affected, option 1 (leave it, pinned) is the standing answer and the card can close on the measurement.
needs-user-decision→pm:queue.Generated by Claude Code
claude commentedon Sep 7, 2026
Claim: PM loop round R46 —
domain:devx @ objectuiexecution seat.Session:
session_01FhBNJcLRZLe8M87VcUgpKrBranch:
claude/issue-7882-comment-mask-corpus-sweepWorktree:
/home/user/objectui-issue-7882Domain:
domain:devxFile surface: new
scripts/check-comment-mask-corpus.mjs(ported from objectstack) plus its test underscripts/__tests__/, acheck:*line in the rootpackage.json, and the header paragraph ofscripts/js-comment-mask.mjsthat says the sweep is not ported. ⛔ Not the masker's scanner, not the fourKNOWN LIMITSpins, not.github/workflows/**.Dispatch: one
os-devsubagent of this session (mode:subagent,model: opus); the dispatch note with the brief follows on this card. Labelspm:queue→pm:dispatchedand assignee set in one write, read back at 2026-09-07T07:23Z.Generated by Claude Code
claude commentedon Sep 7, 2026
Dispatched —
domain:devx @ objectuiexecution seat, PM sessionsession_01FhBNJcLRZLe8M87VcUgpKr, R46, 2026-09-07T07:25Z. Batch 3 of 3 in flight (with #5174 batch 35 in CI on PR #8271 and #7964; disjoint file surfaces).Dev. One
os-devsubagent of this session on branchclaude/issue-7882-comment-mask-corpus-sweep, worktree/home/user/objectui-issue-7882, baseorigin/mainatb6f821a8bor newer. Race re-read at 07:23Z: the newestClaim:on this card is5566735980, naming that branch.Ruling applied, verbatim in the brief: comment
5565859060(director seat, decision batch #72, maintainer 「其他同意」): the masker stays a cheap language-level masker (option 3 ruled out); port objectstack'sscripts/check-comment-mask-corpus.mjsand measure the residue against an independent parser — direction, and which gates' verdicts it can move; the fourKNOWN LIMITSpins stay; nothing closes the card silently. The PR is the port (Part of #7882, no closing keyword); the measurement is posted on this card, and this seat decides on it whether the card closes on option 1 or returns to the decision box.Premise check (read 07:22Z on
b6f821a8b). The discriminator atjs-comment-mask.mjsline ~302 is as the triage read it (/after}or>still opens a phantom); the header (lines ~64–70) still says the sweep "is not ported to objectui and nothing in this repository runs it";KNOWN LIMITSat the test's line 189; objectstack's script is at blobdd50cb13aand imports a helper (import-prerequisite.mjs) objectui lacks. Whether@typescript-eslint/parseris a root devDependency here is the first assumption tested: present → use; transitive only → declare at the lockfile's version; absent → measured stop (a new dependency is the maintainer's call).Mechanism assumptions the dev tests first: objectui's
scanSourceexposes a per-byte comment flag the comparator can diff; the ported--self-testis green on the unchanged masker and the--maskercontrol is red, both by exit code; the sweep bounds in time over this tree; the card's 145 bytes / 28 files re-derive (not inherited); "which gates it can move" is answered by running the masker's importers underscripts/on the base against the FABRICATES files.Generated by Claude Code
claude commentedon Sep 7, 2026
os-dev-report
The measurement (the deliverable the ruling asked for)
Taken on
b6f821a8b, whole tree,@typescript-eslint/parser8.67.0 as the independent oracle.Which direction it fails in: OVER-MASKS, exclusively. There is not one fabricated byte anywhere in the tree. That is the direction
js-comment-mask.mjs's own header calls the better one to fail in.The one disagreement
apps/console/src/pages/DocsIndex.tsx, one contiguous span, lines 63 to EOF (1,517 bytes, 37 lines).It is objectui#7882's "slash in JSX text" half, reaching a worse case than the card describes. The JSX text
src/docs/*.mdputs a/immediately before a*, so the masker opens a phantom block comment -- not the phantom regex the card leads with -- and since no terminator follows anywhere in the file, it swallows the entire remainder of the file. Every gate that masks this file reads 37 lines of live JSX as prose.Which gates' verdicts it can move: NONE, measured
Seven gates under
scripts/import the masker. Which of them actually open that file was measured with areadFileSyncprobe rather than reasoned from their globs:check-vi-mock-inheritcheck-vi-mock-specifierscheck-doc-example-shared-readercheck-docs-route-eager-closurecheck-element-data-source-declarationcheck-entry-guardcheck-pre-install-import-graphFor the two that read it: each was run against the tree as-is and against a copy in which the phantom is closed by a one-character edit to the JSX text. Both exit 0 in both conditions with byte-identical output. The mutation was verified on disk (blob hash moved, marker counts flipped) and the restore verified by an empty
git diff HEADplus a matching blob hash.The card's own numbers, re-derived rather than inherited
WARNING -- these are a different measurement from the sweep's, and neither supersedes the other. The card's 145/28 is a diff of the LITERAL flag arrays between today's masker and the pre-objectui#6891 one. The sweep diffs the COMMENT array against a parser. Because the two maskers differ by zero comment bytes tree-wide, a comment-array comparator is blind to the objectui#6891 / objectui#7882 phantom by construction, except where that phantom happens to swallow a comment opener -- which in this tree happens exactly once, in the file above.
Consequence worth carrying into the decision: the ported sweep is not an instrument for the literal-flag half of this card. A green run must not be read as coverage of it. This is stated in both headers rather than left to be discovered.
Against the ruling's own decision rule
FABRICATES is zero bytes, and no gate's verdict moves. On the ruling's own terms that is the option 1 branch: leave it, pinned. The four
KNOWN LIMITSpins are untouched and still pass.One qualification for the PM, offered rather than acted on: the reading is clean in the FABRICATES direction but not clean overall -- 37 lines of a real component are currently invisible to two gates. That costs nothing today because neither gate has anything to say about that file, and it is the quiet direction by design. It is a fact with a shelf life, not a property, which is what the ported script exists to re-take.
The instrument
Draft PR: #8277 --
Part of #7882, no closing keyword, left in draft.{ "issue": 7882, "status": "done", "branch": "claude/issue-7882-comment-mask-corpus-sweep", "pr": "https://github.com/objectstack-ai/objectui/pull/8277", "base_sha": "b6f821a8b7feda5636521b603c3e974fa52d5db3", "final_head": "4e28c0dae66384f38416522863b5575c4b7a4151", "premise_still_valid": true, "assumptions": { "A1_parser_resolves_from_root": "FALSIFIED as stated, and routed around without a new dependency. '@typescript-eslint/parser' is NOT a root devDependency here (only the 'typescript-eslint' meta-package, ^8.67.0) and does not resolve from the repo root at all under pnpm isolated node_modules (measured: ERR_MODULE_NOT_FOUND). The dispatch's remedy was to declare it at the lockfile version; a cheaper one exists and was taken: 'typescript-eslint' re-exports the identical parser at the identical version and is already declared, so the port adds NO dependency and moves NO lockfile line. Cost: the re-export exposes parseForESLint rather than parse, adapted in one function (adaptParse).", "A2_scanSource_exposes_comment_array": "HELD. objectui's scanSource returns { comment, literal } as same-length Uint8Arrays; the comparator needed no adaptation.", "A3_selftest_green_and_masker_control_red": "HELD, but NOT via the masker the dispatch suggested. --self-test passes unchanged (23 cases, exit 0). The positive control is a blind masker in a temp dir: exit 1, 4,370 of 4,418 files, 21,602,924 fabricated bytes. The pre-objectui#6891 masker does NOT work as a control here -- measured, it produces an IDENTICAL reading and exits 0, because that defect moves zero comment bytes. Recorded in the header as a property of the instrument.", "A4_bounded_time": "HELD. 4,418 files in 32.0-33.4 s wall clock across five runs; 0 unparseable.", "A5_card_numbers_rederived": "HELD and re-derived, not inherited: 145 literal bytes across 28 files, and 0 comment bytes changed, all exact. But the sweep CANNOT derive them -- they are a literal-array masker-vs-masker diff, taken with a separate one-off instrument. FABRICATES is reachable in principle (the KNOWN LIMITS pin proves the shape) yet measures ZERO in this tree." }, "sweep_reading": { "files_scanned": 4418, "wall_clock_s": 32.0, "unparseable": 0, "fabricates": { "files": 0, "bytes": 0 }, "over_masks": { "files": 1, "bytes": 1517 }, "card_claim_rederived": "equal -- 145 bytes / 28 files / 0 comment bytes moved, all exact, via a separate literal-array instrument" }, "gates_that_read_fabricates_files": [], "gates_that_read_the_disagreeing_file": [ { "gate": "check-vi-mock-inherit", "reads": true, "verdict_moves": false, "evidence": "exit 0 both before and after the phantom is closed; stdout byte-identical (diff empty)" }, { "gate": "check-vi-mock-specifiers", "reads": true, "verdict_moves": false, "evidence": "exit 0 both before and after; stdout byte-identical (diff empty)" }, { "gate": "check-doc-example-shared-reader", "reads": false, "verdict_moves": false, "evidence": "readFileSync probe: never opens the file" }, { "gate": "check-docs-route-eager-closure", "reads": false, "verdict_moves": false, "evidence": "readFileSync probe: never opens the file" }, { "gate": "check-element-data-source-declaration", "reads": false, "verdict_moves": false, "evidence": "readFileSync probe: never opens the file" }, { "gate": "check-entry-guard", "reads": false, "verdict_moves": false, "evidence": "readFileSync probe: never opens the file" }, { "gate": "check-pre-install-import-graph", "reads": false, "verdict_moves": false, "evidence": "readFileSync probe: never opens the file" } ], "files_changed": [ "scripts/check-comment-mask-corpus.mjs (new)", "scripts/__tests__/check-comment-mask-corpus.test.ts (new)", "scripts/js-comment-mask.mjs (header only)", "package.json (one check: script line)" ], "gates": { "vitest scripts/__tests__/check-comment-mask-corpus.test.ts": 0, "vitest scripts/__tests__/js-comment-mask*.test.ts": 0, "vitest scripts/__tests__/ (whole suite, 116 files / 3443 tests)": 0, "check-comment-mask-corpus.mjs --self-test": 0, "check-comment-mask-corpus.mjs (the sweep)": 0, "check-comment-mask-corpus.mjs --masker BLIND (positive control)": 1, "check-comment-mask-corpus.mjs --no-such-flag (usage)": 2, "pnpm check:phantom-deps": 0, "pnpm check:entry-guard": 0, "pnpm check:control-bytes": 0, "pnpm check:upstream-port-parity": 0, "pnpm type-check:scripts": 0, "pnpm lint:root": 0, "node scripts/check-changeset-presence.mjs": 0, "node scripts/check-governed-queue-guard.mjs --test (4 paths)": 0 }, "deviations": [ "Oracle reached through the already-declared 'typescript-eslint' instead of declaring '@typescript-eslint/parser' at the root as the dispatch proposed. Strictly smaller: no new dependency, no lockfile movement, same package and version underneath. Declared in the script header as divergence 1.", "The sweep's posture is REPORT-with-a-ceiling rather than blanket exit 0. The dispatch said to make it REPORT because objectstack has no known-residue posture (verified: upstream fails on ANY disagreement, no allowlist). This copy exits 0 on today's tree -- so no new red, the stated constraint -- while a FABRICATED byte (currently zero) or an over-mask above the declared ceiling still reds. A gate that can never fail would have been a phantom check.", "Upstream's '.cache' skip entry dropped (objectstack-only: its console build materialises an objectui checkout there; this repo has no such directory) and this tree's own generated dirs added. 'apps/site/.source' really was in the corpus before this entry -- 4 generated files.", "objectstack's 1,448-line scripts/import-prerequisite.mjs was NOT ported; its one needed behaviour is inlined in ~15 lines. Cited in the header.", "The port is deliberately NOT enrolled in scripts/upstream-port-pin.json. That pin verifies by reversing declared divergences to a byte-exact reconstruction, which cannot pay off for a port with four structural divergences. Flagged as an open question rather than decided here.", "objectstack's scripts/pm/dispatch-gates.mjs does not exist in objectui, so the gate list was derived by hand from this repo's package.json and the importer graph, not machine-derived." ], "out_of_scope_findings": [ "filed as objectstack#16540: check-comment-mask-corpus swallows an unknown FIRST option -- with --masker absent, indexOf returns -1 so the filter drops argv[0] instead of the flag pair, and an unknown flag runs a full sweep and exits 0 instead of the usage error. Found by this port's own test on its first run; the port carries the guard, objectstack does not." ], "open_questions": [ { "question": "Should the ported sweep be enrolled in scripts/upstream-port-pin.json?", "options": [ "A -- leave it unenrolled (what this PR does): the four divergences are structural, so a reversal would reconstruct most of the file and the pin would assert almost nothing while adding a re-sync ritual to every upstream edit.", "B -- enrol it: objectstack keeps fixing this script (the argv defect above is one), and today nothing tells objectui that upstream moved." ], "recommendation": "A for now, and B is a real card once the divergences settle. On the business-need axis the pin's demonstrated value was catching 3,600 lines of silent drift in a near-verbatim copy; this copy is not near-verbatim, so that mechanism does not fit it. On the startup-focus axis an enrolment costs a ritual on every upstream edit for an instrument run by hand today. The cost of A is real and named: upstream fixes will not arrive on their own." }, { "question": "Is the ceiling posture the right long-term shape, or should the sweep be wired into CI?", "options": [ "A -- leave it a hand-run instrument (what this PR does; no workflow edits were permitted).", "B -- wire it into lint.yml so the FABRICATES-zero assertion is actually enforced on every PR." ], "recommendation": "B eventually, as a separate card with the workflow change. On the anti-AI-error axis the FABRICATES assertion is exactly the contract-tightening kind -- it is at zero today, so enforcing it is free and it fails loudly rather than tolerantly. But it needs a ~33 s CI step and a workflow edit this dispatch forbids, so it is a decision, not a rider." } ], "mcp_calls": 2, "summary": "Ported objectstack's corpus sweep to objectui as scripts/check-comment-mask-corpus.mjs with a test, a package.json script line, and the js-comment-mask.mjs header paragraph that claimed the sweep was unported. scanSource, the discriminator and the four KNOWN LIMITS pins are untouched. The measurement: 4,418 files, ONE disagreement, ZERO fabricated bytes, 1,517 over-masked bytes, all of it objectui#7882's slash-in-JSX-text shape in apps/console/src/pages/DocsIndex.tsx, where a slash before a star opens a phantom block comment that runs to EOF. No gate's verdict moves -- measured by running the two gates that actually read the file against a copy with the phantom closed. The card's 145 bytes / 28 files re-derives exactly, but via a separate literal-array instrument: the comment-array sweep is blind to that half by construction, which is recorded in both headers so a green run is not misread as coverage.", "tests": "vitest scripts/__tests__/check-comment-mask-corpus.test.ts -> exit 0, 28 tests. vitest on both mask test files -> exit 0, 50 tests (KNOWN LIMITS pins intact). Whole scripts suite -> exit 0, 116 files / 3,443 tests (via the shared verify lock: VERDICT command-exit 0, held the lock 117s). Self-test -> exit 0, 23 cases. Sweep -> exit 0 (REPORT). Positive control, blind masker in a temp dir -> exit 1, 4,370 of 4,418 files, 21,602,924 fabricated bytes, with the temp fixture removed by a trap. Verdict-move experiment -> source mutation proven on disk (blob 40900c7b to 00f29056, marker counts 1 to 0 and 0 to 1), both gates byte-identical, restore proven by empty git diff HEAD and a blob hash matching HEAD. All exit codes captured by redirect, never through a pipe." }Generated by Claude Code
claude commentedon Sep 7, 2026
LANDED and CLOSED on the measurement —
domain:devx @ objectuiseat, PM sessionsession_01FhBNJcLRZLe8M87VcUgpKr, 2026-09-07T08:29Z.PR #8277 merged at 08:28:37Z as
3c6394cb2; content probe on re-fetchedorigin/mainpassed (thecheck:comment-mask-corpusscript line,KNOWN_RESIDUEwith the 1,517-byte ceiling, the masker header updated; control line present).The reading, per the ruling (
5565859060) — "if it shows no gate is affected, option 1 (leave it, pinned) is the standing answer and the card can close on the measurement":apps/console/src/pages/DocsIndex.tsx, the slash-in-JSX-text shape reaching a phantom block comment to EOF)check-vi-mock-inherit,check-vi-mock-specifiers— both byte-identical output before and after the phantom is closedSo: option 1 stands. The four
KNOWN LIMITSpins inscripts/__tests__/js-comment-mask-jsx-6891.test.tsstay, untouched by this PR, and remain the deliberate record; the masker stays a cheap language-level masker (option 3 ruled out). Closed as completed: the measurement the card was reopened for is taken and on record. State:pm:dispatchedremoved, assignee cleared in the same write.Follow-ups the dev left as open questions, routed: (1) wiring the sweep into CI so the FABRICATES-zero assertion is enforced on every PR (a ~33 s step and a workflow edit) → filed as its own
finding; (2) enrolling the port inupstream-port-pin.json→ recorded in the same card as the second question, since the same instrument is at stake. Upstream's argv defect found by the port's test is objectstack#16540 (finding).Generated by Claude Code
claude commentedon Sep 7, 2026
Follow-up filed: #8286 (
finding) carries both open questions from the report — wiring the sweep into CI so the FABRICATES-zero assertion is enforced, and whether the port tracks upstream viaupstream-port-pin.json. —domain:devx @ objectuiseat, 2026-09-07T08:32Z.Generated by Claude Code