Repository navigation
Six source-scanning gates strip comments with a naive regex that a /* inside a string literal blinds — the repo already has the string-aware masker they should use #9367
Description
Filed unassigned from the #8985 dev seat (session session_012WKSnqAaoqtW3QX7SSf1Vk, PR #9365). Found while reverse-verifying a pin I wrote; the same mechanism bit my own detector first, which is how it was noticed at all.
The mechanism
Several check-*.mjs gates decide "is this code or prose?" with
source.replace(/\/\*[\s\S]*?\*\//g, '')That regex has no idea what a string literal is. Any source carrying /* inside a string — a glob ('src/**/*', `@objectstack/*`), a route wildcard (`/api/v1/auth/*`, `/_console/*`) — opens a phantom block comment that runs to the next real */, usually a docblock hundreds of lines further down, taking every line of real code in between with it.
The failure direction is the dangerous one: silent under-reporting. The gate reports clean over code it never looked at. AGENTS.md already names this class — "a verifier that silently degrades (reusing a stale build, skipping a check it could not run) is worse than no verifier, because it reports success."
Measured
How much real source each file loses to the naive strip, on 0a5adbaed:
| file | lines | removed by the naive strip |
|---|---|---|
packages/cli/src/commands/start.ts |
469 | 265 (56%) |
packages/cli/src/commands/serve.ts |
4172 | 1058 |
packages/cli/src/utils/console.ts |
653 | 209 |
Those totals include genuine comments, so here is the isolated proof on start.ts, which is where I hit it: --auth-secret's flag description contains the literal /api/v1/auth/*, and the phantom comment it opens closes against a docblock 250 lines later. A detector looking for a specific assignment reported the file clean while the assignment was demonstrably present on line 327 — the phantom span covered lines 133 to 384.
105 non-test source files under packages/*/src and packages/*/*/src carry a string literal containing /*, i.e. carry the trigger:
grep -rlE "['\"\`][^'\"\`]*/\*[^'\"\`]*['\"\`]" --include=*.ts packages/*/src packages/*/*/src | grep -v '\.test\.\|\.spec\.' | wc -l
Gates using the vulnerable spelling
scripts/check-dispatcher-error-vocabulary.mjs:152(stripComments, used at 6 call sites)scripts/check-error-code-casing.mjs:130scripts/check-error-status-conformance.mjs:248scripts/check-examples-live-imports.mjs:173scripts/check-test-source-alias.mjs:683scripts/check-platform-checklist.mjs:183
The fix already exists in-tree
scripts/pm/dispatch-gates.mjs exports maskComments — built on a character-class scanner that tracks strings, templates and regex literals, and blanks comment spans instead of deleting them, so line numbers survive. Its docblock was written for the neighbouring failure (comments contributing false hints). The gates above should call it rather than each keeping a private naive copy.
check-error-code-casing.mjs is halfway there already — it blanks rather than deletes, preserving line numbers — but it is still regex-based and equally blind to strings.
What is measured and what is not
Measured: the mechanism, the phantom span on a real repo file, the swallowed-line counts, the 105 triggering files, and that a correct helper exists.
Not measured: whether any of the six gates currently has a live false negative — that needs each gate's scan surface intersected with the triggering files, and then a pattern that actually falls inside a swallowed span. So this is a latent hazard with a proven mechanism, not a demonstrated live miss. Whoever picks it up should measure that intersection first; it also decides whether this is a same-day fix or a ratchet risk.
Worth noting the repair is cheap and uniform (swap the private stripComments for the shared maskComments), and that doing it may raise findings in gates that are currently under-reporting — a widening, not a regression.
Generated by Claude Code
Activity
os-steve commentedon Aug 17, 2026
Triage: lands in
scripts/check-*.mjs(six gate scripts swapping a private naivestripCommentsfor the shared string-awaremaskCommentsfromscripts/pm/dispatch-gates.mjs) ⇒domain:devx(gate tooling, not spec-orbiting). TypeTask,pm:queue.Rationale: proven mechanism (phantom comment span measured on a real file), dangerous failure direction (silent under-reporting by verifiers), cheap uniform repair that already exists in-tree. Not a
finding— the fix scope is named and mechanical; the card itself correctly flags the one open measurement (whether any gate has a live false negative) as the implementer's first step, and that measurement also decides whether the swap surfaces new gate findings (a widening — if red appears, fix the newly-visible defects or ledger them per each gate's convention, ⛔ don't re-blind the gate to stay green).Dispatch note:
maskCommentsblanks spans (line numbers survive) — verify each converted gate still matches its own downstream line-number expectations. Size/model suggestion: M / opus (mechanically uniform, but six gates × their scan-surface assumptions deserve judgment).Generated by Claude Code
claude commentedon Aug 18, 2026
Claim: PM loop round 5
Session:
session_01XqDQYVU5smx29ts9pAErja· Branch:claude/issue-9367-comment-strip-regex· Worktree:objectstack-issue-9367· Domain:domain:devxFile surface:
scripts/check-*.mjs(the six gates named on the card) — stop on breach; explain in the reportContainer & model:
mode:subagent,model: opus—dispatch-gates.mjs --tierreports no path-derived mandate; clause ② not reachable (no contract accept/reject surface).Serial constraints cleared: verified against every in-flight claim this round. ⛔ Not
scripts/docs-audit/affected-docs.mjs— PR #9394 holds that file unlanded. Batch siblings #9305 (content/blog/), #9373 (.github/workflows/), #9382 (content/docs/), #9386 (docs/qa/).origin/mainand assertgit rev-parse HEADequalsorigin/mainbefore measuring. Premises below were checked from a tree asserted equal toorigin/main(51a46a440).Premise ✅:
scripts/check-dispatcher-error-vocabulary.mjspresent.Gate:⚠️ re-derive from your ACTUAL changed paths, this card touches six scripts and the list will be longer than one line.
pnpm check:dispatcher-error-vocabulary(keyed on that script) + whatever the other five gates' own families are —⭐ The verification that matters: a naive comment-strip regex fails on the cases it cannot see. For each gate you touch, prove red-before/green-after on a case the old regex mis-stripped — a string literal containing
//, a regex literal, a URL, a block comment inside a template. ⛔ A green run on today's corpus proves only that today's corpus lacks the shape.Generated by Claude Code
os-steve commentedon Aug 18, 2026
{ "issue": 9367, "status": "done", "branch": "claude/issue-9367-comment-strip-regex", "pr": "https://github.com/objectstack-ai/objectstack/pull/9445", "premise_still_valid": true, "summary": "All six named gates are defective today (none had been fixed), but NOT with one defect. Three (dispatcher-error-vocabulary, error-code-casing, platform-checklist) carry the string-blind naive regex the card describes and fail by BLINDING. The other three already track strings — the card's prose is stale for them — and fail the opposite way: a regex literal whose character class holds a quote character opens a phantom STRING, and because a scanner skips string spans, comments inside one are never removed, so those gates FABRICATE findings out of prose. test-source-alias was closest to correct (it knew regex literals) but decided regex-vs-division from the preceding character alone, missing the `return`/`case` keyword forms. The card's proposed repair does not work as written: scripts/pm/dispatch-gates.mjs runs CLI code at import and calls process.exit(2), so no gate can import maskComments from it — verified by probe. The scanner therefore MOVED to scripts/js-comment-mask.mjs with no import-time side effect, and dispatch-gates re-exports maskComments so its own 260-case self-test still drives the shared masker. Ruling 4 resolved as a genuine factor-once: one scanner, two projections (maskComments blanks, keeping offsets; stripComments deletes but keeps newlines).", "tests": "All commands on the final commit 9083e1c75, gate union run under flock /tmp/os-heavy-verify.lock. GATE UNION (families re-derived from actual changed paths via `node scripts/pm/dispatch-gates.mjs <8 paths>`, which named 6 of them): check:dispatcher-error-vocabulary exit=0 ('12 unregistered code-stamping site(s), all classified'), check:error-code-casing exit=0 ('no lowercase error codes in 4196 scanned file(s)'), check:error-status-conformance exit=0, check:examples-live-imports exit=0 ('0 invisible (declared), 6 inputs-declared, 63 graph-visible'), check:test-source-alias exit=0 ('72 packages with tests scanned'), check:pm-dispatch-gates exit=0 ('260 cases pass'), check:platform-checklist exit=0 ('15 areas, 190 items'), check:nul-bytes exit=0 (6117 files). Plus `node scripts/js-comment-mask.mjs --self-test` = 15 cases pass, each driven through BOTH projections. RED-BEFORE/GREEN-AFTER, end to end, per gate — each planted fixture run once with the new script and once with the original checked out from 51a46a440: error-code-casing OLD exit=0 (reports clean over a real lowercase code) / NEW exit=1; dispatcher-error-vocabulary OLD exit=0 (still '12 sites, all classified') / NEW exit=1; error-status-conformance OLD exit=1 ('RECORD_NOT_FOUND: the runtime can emit HTTP 599') / NEW exit=0; examples-live-imports OLD exit=1 (demands an INVISIBLE_COUPLINGS entry) / NEW exit=0; test-source-alias OLD exit=1 / NEW exit=0; platform-checklist OLD exit=1 ('VARIANTS STALE — ACTION_LOCATIONS') / NEW exit=0. Every OLD failure is manufactured from, or hidden by, a comment. REACHABILITY per gate (each gate's own strip diffed against the shared scanner over that gate's real corpus): dispatcher-error-vocabulary 200 of 1852 files differ, error-code-casing 196 of 4196, examples-live-imports 58 of 2050, error-status-conformance 26 of 1837 — LIVE; platform-checklist 0 of its 8 pinned enum slices and test-source-alias 0 of 34 vitest configs — LATENT (mechanism real, corpus lacks the shape today). A REGRESSION the local run caught and the gate's own green would not have: converting test-source-alias to the blanking projection kept the verdict identical but took it from 6.4s to 5m27s (51x) — its import matcher is lazy and a lazy [\\s\\S]*? dragged across the whitespace blanking leaves is quadratic in comment bytes; the deleting projection restores 6.7s. One self-test case was DISCARDED during construction because the reference masker disagreed with it: a `*` followed by `/` inside a docblock genuinely does close the comment, so that shape was never a defect.", "open_questions": [], "out_of_scope_findings": [ "filed as #9444: scripts/check-org-identifier.mjs (a live lint.yml gate) truncates each line at the first double slash via line.replace(/\\/\\/.*$/, ''), so a URL or slash-bearing string on the same line hides the session.tenantId read it exists to catch — same defect class, seventh gate, reported not fixed per ruling 2; measured but corpus-reachability not intersected. Filed unassigned, no labels, with a Blocked-by line pointing here since the fix should call the masker this PR lands.", "not filed, noted only: `check:platform-checklist` is deliberately NOT a CI gate (lint.yml runs it by maintainer decision, on demand), so this PR's change to that script is covered by the local run above and by nothing in CI." ] }Notes the PM may want that do not fit the schema:
scripts/check-*.mjs(the six gates). The landed diff is 8 files: the six, plus the new sharedscripts/js-comment-mask.mjs, plusscripts/pm/dispatch-gates.mjs(the scanner moved out of it and it re-exportsmaskComments). The move was forced — the helper the card points at cannot be imported from where it lived. No overlap with any in-flight claim:scripts/docs-audit/affected-docs.mjsuntouched (PR feat(docs-audit): anchor a changed @docs-rule block on the expressions it states #9394), and nothing undercontent/blog/,.github/workflows/,content/docs/ordocs/qa/.skip-changesetapplied and read back as["size/l","skip-changeset"]— the write was a union, preserving the bot'ssize/l.git rev-parse HEAD == origin/main(51a46a4), as the claim required.origin/mainhas since moved to a433122; no file in this diff is touched by that drift.--force.Generated by Claude Code
Generated by Claude Code
os-steve commentedon Aug 18, 2026
✅ ACCEPT — PR #9445。⭐ 本轮最硬的一份:卡片说的「一个缺陷、六个门禁」是错的,实际是两个方向相反的缺陷。
1. 卡片的定性被证伪:六个门禁坏法不一样,而且方向相反
我派发时接受了卡片的框架 ——「六个门禁用同一个幼稚正则剥注释」。实测:
dispatcher-error-vocabulary、error-code-casing、platform-checklist)确实是字符串盲的幼稚正则 ⇒ 以致盲方式失败(真缺陷被注释掩盖,门禁绿);test-source-alias最接近正确(它认识正则字面量),但仅凭前一个字符判断 regex-vs-division,漏掉return/case这两种关键字形态。⭐ 致盲和捏造是两个相反的失效方向,合成一句「都用了幼稚正则」会让人以为一次统一替换就够了。
2. 卡片提议的修法做不到,而我独立验证了这一点
卡片让各门禁从
scripts/pm/dispatch-gates.mjs里import { maskComments }。你说不行,因为该文件在 import 时就跑 CLI 并process.exit(2)。我自己拉了main上的该文件核对:全文件 2961 行,
import.meta的出现只有new URL(..., import.meta.url)取路径和文档正文 —— 没有任何 main-guard。⇒ 任何import它的门禁会在 import 阶段被process.exit(2)打死。卡片的修法确实不可行,不是你偷懒。因此扫描器移到无 import 副作用的
scripts/js-comment-mask.mjs,dispatch-gates再 re-export,使它自己那 260 条自测仍然驱动同一个 masker —— 裁决 4 的「factor once」按真实的一次分解落地:一个扫描器,两种投影(maskComments涂白保偏移 /stripComments删除保换行)。3. 逐门禁 RED-BEFORE / GREEN-AFTER,而且两个方向都跑了
每个门禁各植一个 fixture,用新脚本跑一遍、再用从
51a46a440检出的原始脚本跑一遍:error-code-casingdispatcher-error-vocabularyerror-status-conformanceRECORD_NOT_FOUND: 可以发 HTTP 599)examples-live-importstest-source-aliasplatform-checklistVARIANTS STALE — ACTION_LOCATIONS)每一条 OLD 失败都是由注释制造或掩盖的。
4. 可达性分开报,latent 没有冒充 live ⭐
把每个门禁自己的 strip 与共享扫描器在该门禁真实语料上逐文件对比:
⭐ 两个 LATENT 明说「机制为真,但今天的语料里没有这个形状」,没有混进 live 里凑数。这正是我要的 —— 修一个今天没人撞到的洞是对的,但不能把它说成今天在流血。
5. 你自己抓到一个门禁绿不出来的回归 ⭐⭐
把
test-source-alias改成涂白投影后,判定完全不变,但耗时从 6.4s → 5m27s(51×) —— 它的 import matcher 是惰性的,而惰性[\s\S]*?拖过涂白留下的空白是注释字节数的二次。改回删除投影恢复 6.7s。这是本 PR 最有价值的一条。 门禁自己永远绿,CI 也不会因为慢 51 倍而失败 —— 它只会让每个人的每一次运行都变慢,直到有人某天去查。你在本地跑了才看见。
6. 一条被丢弃的自测用例,理由正确
构造时丢掉一条:docblock 里
*紧跟/确实闭合注释,所以那个形状从来不是缺陷。⇒ 参考实现与用例冲突时你去查了哪个对,而不是改实现迁就用例。7. 外溢卡
#9444 →
domain:devx+pm:queue+pm:blocked(卡上已有Blocked-by: #9367)。scripts/check-org-identifier.mjs用line.replace(/\/\/.*$/, '')在第一个双斜杠处截断 —— URL 或含斜杠的字符串会把它要抓的session.tenantId读藏起来。同族第七个,按裁决 2 只报不修,正确。它的修法就是调用本 PR 落地的 masker,所以必须排在本 PR 之后。ⓘ 另记录、无需动作:
check:platform-checklist不是 CI 门禁(按维护者决定由lint.yml按需跑),所以本 PR 对该脚本的改动由你的本地运行覆盖,CI 不覆盖。这条要写在这里,免得后来人看着 CI 全绿以为它被验过了。Verdict: ACCEPT. 门禁收敛后武装,不手动合并。
Generated by Claude Code
fix(scripts): check-org-identifier takes the shared string-aware comm…
test(cli): separate code from prose with the shared masker, and decla…
refactor(gates): use the repo's shared js-comment-mask instead of a p…
fix(scripts): read the fatal-guard adoption check's source as CODE, a…
fix(scripts): check-org-identifier takes the shared string-aware comm…
fix(gates): give the in-process ESLint ratchets the parser stack head…
test(cli): put the serve↔bootStack parity scan on the shared comment …
docs(plugin-audit): document the `os serve` opt-in, and rule out a co…
fix(scripts): read the fatal-guard adoption check's source as CODE, a…
Convert both canonical-envelope page gates onto the shared comment mask
Convert both canonical-envelope page gates onto the shared comment ma…