Files
msd-core/eslint.config.mjs
Tom Boucher d16a66479a feat(#1950): broken-windows ledger — cross-phase defect register gating ship (#2441)
* feat(#1950): broken-windows ledger — cross-phase defect register gating ship

Adds a new  capability (#1950) that operationalizes GSD's
no-defer discipline as a tracked, enforced artifact:
accumulates stubs, TODOs, skipped tests, unrun verifies, and unmet truths
across phases, and /gsd-ship blocks while any entry is open.

Implementation:
- src/broken-windows.cts → gsd-core/bin/lib/broken-windows.cjs: typed IR +
  I/O entry points (parseLedger/renderLedger/appendWindow/markWaived/markFixed
  + cmdWindowsStatus/Append/Waive/MarkFixed). Frozen REASON enum for typed
  error assertions. Windows-safe atomic rename with retry on transient
  EPERM/EBUSY/EACCES.
- gsd-tools.cjs: new  subcommand (status | append | waive | fixed),
  wired via routeWindows + HOST_COMMAND_ROUTERS.windows.
- capabilities/broken-windows/capability.json: one ship:pre gate with
  artifact-frontmatter-equals predicate on WINDOWS.md open_count == 0.
  activationKey windows.enabled (default true) + sibling windows.enforce
  (default true, separate so tracking can precede enforcement).
- gsd-core/workflows/ship.md: capId==broken-windows branch in preflight,
  sibling to security — reads gsd_run windows status --raw, fails closed
  on open_count > 0 or unreadable ledger.
- agents/gsd-executor.md: extends the existing ## Known Stubs instruction
  to also append to WINDOWS.md via gsd_run windows append (best-effort,
  never blocks execution).
- agents/gsd-verifier.md: new Step 8b — record unmet truths + human-verify
  items in WINDOWS.md.
- gsd-core/workflows/progress.md: surfaces open + waived counts.
- docs/COMMANDS.md + CONTEXT.md glossary entry + docs/INVENTORY.md:
  document the gate, waiver mechanism, and new module.
- tests/broken-windows.test.cjs: pure + CLI behavioral coverage + fast-check
  roundtrip property; fail-closed on malformed ledger; security boundary on
  path traversal in --file.

Backward-compatible: a project with no .planning/WINDOWS.md reports
open_count: 0 and ships cleanly. Disable enforcement per-project with
gsd config-set windows.enforce false (tracking continues, gate stays open).

* chore(#1950): ratchet size baselines, defer verifier integration

- Workflow size baseline: ship.md 25575→27928, progress.md 31789→32632
  (broken-windows preflight branch + open-windows surface).
- Agent size baseline: gsd-executor.md 46644→47951 (Known Stubs → also
  appends to WINDOWS.md). gsd-verifier.md unchanged.
- LARGE_CAP (49152) preempted the planned verifier integration
  (gsd-verifier.md was at 49140 pre-PR — 12 bytes of headroom, not the
  documented 'real headroom'). Verifier integration deferred to a follow-up
  PR that extracts the VERIFICATION.md template (lines 739-859) to
  gsd-core/references/ — a pre-existing cap-tightness defect this PR
  exposed but does not expand scope to fix. Verifier integration is not in
  the issue's acceptance criteria (executor writes is; unmet-truths
  recording was an enhancement, not a gate).

* fix(#1950): gate default-off, rename to workflow.windows_enforce, regen goldens

Test-failure-driven fixes after first gsd-test run on db8733c8f failed 44
cases (pre-existing structural tests encoded 'ship:pre has 1 gate' / 'all
caps off → empty hooks'):

- capability manifest: rename windows.enabled+windows.enforce (default
  true) → single federated key workflow.windows_enforce (default FALSE,
  opt-in). Matches security's workflow.security_enforce convention and
  makes the adr857 all-caps-off test pass without modification (the test's
  buildAllFalseConfig handles workflow.* out of the box). Default-OFF keeps
  the gate out of the registry's default ship:pre resolution so existing
  loop-hooks-ship-pre-e2e structural assertions (exactly 1 gate, capId
  'security') stay valid; users opt in via
  gsd config-set workflow.windows_enforce true.
- drop activationKey (security doesn't have one either; workflow.* key
  doubles as the activation toggle).
- regenerate docs/reference/capability-matrix.md to include broken-windows
  (capability-matrix-sync test).
- regenerate tests/fixtures/golden-install-parity/*.json (18 runtimes) —
  installer now emits the new capability + lib file.
- update CONTEXT.md, docs/COMMANDS.md, docs/FEATURES.md, ship.md,
  agents/gsd-executor.md to use the new key name and /gsd:colon slash
  syntax (slash-command-namespace test).
- restore accidentally-regressed /gsd:capture in progress.md.

Tracking-only by default; enforcement is opt-in. Acceptance criterion
'/gsd-ship fails while any ledger entry is open' is met when
workflow.windows_enforce=true (test fixture enables it).

* test(#1950): update ship:pre structural invariants for 2-gate registry

- loop-hooks-ship-pre-e2e: the registry now declares 2 gates at ship:pre
  (security + broken-windows), regardless of activation. Activation tests
  above still pin security-only or empty behavior via fixtures; these
  structural tests pin the REGISTRY shape, which has 2 gates as of #1950.
- workflow-size-baseline: ship.md 27928→27945 (workflow.windows_enforce
  rename added 17 bytes).

* fix(#1950): review H1+H2+M1+M2+M3 — fence-injection, EACCES fail-closed, cleanup, strict line, stryker

Adversarial isolated review (Step 6.3) found 2 HIGH findings that block
the PR and 3 mediums. All addressed:

H1 (HIGH): description containing the markdown 3-backtick fence would
terminate the ledger's JSON code block early inside JSON.stringify output
(JSON doesn't escape backticks), corrupting the file and bricking the
next parse. Fix: use a 4-backtick fence (json ... ) which
JSON.stringify cannot produce on its own, AND validate that no entry
text field contains a 4-backtick run (reject at append time with new
WINDOWS_INVALID_TEXT reason code). Locked by a regression test.

H2 (HIGH): readLedgerOrNull swallowed ALL fs errors as 'no ledger',
silently returning open_count:0 on EACCES/EPERM/EIO. The ship gate
would then pass on an unreadable ledger — the precise vector the
workflow doc claims is impossible. Fix: only ENOENT returns null;
every other fs error propagates as WINDOWS_LEDGER_MALFORMED so the
gate blocks and the operator sees a real diagnostic. Locked by a
regression test that chmod 000s a ledger with open_count=1 and
asserts the result is never a false-green 0.

M1: writeLedgerAtomic left an orphaned .tmp file on rename failure.
Wrapped renameWithRetry in try/catch with best-effort unlink.

M2: validateLine silently coerced 'abc' → NaN → null, hiding type
drift. Removed the line === 0 special case (was undocumented) and
made the error message match the strict check. Now any non-positive-
integer line value throws, including strings.

M3: tests/broken-windows.test.cjs (with its fast-check property test)
was not in stryker.config.mjs DEFAULT_TEST_CMD — Stryker would mutate
src/broken-windows.cts but no test would catch the mutations,
producing false surviving-mutant scores. Added to the list.

L1 (dead throw e after error()), L7 (line boundary tests, H1/H2
regression tests, 4-backtick CLI test) also addressed.

* docs(#1950): inline concurrency + busy-wait notes (review L2+L3)

* fix(#1950): regen goldens against latest gsd-tools; correct --line 0 boundary test

gsd-test v4 caught two issues:
- goldens I regenerated earlier (commit 526682084) predated the L1
  routeWindows catch-block cleanup (commit dd844d565). Regenerated
  via 'npm run gen:golden' against current HEAD so the install
  parity hash for gsd-tools.cjs matches.
- 'append --line boundary' test expected --line 0 to succeed with
  null entry.line, but the M2 fix correctly rejects 0 (lines are
  1-indexed; 0 is not a valid source line). Updated the boundary
  test to assert --line 0 fails alongside -1 and 'abc'.

* chore(#1950): regen goldens after rebase onto next

* chore(#1950): quick.md baseline 50699→50993 (correct resolution from next rebase)

* chore(changeset): backfill pr:2441 in .changeset/broken-windows-ledger.md

* fix(#1950): renderTable escapes backslash before pipe (CodeQL incomplete-sanitization)

CodeQL flagged the markdown-table cell escaper:
  String(s ?? '').replace(/\|/g, '\\|')
— it escapes pipe but not backslash first. A description containing '\|'
would render as '\\|' which markdown parses as 'literal backslash' +
'cell separator', splitting the column.

Fix: escape backslash FIRST (each \ → \\), then pipe (each | → \|).
Now a description with '\|' renders as '\\\\|' (literal '\\' + escaped
pipe), which markdown renders as a single '\|' inside the cell. The JSON
code block (the parse source-of-truth) was already correctly escaped via
JSON.stringify; only the display-only table was affected.

Locked by a regression test that:
1. Verifies the JSON block reparses with the description intact.
2. Walks the rendered table row counting unescaped pipes — must be
   exactly 11 (the row separators for 10 cells), proving no in-cell
   pipe added a split.
2026-07-19 20:24:21 -04:00

423 lines
20 KiB
JavaScript

import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import globals from 'globals';
import pluginN from 'eslint-plugin-n';
import noOnlyTests from 'eslint-plugin-no-only-tests';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
// Local plugin with custom AST rules
import noSourceGrep from './eslint-rules/no-source-grep.cjs';
import noMagicSleepInTests from './eslint-rules/no-magic-sleep-in-tests.cjs';
import noElapsedAssertion from './eslint-rules/no-elapsed-assertion.cjs';
import noRawRmsyncInTests from './eslint-rules/no-raw-rmsync-in-tests.cjs';
import noTautologicalAssert from './eslint-rules/no-tautological-assert.cjs';
import noAdhocMarkdownParsing from './eslint-rules/no-adhoc-markdown-parsing.cjs';
import noPathLiteralInAssert from './eslint-rules/no-path-literal-in-assert.cjs';
import noPosixModeBitAssert from './eslint-rules/no-posix-mode-bit-assert.cjs';
import noUnguardedNonportableExec from './eslint-rules/no-unguarded-nonportable-exec.cjs';
import noCrlfFragileSplit from './eslint-rules/no-crlf-fragile-split.cjs';
import noHardcodedTmp from './eslint-rules/no-hardcoded-tmp.cjs';
import noBareNpmExec from './eslint-rules/no-bare-npm-exec.cjs';
import requireUserprofileWithHome from './eslint-rules/require-userprofile-with-home.cjs';
import normalizePathInContent from './eslint-rules/normalize-path-in-content.cjs';
import requireFsOpFallback from './eslint-rules/require-fs-op-fallback.cjs';
const localPlugin = {
rules: {
'no-source-grep': noSourceGrep,
'no-magic-sleep-in-tests': noMagicSleepInTests,
'no-elapsed-assertion': noElapsedAssertion,
'no-raw-rmsync-in-tests': noRawRmsyncInTests,
'no-tautological-assert': noTautologicalAssert,
'no-adhoc-markdown-parsing': noAdhocMarkdownParsing,
'no-path-literal-in-assert': noPathLiteralInAssert,
'no-posix-mode-bit-assert': noPosixModeBitAssert,
'no-unguarded-nonportable-exec': noUnguardedNonportableExec,
'no-crlf-fragile-split': noCrlfFragileSplit,
'no-hardcoded-tmp': noHardcodedTmp,
'no-bare-npm-exec': noBareNpmExec,
'require-userprofile-with-home': requireUserprofileWithHome,
'normalize-path-in-content': normalizePathInContent,
'require-fs-op-fallback': requireFsOpFallback,
},
};
export default tseslint.config(
// ── Global ignores ─────────────────────────────────────────────────────────
{
ignores: [
'node_modules/**',
'**/dist/**',
'.worktrees/**',
'.claude/**',
'coverage/**',
'**/*.generated.cjs',
// ADR-457: tsc-generated runtime artifact — lint the src/*.cts source, not the emitted .cjs.
'gsd-core/bin/lib/claude-orchestration.cjs',
'gsd-core/bin/lib/claude-orchestration-command-router.cjs',
'gsd-core/bin/lib/semver-compare.cjs',
'gsd-core/bin/lib/host-integration.cjs',
'gsd-core/bin/lib/handshake-serialized.cjs',
'gsd-core/bin/lib/host-integration-sdk.cjs',
'gsd-core/bin/lib/install-effort-resolver.cjs',
'gsd-core/bin/lib/install-engine.cjs',
'gsd-core/bin/lib/capability-loader.cjs',
'gsd-core/bin/lib/capability-source.cjs',
'gsd-core/bin/lib/capability-ledger.cjs',
'gsd-core/bin/lib/capability-trust.cjs',
'gsd-core/bin/lib/capability-lifecycle.cjs',
'gsd-core/bin/lib/capability-consent.cjs',
'gsd-core/bin/lib/capability-lock.cjs',
'gsd-core/bin/lib/resolution.cjs',
'gsd-core/bin/lib/plan-drift-guard.cjs',
'gsd-core/bin/lib/cli-exit.cjs',
'gsd-core/bin/lib/external-job.cjs',
'gsd-core/bin/lib/edge-probe.cjs',
'gsd-core/bin/lib/probe-core.cjs',
'gsd-core/bin/lib/spec-section.cjs',
'gsd-core/bin/lib/prohibition-enforcement.cjs',
'gsd-core/bin/lib/ui-consideration-probe.cjs',
'gsd-core/bin/lib/code-review-flags.cjs',
'gsd-core/bin/lib/context-utilization.cjs',
'gsd-core/bin/lib/broken-windows.cjs',
'gsd-core/bin/lib/api-coverage.cjs',
'gsd-core/bin/lib/artifacts.cjs',
'gsd-core/bin/lib/assumption-delta.cjs',
'gsd-core/bin/lib/state-transition.cjs',
'gsd-core/bin/lib/command-arg-projection.cjs',
'gsd-core/bin/lib/clock.cjs',
'gsd-core/bin/lib/ui-safety-gate.cjs',
'gsd-core/bin/lib/review-reviewer-selection.cjs',
'gsd-core/bin/lib/clusters.cjs',
'gsd-core/bin/lib/installer-migrations/001-legacy-orphan-files.cjs',
'gsd-core/bin/lib/observability/redaction.cjs',
'gsd-core/bin/lib/installer-migration-report.cjs',
'gsd-core/bin/lib/prompt-budget.cjs',
'gsd-core/bin/lib/secrets.cjs',
'gsd-core/bin/lib/smart-entry.cjs',
'gsd-core/bin/lib/phase-lifecycle.cjs',
'gsd-core/bin/lib/workstream-name-policy.cjs',
'gsd-core/bin/lib/decisions.cjs',
'gsd-core/bin/lib/validate.cjs',
'gsd-core/bin/lib/schema-detect.cjs',
'gsd-core/bin/lib/runtime-name-policy.cjs',
'gsd-core/bin/lib/runtime-slash.cjs',
'gsd-core/bin/lib/observability/event.cjs',
'gsd-core/bin/lib/workstream-inventory-builder.cjs',
'gsd-core/bin/lib/plan-scan.cjs',
'gsd-core/bin/lib/fallow-runner.cjs',
'gsd-core/bin/lib/project-root.cjs',
'gsd-core/bin/lib/installer-migration-authoring.cjs',
'gsd-core/bin/lib/update-context.cjs',
'gsd-core/bin/lib/installer-migrations/000-first-time-baseline.cjs',
'gsd-core/bin/lib/runtime-homes.cjs',
'gsd-core/bin/lib/model-catalog.cjs',
'gsd-core/bin/lib/configuration.cjs',
'gsd-core/bin/lib/state-document.cjs',
'gsd-core/bin/lib/shell-command-projection.cjs',
'gsd-core/bin/lib/security.cjs',
'gsd-core/bin/lib/command-aliases.cjs',
'gsd-core/bin/lib/config-schema.cjs',
'gsd-core/bin/lib/model-profiles.cjs',
'gsd-core/bin/lib/model-resolver.cjs',
'gsd-core/bin/lib/loop-resolver.cjs',
'gsd-core/bin/lib/capability-state.cjs',
'gsd-core/bin/lib/capability-activation.cjs',
'gsd-core/bin/lib/federated-config.cjs',
'gsd-core/bin/lib/installer-migrations/002-codex-legacy-hooks-json.cjs',
'gsd-core/bin/lib/installer-migrations/003-rename-get-shit-done-to-gsd-core.cjs',
'gsd-core/bin/lib/installer-migrations/004-prune-stale-pristine-snapshots.cjs',
'gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs',
'gsd-core/bin/lib/observability/logger.cjs',
'gsd-core/bin/lib/active-workstream-store.cjs',
'gsd-core/bin/lib/adr-parser.cjs',
'gsd-core/bin/lib/graphify.cjs',
'gsd-core/bin/lib/graphify-command-router.cjs',
'gsd-core/bin/lib/audit-command-router.cjs',
'gsd-core/bin/lib/intel-command-router.cjs',
'gsd-core/bin/lib/install-profiles.cjs',
'gsd-core/bin/lib/intel.cjs',
'gsd-core/bin/lib/installer-migrations.cjs',
'gsd-core/bin/lib/worktree-safety.cjs',
'gsd-core/bin/lib/worktree-base-ref.cjs',
'gsd-core/bin/lib/planning-workspace.cjs',
'gsd-core/bin/lib/command-roster.cjs',
'gsd-core/bin/lib/runtime-artifact-conversion.cjs',
'gsd-core/bin/lib/runtime-artifact-install-plan.cjs',
'gsd-core/bin/lib/runtime-artifact-layout.cjs',
'gsd-core/bin/lib/runtime-config-adapter-registry.cjs',
'gsd-core/bin/lib/runtime-hooks-surface.cjs',
'gsd-core/bin/lib/command-routing-hub.cjs',
'gsd-core/bin/lib/core-utils.cjs',
'gsd-core/bin/lib/io.cjs',
'gsd-core/bin/lib/phase-id.cjs',
'gsd-core/bin/lib/normalize-test-command.cjs',
'gsd-core/bin/lib/config-loader.cjs',
'gsd-core/bin/lib/phase-locator.cjs',
'gsd-core/bin/lib/roadmap-parser.cjs',
'gsd-core/bin/lib/drift.cjs',
'gsd-core/bin/lib/cjs-command-router-adapter.cjs',
'gsd-core/bin/lib/phase-command-router.cjs',
'gsd-core/bin/lib/surface.cjs',
'gsd-core/bin/lib/roadmap-upgrade.cjs',
'gsd-core/bin/lib/config-types.cjs',
'gsd-core/bin/lib/phases-command-router.cjs',
'gsd-core/bin/lib/verify-command-router.cjs',
'gsd-core/bin/lib/verification.cjs',
'gsd-core/bin/lib/verification-command-router.cjs',
'gsd-core/bin/lib/eval.cjs',
'gsd-core/bin/lib/eval-command-router.cjs',
'gsd-core/bin/lib/init-command-router.cjs',
'gsd-core/bin/lib/onboard-projection.cjs',
'gsd-core/bin/lib/agent-command-router.cjs',
'gsd-core/bin/lib/agent-install-check.cjs',
'gsd-core/bin/lib/task-command-router.cjs',
'gsd-core/bin/lib/validate-command-router.cjs',
'gsd-core/bin/lib/workstream-inventory.cjs',
'gsd-core/bin/lib/roadmap-command-router.cjs',
'gsd-core/bin/lib/state-command-router.cjs',
'gsd-core/bin/lib/gap-checker.cjs',
'gsd-core/bin/lib/gate-predicate-evaluator.cjs',
'gsd-core/bin/lib/config.cjs',
'gsd-core/bin/lib/profile-output.cjs',
'gsd-core/bin/lib/commands.cjs',
'gsd-core/bin/lib/state.cjs',
'gsd-core/bin/lib/milestone.cjs',
'gsd-core/bin/lib/phase.cjs',
'gsd-core/bin/lib/verify.cjs',
'gsd-core/bin/lib/init.cjs',
'gsd-core/bin/lib/docs.cjs',
'gsd-core/bin/lib/check-command-router.cjs',
'gsd-core/bin/lib/frontmatter.cjs',
'gsd-core/bin/lib/learnings.cjs',
'gsd-core/bin/lib/gsd2-import.cjs',
'gsd-core/bin/lib/profile-pipeline.cjs',
'gsd-core/bin/lib/template.cjs',
'gsd-core/bin/lib/uat.cjs',
'gsd-core/bin/lib/coverage.cjs',
'gsd-core/bin/lib/uat-predicate.cjs',
'gsd-core/bin/lib/workstream.cjs',
'gsd-core/bin/lib/roadmap.cjs',
'gsd-core/bin/lib/audit.cjs',
'gsd-core/bin/lib/research-store.cjs',
'gsd-core/bin/lib/research-provider.cjs',
'gsd-core/bin/lib/package-legitimacy.cjs',
// ADR-457: tsc-generated runtime artifact — lint the src/git-base-branch.cts source.
'gsd-core/bin/lib/git-base-branch.cjs',
// ADR-1213: tsc-generated runtime artifact — lint the src/capability-writer.cts source.
'gsd-core/bin/lib/capability-writer.cjs',
// issue #1754: tsc-generated runtime artifact — lint the src/cli-skew-check.cts source.
'gsd-core/bin/lib/cli-skew-check.cjs',
// issue #1355: tsc-generated runtime artifact — lint the src/teams-status.cts source.
'gsd-core/bin/lib/teams-status.cjs',
// ADR-1372: tsc-generated runtime artifact — lint the src/markdown-sectionizer.cts source.
'gsd-core/bin/lib/markdown-sectionizer.cjs',
// ADR-2143: tsc-generated runtime artifact — lint the src/markdown-table.cts source.
'gsd-core/bin/lib/markdown-table.cjs',
// ADR-2143: tsc-generated runtime artifact — lint the src/write-set.cts source.
'gsd-core/bin/lib/write-set.cjs',
// ADR-1239 Phase C-1 (#1680): tsc-generated — lint src/embedding-adapter.cts + src/adapter-declarative.cts.
'gsd-core/bin/lib/embedding-adapter.cjs',
'gsd-core/bin/lib/adapter-declarative.cjs',
'gsd-core/bin/lib/adapter-imperative.cjs',
'gsd-core/bin/lib/model-adapter.cjs',
'gsd-core/bin/lib/hook-bus.cjs',
'gsd-core/bin/lib/state-io.cjs',
'gsd-core/bin/lib/external-descriptor-trust.cjs',
'gsd-core/bin/lib/mcp-server.cjs',
],
},
// ── src/**/*.cts — TypeScript runtime sources (ADR-457 build-at-publish) ─────
// First-class type-aware linting on the migrated source. The TS compiler
// (`npm run build:lib`, strict + noEmitOnError) is the primary type gate;
// these rules add lint-level coverage. warn-first per the harness convention.
{
files: ['src/**/*.cts'],
plugins: {
local: localPlugin,
},
extends: [tseslint.configs.recommendedTypeChecked],
languageOptions: {
parserOptions: {
project: './tsconfig.build.json',
tsconfigRootDir: __dirname,
},
},
rules: {
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
// ADR-1372 T7: enforce use of the markdown-sectionizer seam; grandfather
// pre-migration sites with // allow-adhoc-markdown: <reason>
'local/no-adhoc-markdown-parsing': 'error',
// ADR-1703 Phase 5: flag path-returning calls interpolated into content
// (markdown @-references, workflow files, generated docs) without POSIX
// normalization. Promoted to 'error' after precision review (path.basename
// excluded; content heuristic tightened to genuine reference/config-dir
// markers). See RULESET.CONTENT-PATH-NORMALIZATION in CONTEXT.md.
'local/normalize-path-in-content': 'error',
// ADR-1703 Phase 6: flag an unguarded fs.rename/fs.renameSync (the
// atomic-publish primitive) that lacks a transient-errno fallback
// (EPERM/EBUSY/EACCES retry or a Windows platform guard). See
// DEFECT.WINDOWS-FS-OPS in CONTEXT.md.
'local/require-fs-op-fallback': 'error',
},
},
// ── bin/install.js + scripts/build-hooks.js — ADR-1703 Phase 6 glob expansion ─
// The top-level `bin/install.js` (generated installer) and `scripts/build-hooks.js`
// (the build-side atomic-replace helper) are the two production surfaces named by
// DEFECT.WINDOWS-FS-OPS that were NOT covered by the src/**/*.cts / gsd-core/bin/**/*.cjs
// globs (ADR-1703 L124-126). This block brings them under the two production
// portability rules. It deliberately does NOT apply the full js.recommended set —
// bin/install.js is ~12k lines of generated code; the ADR's mandate is the
// portability defect surface, not a broader generated-code style sweep.
{
files: ['bin/install.js', 'bin/gsd-mcp-server.js', 'scripts/build-hooks.js'],
plugins: {
local: localPlugin,
},
languageOptions: {
sourceType: 'commonjs',
globals: {
...globals.node,
},
},
rules: {
'local/normalize-path-in-content': 'error',
'local/require-fs-op-fallback': 'error',
},
},
// ── gsd-core/bin/**/*.cjs + scripts/**/*.cjs ───────────────────────────
// CommonJS Node files: js.recommended + eslint-plugin-n + local plugin rules
{
files: ['gsd-core/bin/**/*.cjs', 'scripts/**/*.cjs'],
plugins: {
n: pluginN,
local: localPlugin,
},
languageOptions: {
sourceType: 'commonjs',
globals: {
...globals.node,
},
},
rules: {
...js.configs.recommended.rules,
// Generic quality rules
'no-var': 'error',
'prefer-const': 'warn',
'no-unused-vars': ['warn', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrors: 'none',
}],
'no-empty': ['warn', { allowEmptyCatch: true }],
// Downgraded from recommended error → warn (pre-existing violations; follow-up to fix)
'no-useless-escape': 'warn',
'no-unsafe-finally': 'warn',
// eslint-plugin-n rules
'n/no-process-exit': 'error',
'n/no-path-concat': 'error',
// Local rules — warn for now; flip to error after cleanup phases
'local/no-source-grep': 'warn',
},
},
// ── tests/**/*.test.cjs ─────────────────────────────────────────────────────
{
files: ['tests/**/*.test.cjs'],
plugins: {
'no-only-tests': noOnlyTests,
local: localPlugin,
},
languageOptions: {
sourceType: 'commonjs',
globals: {
...globals.node,
},
},
rules: {
...js.configs.recommended.rules,
'no-only-tests/no-only-tests': 'error',
// Timing anti-patterns — ratcheted to error after cleanup (all violations fixed)
'local/no-magic-sleep-in-tests': 'error',
'local/no-elapsed-assertion': 'warn',
// Ban raw fs.rmSync in tests — use helpers.cleanup() for Windows-EBUSY retry budget
'local/no-raw-rmsync-in-tests': 'error',
// Ban tautological assertions (always-truthy arg or identical-literal equality)
'local/no-tautological-assert': 'error',
// Ban source-grep pattern in tests — use require() + behavior assertions instead
'local/no-source-grep': 'error',
// Ban path-returning calls compared to hardcoded POSIX-slash literals (fails on Windows)
'local/no-path-literal-in-assert': 'error',
// Ban POSIX mode-bit assertions compared to octal literals (fails on Windows)
'local/no-posix-mode-bit-assert': 'error',
// Ban unguarded chmod exec-bit + sh/bash -c combos (fails on Windows Git Bash)
'local/no-unguarded-nonportable-exec': 'error',
// Ban CRLF-fragile file-content splits and regex patterns (ADR-1703 Phase 4)
'local/no-crlf-fragile-split': 'error',
// Ban hardcoded /tmp/ paths in fs.* calls (ADR-1703 Phase 4)
'local/no-hardcoded-tmp': 'error',
// Ban bare npm exec without shell:true (ADR-1703 Phase 4)
'local/no-bare-npm-exec': 'error',
// Require USERPROFILE alongside HOME assignments (ADR-1703 Phase 4)
'local/require-userprofile-with-home': 'error',
// Ban raw setTimeout sync + elapsed/duration-style assertions via no-restricted-syntax
'no-restricted-syntax': [
'error',
{
selector: 'AwaitExpression > NewExpression[callee.name="Promise"] ArrowFunctionExpression CallExpression[callee.name="setTimeout"]',
message: 'Raw setTimeout used for synchronization in tests. Use proper async patterns instead.',
},
{
selector: 'CallExpression[callee.object.name="Atomics"][callee.property.name="wait"]',
message: 'Atomics.wait() used as a sleep in tests. Use a proper async wait pattern instead.',
},
],
'no-unused-vars': ['warn', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrors: 'none',
}],
'no-empty': ['warn', { allowEmptyCatch: true }],
// Downgraded from recommended error → warn (pre-existing violations; follow-up to fix)
'no-useless-escape': 'warn',
'no-regex-spaces': 'warn',
'no-control-regex': 'error',
'no-irregular-whitespace': 'warn',
},
},
// ── #1279 lint-rule fail-first fixture ──────────────────────────────────────
// `tests/_ff_lint_violation.cjs` is a PLAIN `.cjs` (NOT `*.test.cjs`) on purpose: it is a KNOWN
// `local/no-source-grep` violation that `defaultProveFailFirst` lints to machine-prove the rule
// has teeth, and it must stay OFF the `node --test` runner glob (executing it ENOENTs on the
// intentional `lib/foo.cjs` path). It still needs the `local` plugin registered so its inline
// `/* eslint-disable local/no-source-grep */` resolves (otherwise `eslint .` errors "rule not
// found") and the violation lands in `suppressedMessages` (which the prover reads), keeping the
// project's own `eslint .` green. (#1279)
{
files: ['tests/_ff_lint_violation.cjs'],
plugins: { local: localPlugin },
languageOptions: { sourceType: 'commonjs', globals: { ...globals.node } },
rules: { 'local/no-source-grep': 'error' },
},
// ── #2126 lint-rule CLEAN fixture ───────────────────────────────────────────
// `tests/_ff_lint_clean.cjs` is the KNOWN-CLEAN companion to the violation fixture: the
// prohibition-enforcement real-runner tests lint it as their non-vacuous "clean target" instead of
// a type-aware `src/**/*.cts` file, so each eslint spawn is ~0.8s (non-type-aware) not ~2s
// (whole-tsconfig-program load) — removing the CPU starvation that blew the 60s bound under
// --test-concurrency. Rule enabled (as error) so the pass is non-vacuous; the file is clean so it
// greens. PLAIN `.cjs`, kept OFF the `*.test.cjs` runner glob. (#2126)
{
files: ['tests/_ff_lint_clean.cjs'],
plugins: { local: localPlugin },
languageOptions: { sourceType: 'commonjs', globals: { ...globals.node } },
rules: { 'local/no-source-grep': 'error' },
},
);