* refactor(#3183): route live-plan counting through scanPhasePlans scanPhasePlans becomes the sole owner of the live-plan derivation. Twenty-one independent re-derivations across seven modules now route through it, and scripts/lint-plan-count-drift.cjs reports zero, scanning the whole repo rather than an allowlist (ADR-3180 Decision 4a). The epic scoped this at three copies. A whole-repo guard found twenty-six sites across nine files, so Phase 1 absorbs every live-plan re-derivation and Phase 3 narrows to window plus sentinel enumeration. Two sites are exempt with a documented reason rather than a bare allowlist: audit.cts scans one quick task's own directory for a single completion record, and gsd2-import.cts reads a foreign GSD-2 tasks/ layout during a one-time import. Neither is a phase directory. scanPhasePlans gains allPlanFiles (pre-supersession) alongside planFiles so one owner answers both questions: verify.cts's numbering-gap check wants every plan on disk, its pairing check wants the live set. Both fields are additive. Highest-severity fix: cmdPhasePlanIndex, which feeds execute-phase wave scheduling, was scheduling status:superseded plans into waves and reporting zero plans for the post-#3139 nested layout. filterPlanFiles and filterSummaryFiles are deleted; getPhaseFileStats orphaned them and only their own tests still called them. New leaf module src/planning-scope.cts carries the frozen SCOPE discriminator, with its six-gate ripple closed: gitignore, inventory manifest, INVENTORY.md and the CONTEXT.md glossary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012qYy4ZWif3sscQyMsup6Ma * docs(#3183): amend ADR-3180 for the Phase 1/3 boundary re-slice The contract held; the phase boundary did not. The whole-repo drift guard found 26 re-derivations across 9 files against the epic's estimate of 3, and cmdProgressRender re-derives both enumeration and plan counting on adjacent lines, so DW4 was unsatisfiable within Phase 1's original file scope. Records the amended scope, scanPhasePlans's new allPlanFiles field, findOrphanSummaries, the two documented exemptions, the re-derived Tier-2 table, and the describeNonCanonicalPlans trap for later phases. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012qYy4ZWif3sscQyMsup6Ma * fix(#3183): complete the canonical pairing rule and gate the naming diagnostic The remote runner went red with 13 deterministic failures on both lanes, and they were right: replacing verify.cts's canonicalPlanStem pairing with summaryCandidates dropped a case the bespoke rule covered. A plan carrying a descriptive slug after its id (68-01-scaffolding-PLAN.md) pairs with its canonical-stem summary (68-01-SUMMARY.md), and summaryCandidates generated no such candidate, so the plan read unsummarized. The fix is to complete the one rule rather than restore a second: summaryCandidates gains a canonical-id candidate, narrowed to fire only when an id pair was actually extracted. countMatchedSummaries, findUnsummarizedPlans and findOrphanSummaries all inherit it. The two-plans-one-summary collision behaviour of the original rule is preserved deliberately and documented in place. Second defect, independently root-caused while verifying: routing the #2893 naming diagnostic through scanPhasePlans exposed it to the loose /PLAN/i fallback, which is correct for counting and wrong for a naming check — a non-canonically-named file was accepted as a valid plan and the diagnostic went silent. cmdPhasesList, cmdFindPhase and cmdPhasePlanIndex now intersect with a strict isCanonicalPlanFile predicate before reporting names. Same class as the describeNonCanonicalPlans trap already recorded in ADR-3180: a question about file naming wants the physical, strictly-matched set; only a question about outstanding work wants the live set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012qYy4ZWif3sscQyMsup6Ma * chore(#3183): register planning-scope.cjs in the eslint migration list tests/repo-invariants.test.cjs asserts every bin/lib/*.cjs is linted xor ignored per its ADR-457 migration state. The new planning-scope module closed five of the six .cts ripple gates - gitignore, inventory manifest, INVENTORY.md and the CONTEXT.md glossary - but not eslint, because that one is enforced by a test rather than by lint:ci, so the local pipeline stayed green while it was missing. Generated from src/planning-scope.cts, so the .cjs is ignored and the .cts is linted, matching every other migrated module. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012qYy4ZWif3sscQyMsup6Ma * fix(#3183): replace the plan-count drift detector with a literal tokenizer CodeQL reported 4 high-severity js/redos alerts on REGEX_LITERAL_MD_RE, the backtracking regex that finds "a regex literal mentioning PLAN/SUMMARY and an escaped \.md". Five review rounds found it had two defects, not one: - EXPONENTIAL, then CUBIC. Its "any char" atom `(?:\\.|[^/\r\n])` let a `\.` pair be consumed either as one escape or as two class characters, which is exponential backtracking: 27,464ms on `"/\.mdplan" + "\.".repeat(28) + "X"`. Excluding `\` from the class killed that but left a cubic path — 23ms at N=200, 172ms at N=400, 1362ms at N=800 on `"/" + "PLAN\.md".repeat(N)` with no closing `/`. This guard is the last stage of `npm run lint:ci`, which CI runs on fork pull requests, so a crafted src/*.cts could stall the job. - A DETECTION HOLE. A character class holding a bare, unescaped `/` — e.g. `/SUMMARY[^/]*\.md$/`, an ordinary path-excluding filter — terminated the literal at that `/`, so the scan never reached `\.md` and the guard missed it entirely. (Classes holding an ESCAPED `\/` were already matched; the tests cover those separately as parity, not as regressions.) Both defects have one root cause: regex-literal grammar — `\x` escapes, and `/` inside `[...]` not terminating — is not expressible in a backtracking regex. So the detector is now a tokenizer, not a regex. readRegexLiteralAt reads the literal at a given `/` in a single left-to-right pass with no backtracking, treating escapes as two-character units and suppressing the `/` terminator inside a character class. findRegexLiteralMdMatch restarts it at every `/` on the line, preserving the old "find anywhere" behaviour; MAX_REGEX_LITERAL_LEN (400) bounds each read — including the trailing-flag scan — which keeps the whole-line cost linear. Results: cubic shape flat at 0.06-0.39ms out to N=3200 (25KB), exponential shape 0.01ms at 28 reps and 0.00ms at 64, and the bare-`/` class shapes are now caught. Differential against the old regex over 28,474 lines (those matching FILENAME_TEST_RE but not PLAN_SUMMARY_LITERAL_RE, across src/tests/scripts/ gsd-core/bin/eslint-rules, excluding 265 lines with >6 backslashes on which the old regex hangs): 6 differences, all the tokenizer returning the fuller or newly-correct literal, 0 old-only misses. The `\.md` token stays case-insensitive, matching the `/i` the old regex carried. Also closes three holes in the same new file: - walk() tested entry.isFile(), false for a symlink, so a symlinked src/*.cts was silently unscanned — an evasion of a guard whose stated principle (ADR-3180 Decision 4a) is whole-repo discovery with no allowlist. It now resolves symlinks, but confined: file links must resolve inside the repo root, directory links inside the scanned dir itself. Every sibling drift guard in scripts/ uses the Dirent classification and never follows links, so following them unconfined would have made this the only linter able to read outside the tree — on fork PRs an arbitrary out-of-repo read whose matched fragments reach a public CI log. The narrower directory rule additionally stops `src/up -> ..` from sweeping the whole repo, and the skip list is now checked against resolved paths so `src/g -> ../.git` cannot reach .git/** or node_modules/**. Real paths are de-duplicated and files reported canonically, so a symlink alias cannot shift which FUNCTION_SCOPED_EXEMPTIONS key applies. - Both the reported fragment and the reported FILE PATH are attacker- controlled source text written straight to a CI log, and git permits control bytes in a filename. Both are now escaped — C0/C1/DEL plus the bidi and zero-width controls — so a crafted literal or filename cannot recolour the log, overwrite a line with CR, or fabricate a line that looks like this guard's own success output. Regression coverage in tests/plan-count-single-owner.test.cjs: a child-process probe over both pathological shapes (catastrophic backtracking is synchronous and would freeze the suite rather than fail one test), the bare-`/` class shapes verified to fail against the parent-commit blob, root-confinement tests covering the outside-file, outside-directory, cycle, broken-link and duplicate cases, direct isInsideRoot coverage including the sibling-prefix case that a bare startsWith would let through, sanitizeForReport coverage, and limit-1/limit/limit+1 coverage of MAX_REGEX_LITERAL_LEN derived from the exported constant. The earlier structural assertion was dropped — it checked for the substring `[^/`, which respelling the class as `[^\r\n/]` defeats while staying exponential. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012qYy4ZWif3sscQyMsup6Ma * chore(#3183): backfill changeset PR number Restores b77931869, which a force-push during the ReDoS remediation dropped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012qYy4ZWif3sscQyMsup6Ma --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
487 lines
24 KiB
JavaScript
487 lines
24 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';
|
|
import noUnboundedSpawn from './eslint-rules/no-unbounded-spawn.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,
|
|
'no-unbounded-spawn': noUnboundedSpawn,
|
|
},
|
|
};
|
|
|
|
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/commonjs-marker.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/unusable-input.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/review-lane-descriptor.cjs',
|
|
'gsd-core/bin/lib/review-lane-invocation.cjs',
|
|
'gsd-core/bin/lib/review-lane-runner.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/installer-migrations/008-cursor-retire-commands-surface.cjs',
|
|
'gsd-core/bin/lib/retired-artifact-cleanup.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',
|
|
// 007 is tsc output like its siblings, but unlike 006 it imports node
|
|
// builtins — so tsc emits its `__importDefault` helper, which uses `var`
|
|
// and trips no-var. ADR-457: the linted source is the .cts.
|
|
'gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs',
|
|
// 009 also imports node builtins (fs, path) like 007, so tsc emits the
|
|
// same `__importDefault` helper. ADR-457: the linted source is the .cts.
|
|
'gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-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/planning-scope.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/phase-estimation.cjs',
|
|
'gsd-core/bin/lib/estimate-cli.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/plan-dependency-graph.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',
|
|
// #3072: tsc-generated runtime artifact — lint the src/mcp-catalog.cts source.
|
|
'gsd-core/bin/lib/mcp-catalog.cjs',
|
|
// ADR-1671 (#2928): tsc-generated runtime artifact — lint the src/context-predicates.cts source.
|
|
'gsd-core/bin/lib/context-predicates.cjs',
|
|
// #2929: tsc-generated runtime artifact — lint the src/context-composer.cts source.
|
|
'gsd-core/bin/lib/context-composer.cjs',
|
|
// ADR-1671 (#2930): tsc-generated runtime artifact — lint the src/workflow-fragments.cts source.
|
|
'gsd-core/bin/lib/workflow-fragments.cjs',
|
|
// ADR-1671 Phase 5 (#2932): tsc-generated runtime artifact — lint the src/section-manifest.cts source.
|
|
'gsd-core/bin/lib/section-manifest.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/**/*.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 unbounded sync child_process spawns in tests (DEFECT.UNBOUNDED-SUBPROCESS).
|
|
// No allowlist: the epic (#3064) migrated every site; the rule runs with no
|
|
// exemption surface. The only sanctioned escapes are an explicit `timeout` on
|
|
// a raw spawn or the `// allow-spawn-timeout-ceiling: <reason>` marker.
|
|
'local/no-unbounded-spawn': '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' },
|
|
},
|
|
|
|
// ── #2453 Command Routing Hub: uniform handler signature ────────────────────
|
|
// Every route handler in gsd-tools.cjs is declared with the SAME destructured
|
|
// signature — `function routeX({ args, cwd, raw, error })` — whether or not it
|
|
// uses all four members. That uniformity is the point: it is the dispatch
|
|
// contract, so a handler can be moved or added without re-deriving which
|
|
// members exist.
|
|
//
|
|
// `argsIgnorePattern: '^_'` is structurally in conflict with that convention:
|
|
// satisfying it would mean `_`-prefixing ~50 parameters, which makes the
|
|
// signature non-uniform across the table and defeats the contract. So args
|
|
// checking is disabled HERE ONLY.
|
|
//
|
|
// `varsIgnorePattern` is deliberately left intact: genuinely dead *variables*
|
|
// (the #2379 case — unused `require()` results) must still surface. This
|
|
// narrows the exemption to the one category the convention actually forces.
|
|
//
|
|
// Decision deferred by #732 ("Severities stay `warn` (no config change in this
|
|
// pass)"), resolved by #2453 option 1.
|
|
{
|
|
files: ['gsd-core/bin/gsd-tools.cjs'],
|
|
rules: {
|
|
'no-unused-vars': ['warn', {
|
|
args: 'none',
|
|
varsIgnorePattern: '^_',
|
|
caughtErrors: 'none',
|
|
}],
|
|
},
|
|
},
|
|
);
|