chore(lint): make lint:pkg gate ANSI-invariant + declare sideEffects:false — closes queue #63 + #70 (supersedes #88)#101
Conversation
…I — queue #63 The lint:pkg wrapper captures publint stdout and matches PUBLINT_BLOCK_RE (/^(Suggestions|Warnings|Errors):$/m) to fail the gate on any advisory. In CI, publint colorizes its block headers (color-capable environment), emitting e.g. "\x1b[1m\x1b[34mSuggestions:\x1b[39m\x1b[22m". The leading SGR codes mean the line is not a bare "Suggestions:", so the regex never matched and the gate silently no-op'd — a CI false-NEGATIVE in which a genuine Warning/Error block would sail through undetected. Verified against raw CI logs: the gate had been a no-op in CI since publint 0.3.21 landed 2026-05-11; locally (plain-text, non-TTY) the regex matched and the gate fired correctly. Fix (belt-and-suspenders): spawn publint with NO_COLOR=1 AND strip residual ANSI SGR codes from captured stdout before the regex match, so the verdict is identical in every color environment (plain, TTY, FORCE_COLOR=1). ANSI_RE is built via String.fromCharCode(27) + RegExp() to avoid a control-char literal that oxlint's no-control-regex (Correctness) would reject. Header comment updated to record the verified CI-color root cause. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds "sideEffects": false immediately after "type": "module" in every packages/*/package.json. Clears the publint 0.3.21 Suggestion ("package appears to be consumed by bundlers but does not specify sideEffects") that the now-honest Gate 6 (queue #63 fix) flags as fatal, and lets consumers tree-shake under deep imports. Per-package side-effect audit re-run on current main (post-#87 streamRequest removal from fs-http): every package's src/index.ts is a pure re-export or a file whose top-level statements are imports, type declarations, and const/function factory declarations. console.*/document.*/window.*/ Object.defineProperty and the dialog body-scroll write are all inside function bodies (call-time, not module-evaluation). cached-adapter-store's top-level `new WeakMap()` is pure construction. No bare side-effect imports anywhere. All 11 cleared. CLAUDE.md gains a "No top-level side effects" Conventions bullet locking the convention so a future package author doesn't ship without the flag and re-fire Gate 6. Closes enforcement queue #70. Supersedes PR #88. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploying fs-packages with
|
| Latest commit: |
7edd5da
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2bbae632.fs-packages.pages.dev |
| Branch Preview URL: | https://armorer-queue-63-70-lintpkg.fs-packages.pages.dev |
PR Reviewer · claimed
|
Goosterhof
left a comment
There was a problem hiding this comment.
No blockers. One concern, one nit, and two load-bearing decisions worth naming. This is a clean chore PR with an honestly-stated root cause; verdict is COMMENT, merge-ready once the concern is acknowledged.
I reproduced the central claim before writing this. The fix is correct: a colored CI header \x1b[1m\x1b[34mSuggestions:\x1b[39m\x1b[22m does not match the bare-line PUBLINT_BLOCK_RE, and stripAnsi() restores the bare Suggestions: line so the regex fires. NO_COLOR=1 on the spawn plus the stripAnsi() pass on captured stdout is genuine belt-and-suspenders — verdict identical across plain, TTY, and FORCE_COLOR=1. The diagnosis in the PR body — a CI false-NEGATIVE, not a red gate — matches the code: pre-fix, a real Warnings:/Errors: block would have sailed through CI undetected.
Concern
The sideEffects: false declaration is a runtime contract the gate still does not verify. The eleven manifest additions clear publint's Suggestion, and the now-honest lint:pkg gate will fail if the flag is missing — but nothing checks that the packages are actually side-effect-free. The flag is a promise to bundlers; if a future package author adds a bare top-level import './register-globals' or a module-eval console.warn, the manifest still says false, the bundler tree-shakes it away, and the consumer silently loses the effect. The per-package audit in the PR body is thorough and I spot-checked fs-cached-adapter-store (packages/cached-adapter-store/src/index.ts on main — the new WeakMap() is pure construction, no bare effect lines), but that audit is a point-in-time human pass, not an enforced invariant. The new CLAUDE.md bullet (CLAUDE.md:57) is Level 4 doctrine; the real enforcement gap is Level 1/2. This is in-scope to flag, out-of-scope to fix here — but it should land on the enforcement queue as a follow-up (an arch test asserting every src/index.ts has no module-eval side effects), otherwise queue #70 is closed at the manifest layer while the underlying contract stays unguarded.
Nit
scripts/lint-pkg.mjs:46 — runCaptured's new extraEnv parameter is spread as {...process.env, ...extraEnv} only when truthy, else passes process.env directly. Correct, but the ternary means two callers now share a function whose env behavior is positional-arg-dependent. A one-line doc comment on the parameter (// extraEnv: merged over process.env for the child only) would save the next reader a double-take. Optional.
Praise
Two decisions earned recognition here. First, the ANSI_RE construction via new RegExp(\${String.fromCharCode(27)}...`) instead of a control-char literal (scripts/lint-pkg.mjs:43-48) is the non-obvious move — a literal \x1bin the regex source would trip oxlint'sno-control-regex (Correctness), and the comment cites exactly that. Second, the header comment rewrite (scripts/lint-pkg.mjs:13-25) records the verified root cause with the actual SGR bytes and the "no-op since publint 0.3.21 landed 2026-05-11" timeline — that is the kind of provenance that stops the next maintainer from "simplifying" the NO_COLOR=1-plus-stripAnsi` redundancy back into a single-defense regression.
jasperboerhof
left a comment
There was a problem hiding this comment.
Approved — reviewed, no blockers.
Promote the package-global `sideEffects:false` manifest claim (landed by queue #70 / PR #101) from an unenforced Level-4/Level-6 promise to a Level-1 CI gate. `scripts/lint-pkg.mjs` gains a third per-package check alongside publint/attw (#33/#63) and engines.node (#31): every package source file under `packages/*/src/**/*.ts` (test files excluded) is parsed with the TypeScript compiler API (already a devDep — no new dependency) and its top-level statement list is asserted side-effect-free. CI fails on any bare top-level ExpressionStatement (call / assignment), specifier-less side-effect import, top-level control-flow statement, or `export default` of an evaluated expression. Scope is all source modules, not just `index.ts` and its re-exports — the correct match for a package-global flag (a side effect in a non-re-exported imported module is still covered by the bundler's tree-shaking assumption). Keeps the change out of `packages/*/src/` so coverage Gate 7 and mutation Gate 8 stay untouched. CLAUDE.md "No top-level side effects" bullet updated to cite the now-enforcing gate (doctrine propagation). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What & why
Two coupled fixes that make Gate 6 (
lint:pkg) actually enforce in CI and clear the only standing publint Suggestion.Verified ground truth (the polarity matters)
The
lint:pkgwrapper (scripts/lint-pkg.mjs) runspublint runper package with piped stdout, captures it, matchesPUBLINT_BLOCK_RE = /^(Suggestions|Warnings|Errors):$/m, and fails the gate on a match.\x1b[1m\x1b[34mSuggestions:\x1b[39m\x1b[22m. The leading SGR codes mean the line is not a bareSuggestions:, so the regex never matched → no failure pushed → the gate silently no-op'd. Verified against raw CI logs: this has been a no-op in CI since publint 0.3.21 landed 2026-05-11 — the 2026-05-18 and 2026-05-29mainruns both passedlint:pkgwhile publint printed the Suggestions block.So the real defect is a CI false-NEGATIVE: a genuine publint Warning/Error (e.g. a broken
exportsmap) would sail through CI undetected today. This PR does not "restore Gate 6 to green on main" — Gate 6 was never red in CI. It makes Gate 6 actually enforce in CI and clears the standingsideEffectsSuggestion.(For the record: PRs #86/#87 were red on the
npm auditstep, not Gate 6.)Changes
scripts/lint-pkg.mjs— ANSI invariance (closes queue fs-router 0.1.0 published peer-dep is stale (vue-router ^4.5.0 vs source ^5.0.6) — republish as 0.1.1 #63). Spawn publint withNO_COLOR=1and strip residual ANSI SGR codes from captured stdout before the regex match (belt-and-suspenders, verdict identical in every color environment).ANSI_REis built viaString.fromCharCode(27)+RegExp()to avoid a control-char literal that oxlint'sno-control-regex(Correctness) would reject. Header comment rewritten to record the verified CI-color root cause.packages/*/package.json—"sideEffects": false(closes queue fs-dialog: forward host <dialog> ARIA attributes via dialog.open() options (#67) #70). Added immediately after"type": "module". Clears the publint 0.3.21 Suggestion the now-honest gate flags as fatal; lets consumers tree-shake under deep imports.CLAUDE.md— "No top-level side effects" Conventions bullet locking the convention so a future package author doesn't ship without the flag and re-fire Gate 6.Load-bearing proof the no-op is fixed
npm run lint:pkg(plain), all packages havesideEffectsFORCE_COLOR=1 npm run lint:pkg, all packages havesideEffectsnpm run lint:pkg(plain), one package'ssideEffectsremovedSuggestions:)FORCE_COLOR=1 npm run lint:pkg, one package'ssideEffectsremovedThe bottom-right cell is the whole point: pre-fix, the colored run let the Suggestion through; post-fix it fails. Captured directly —
FORCE_COLOR=1 npx publint runemits\033[1m\033[34mSuggestions:\033[39m\033[22m(verified viaod -c), exactly the ANSI wrap that defeated the bare-line regex.Per-package side-effect audit (re-run on current
main, post-#87 streamRequest removal)Every package's
src/index.tsis a pure re-export or a file whose top-level statements are imports, type declarations, andconst/functionfactory declarations. All flaggedconsole.*/document.*/window.*/Object.defineProperty/ dialog body-scroll writes are inside function bodies (call-time, not module-evaluation). No bare side-effect imports anywhere.new WeakMap()is pure constructiondocument.bodywrite is call-time insideopen()DEFAULT_TIMEOUT_MSliteral + factory consts (streamRequest gone in 0.4.0)console.erroris call-time inside service methodsdocument/windowreads are call-timedefineComponentsetupgetCacheKeyhelper; all pure declarationsVerification — full 8-gate gauntlet, local
npm auditnpm run format:checknpm run lintnpm run buildnpm run typechecknpm run lint:pkgnpm run test:coveragenpm run test:mutationSupersedes #88
Folds PR #88's
sideEffects: falsemanifest changes + CLAUDE.md doctrine bullet into this PR, and adds the queue #63 ANSI fix that #88 was missing. #88 can be closed as superseded.Closes enforcement queue #63 + #70.