Skip to content

chore(lint): make lint:pkg gate ANSI-invariant + declare sideEffects:false — closes queue #63 + #70 (supersedes #88)#101

Merged
Goosterhof merged 3 commits into
mainfrom
armorer/queue-63-70-lintpkg-honest-gate
Jun 1, 2026
Merged

chore(lint): make lint:pkg gate ANSI-invariant + declare sideEffects:false — closes queue #63 + #70 (supersedes #88)#101
Goosterhof merged 3 commits into
mainfrom
armorer/queue-63-70-lintpkg-honest-gate

Conversation

@Goosterhof
Copy link
Copy Markdown
Contributor

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:pkg wrapper (scripts/lint-pkg.mjs) runs publint run per package with piped stdout, captures it, matches PUBLINT_BLOCK_RE = /^(Suggestions|Warnings|Errors):$/m, and fails the gate on a match.

  • In CI publint detects a color-capable environment and emits ANSI-wrapped headers, 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 → 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-29 main runs both passed lint:pkg while publint printed the Suggestions block.
  • Locally (plain-text, non-TTY) publint emits no color → the regex matched → the gate failed correctly. That local "failure" was the gate working, not a false positive.

So the real defect is a CI false-NEGATIVE: a genuine publint Warning/Error (e.g. a broken exports map) 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 standing sideEffects Suggestion.

(For the record: PRs #86/#87 were red on the npm audit step, not Gate 6.)

Changes

  1. 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 with NO_COLOR=1 and strip residual ANSI SGR codes from captured stdout before the regex match (belt-and-suspenders, verdict identical in every color environment). 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 rewritten to record the verified CI-color root cause.
  2. 11 × 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.
  3. 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

Run Pre-fix behaviour Post-fix (this PR)
npm run lint:pkg (plain), all packages have sideEffects PASS PASS (exit 0)
FORCE_COLOR=1 npm run lint:pkg, all packages have sideEffects PASS PASS (exit 0)
npm run lint:pkg (plain), one package's sideEffects removed FAIL (regex saw plain Suggestions:) FAIL (exit 1)
FORCE_COLOR=1 npm run lint:pkg, one package's sideEffects removed false-PASS (ANSI defeated the regex) FAIL (exit 1)

The 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 run emits \033[1m\033[34mSuggestions:\033[39m\033[22m (verified via od -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.ts is a pure re-export or a file whose top-level statements are imports, type declarations, and const/function factory declarations. All flagged console.* / document.* / window.* / Object.defineProperty / dialog body-scroll writes are inside function bodies (call-time, not module-evaluation). No bare side-effect imports anywhere.

Package Top-level verdict
fs-adapter-store pure re-exports + types
fs-cached-adapter-store re-exports; top-level new WeakMap() is pure construction
fs-dialog inline factory; document.body write is call-time inside open()
fs-helpers pure re-exports
fs-http imports + DEFAULT_TIMEOUT_MS literal + factory consts (streamRequest gone in 0.4.0)
fs-loading pure re-exports
fs-router pure re-exports + page-name consts
fs-storage pure re-exports; console.error is call-time inside service methods
fs-theme pure re-exports; document/window reads are call-time
fs-toast inline factory; Popover calls are call-time inside defineComponent setup
fs-translation inline factory + getCacheKey helper; all pure declarations

Verification — full 8-gate gauntlet, local

Gate Result
npm audit PASS (0 vulnerabilities)
npm run format:check PASS (145 files)
npm run lint PASS (0 warnings / 0 errors, 92 files)
npm run build PASS (11 packages, dual ESM+CJS)
npm run typecheck PASS
npm run lint:pkg PASS — and now color-invariant
npm run test:coverage PASS (520/520 tests, 100% per-package)
npm run test:mutation PASS (all 11 packages ≥ 90%: 100/94.81/97.18/100/96.23/92.50/91.20/92.73/96.67/98.36/93.33)

Supersedes #88

Folds PR #88's sideEffects: false manifest 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.

Goosterhof and others added 2 commits May 29, 2026 12:20
…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>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 29, 2026

Deploying fs-packages with  Cloudflare Pages  Cloudflare Pages

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

View logs

@jasperboerhof
Copy link
Copy Markdown
Contributor

PR Reviewer · claimed

@Goosterhof Goosterhof added the Agent Review Requested Requesting review of specialized AI review agents. label May 29, 2026
Copy link
Copy Markdown
Contributor Author

@Goosterhof Goosterhof left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:46runCaptured'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.

Copy link
Copy Markdown
Contributor

@jasperboerhof jasperboerhof left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved — reviewed, no blockers.

@Goosterhof Goosterhof merged commit a79edfd into main Jun 1, 2026
2 checks passed
@Goosterhof Goosterhof deleted the armorer/queue-63-70-lintpkg-honest-gate branch June 1, 2026 11:00
Goosterhof added a commit that referenced this pull request Jun 2, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Agent Review Requested Requesting review of specialized AI review agents.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants