From 77598f47b27b07925319c9b1f22987e896bc9380 Mon Sep 17 00:00:00 2001 From: Aleksandar Grbic Date: Sun, 31 May 2026 19:23:04 +0200 Subject: [PATCH] feat: add no-historical-comments + no-bare-date-now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new rules covering categories of agent-generated drift that the existing plugin set didn't catch: eslint-plugin-comment-hygiene/no-historical-comments Flags comments framing code relative to what it USED TO be or to a past incident: "Codex flagged X", "before the fix", "after the refactor", "we used to", "no longer", "kept for backwards compat", "historically", "Alpine-era workaround". History belongs in commit messages and PR descriptions where it stays pinned to the diff. eslint-plugin-code-flow/no-bare-date-now Flags direct Date.now() / new Date() (no args) / Date() (no args) / Math.random() outside an allowlisted set of utility paths. Determinism is required for snapshot tests, workflow replays, and time-travel debugging — consumers route through a typed util that can be faked. Configurable via the `allowedPaths` option. Both rules included in their plugin's `recommended` config. Tests + docs pages added. Plugin shape + recommended-config integrity tests in eslint-plugin-code-flow updated to enumerate the new rule. --- .changeset/no-bare-date-now.md | 9 ++ .changeset/no-historical-comments.md | 9 ++ .../docs/rules/no-bare-date-now.md | 77 ++++++++++ .../src/configs/recommended.ts | 3 +- eslint-plugin-code-flow/src/index.ts | 2 + eslint-plugin-code-flow/src/rules/index.ts | 4 +- .../src/rules/noBareDateNow.ts | 133 ++++++++++++++++++ eslint-plugin-code-flow/tests/plugin.test.ts | 1 + .../tests/rules/noBareDateNow.test.ts | 91 ++++++++++++ .../docs/rules/no-historical-comments.md | 52 +++++++ .../src/configs/recommended.ts | 1 + eslint-plugin-comment-hygiene/src/index.ts | 7 +- .../src/rules/index.ts | 2 + .../src/rules/no-historical-comments.ts | 96 +++++++++++++ .../rules/no-historical-comments.test.ts | 88 ++++++++++++ 15 files changed, 572 insertions(+), 3 deletions(-) create mode 100644 .changeset/no-bare-date-now.md create mode 100644 .changeset/no-historical-comments.md create mode 100644 eslint-plugin-code-flow/docs/rules/no-bare-date-now.md create mode 100644 eslint-plugin-code-flow/src/rules/noBareDateNow.ts create mode 100644 eslint-plugin-code-flow/tests/rules/noBareDateNow.test.ts create mode 100644 eslint-plugin-comment-hygiene/docs/rules/no-historical-comments.md create mode 100644 eslint-plugin-comment-hygiene/src/rules/no-historical-comments.ts create mode 100644 eslint-plugin-comment-hygiene/tests/rules/no-historical-comments.test.ts diff --git a/.changeset/no-bare-date-now.md b/.changeset/no-bare-date-now.md new file mode 100644 index 0000000..337ac49 --- /dev/null +++ b/.changeset/no-bare-date-now.md @@ -0,0 +1,9 @@ +--- +"@boring-stack-pkg/eslint-plugin-code-flow": minor +--- + +Add `no-bare-date-now` rule. + +Disallows direct calls to non-deterministic time/random sources (`Date.now()`, `new Date()` with no args, `Date()` with no args, `Math.random()`) outside an allowlisted set of utility paths. Determinism is required for snapshot tests, workflow replays, and time-travel debugging — every consumer should route through a typed util that can be faked in tests. + +Configurable via the `allowedPaths` option (substring match against the source file path). Enabled in the `recommended` config. diff --git a/.changeset/no-historical-comments.md b/.changeset/no-historical-comments.md new file mode 100644 index 0000000..da3692c --- /dev/null +++ b/.changeset/no-historical-comments.md @@ -0,0 +1,9 @@ +--- +"@boring-stack-pkg/eslint-plugin-comment-hygiene": minor +--- + +Add `no-historical-comments` rule. + +Flags source comments that frame code relative to what it used to do or to a past incident — `Codex flagged X`, `before the fix`, `after the refactor`, `we used to`, `no longer`, `kept for backwards compat`, `historically`, `Alpine-era workaround`. Source comments must describe the current invariant; history belongs in the commit message or PR description where it stays pinned to the diff it describes. + +Enabled in the `recommended` config. diff --git a/eslint-plugin-code-flow/docs/rules/no-bare-date-now.md b/eslint-plugin-code-flow/docs/rules/no-bare-date-now.md new file mode 100644 index 0000000..8bfee82 --- /dev/null +++ b/eslint-plugin-code-flow/docs/rules/no-bare-date-now.md @@ -0,0 +1,77 @@ +# no-bare-date-now + +Disallow direct calls to non-deterministic time/random sources outside an allowlisted set of utility paths: `Date.now()`, `new Date()` (zero-arg), `Date()` (zero-arg), `Math.random()`. + +## Why + +Direct clock/random reads in business logic make code untestable and undeployable: + +- **Snapshot tests** become flaky because `Date.now()` drifts between runs. +- **Workflow replays** (resumable tasks, event sourcing) diverge from the original run. +- **Time-travel debugging** is impossible — there's no seam to swap in a fake clock. +- **Tests** end up wrapping every call site in stubs instead of swapping one util. + +Every consumer should route through a typed util (e.g. `now()` from `src/lib/time/now.ts`, a seeded random helper, etc.). That single util is exempted via the `allowedPaths` option. + +## Incorrect + +```ts +function timestamp(): number { + return Date.now(); +} + +function makeId(): string { + return `id-${Math.random().toString(36).slice(2)}`; +} + +function expiresAt(): Date { + return new Date(); +} +``` + +## Correct + +```ts +import { now } from "@/lib/time/now"; +import { randomId } from "@/lib/random/id"; + +function timestamp(): number { + return now(); +} + +function makeId(): string { + return randomId(); +} + +function expiresAt(): Date { + return new Date(now()); // explicit argument — pure parser, not a clock read +} +``` + +Parsing an explicit argument is fine: + +```ts +new Date("2026-01-01T00:00:00Z"); +new Date(1700000000000); +Date.parse("2026-01-01"); +``` + +## Options + +```jsonc +{ + "code-flow/no-bare-date-now": [ + "error", + { + // Substring match against the source file's path. Files containing + // any of these segments are exempted — meant to whitelist the util + // wrappers themselves. + "allowedPaths": ["src/lib/time/", "src/lib/random/"] + } + ] +} +``` + +## When not to use + +If your codebase deliberately doesn't have determinism requirements (one-off scripts, scratch repos), disable this rule. For production source code: leave it on. diff --git a/eslint-plugin-code-flow/src/configs/recommended.ts b/eslint-plugin-code-flow/src/configs/recommended.ts index df1cb2c..41c21b8 100644 --- a/eslint-plugin-code-flow/src/configs/recommended.ts +++ b/eslint-plugin-code-flow/src/configs/recommended.ts @@ -1,4 +1,5 @@ export const recommendedRules = { "code-flow/prefer-early-return": "error", - "code-flow/no-template-trim-empty-ternary": "error" + "code-flow/no-template-trim-empty-ternary": "error", + "code-flow/no-bare-date-now": "error" } as const; diff --git a/eslint-plugin-code-flow/src/index.ts b/eslint-plugin-code-flow/src/index.ts index 58f3537..2f56a5d 100644 --- a/eslint-plugin-code-flow/src/index.ts +++ b/eslint-plugin-code-flow/src/index.ts @@ -2,6 +2,7 @@ import type { TSESLint } from "@typescript-eslint/utils"; import { recommendedRules } from "./configs/recommended"; import { rules } from "./rules"; +import { noBareDateNowRule } from "./rules/noBareDateNow"; import { noTemplateTrimEmptyTernaryRule } from "./rules/noTemplateTrimEmptyTernary"; import { preferEarlyReturnRule } from "./rules/preferEarlyReturn"; @@ -25,6 +26,7 @@ plugin.configs.recommended = { rules: recommendedRules }; +export { noBareDateNowRule }; export { noTemplateTrimEmptyTernaryRule }; export { preferEarlyReturnRule }; export { rules }; diff --git a/eslint-plugin-code-flow/src/rules/index.ts b/eslint-plugin-code-flow/src/rules/index.ts index ef5934b..0f7b065 100644 --- a/eslint-plugin-code-flow/src/rules/index.ts +++ b/eslint-plugin-code-flow/src/rules/index.ts @@ -1,7 +1,9 @@ +import { noBareDateNowRule } from "./noBareDateNow"; import { noTemplateTrimEmptyTernaryRule } from "./noTemplateTrimEmptyTernary"; import { preferEarlyReturnRule } from "./preferEarlyReturn"; export const rules = { "prefer-early-return": preferEarlyReturnRule, - "no-template-trim-empty-ternary": noTemplateTrimEmptyTernaryRule + "no-template-trim-empty-ternary": noTemplateTrimEmptyTernaryRule, + "no-bare-date-now": noBareDateNowRule }; diff --git a/eslint-plugin-code-flow/src/rules/noBareDateNow.ts b/eslint-plugin-code-flow/src/rules/noBareDateNow.ts new file mode 100644 index 0000000..ac46647 --- /dev/null +++ b/eslint-plugin-code-flow/src/rules/noBareDateNow.ts @@ -0,0 +1,133 @@ +import type { TSESTree } from "@typescript-eslint/utils"; + +import { createRule } from "../utils/createRule"; + +export const RULE_NAME = "no-bare-date-now"; + +type MessageIds = + | "bareDateNow" + | "bareNewDate" + | "bareMathRandom" + | "bareDateConstructor"; + +export interface INoBareDateNowOptions { + readonly allowedPaths?: readonly string[]; +} + +const DEFAULTS: Required = { + allowedPaths: [] +}; + +function fileMatchesAllowlist( + filename: string, + allowed: readonly string[] +): boolean { + if (allowed.length === 0) { + return false; + } + const normalized = filename.replace(/\\/g, "/"); + return allowed.some((segment) => normalized.includes(segment)); +} + +function isDateNow(node: TSESTree.CallExpression): boolean { + const callee = node.callee; + return ( + callee.type === "MemberExpression" && + callee.object.type === "Identifier" && + callee.object.name === "Date" && + callee.property.type === "Identifier" && + callee.property.name === "now" && + !callee.computed + ); +} + +function isMathRandom(node: TSESTree.CallExpression): boolean { + const callee = node.callee; + return ( + callee.type === "MemberExpression" && + callee.object.type === "Identifier" && + callee.object.name === "Math" && + callee.property.type === "Identifier" && + callee.property.name === "random" && + !callee.computed + ); +} + +function isBareNewDate(node: TSESTree.NewExpression): boolean { + return ( + node.callee.type === "Identifier" && + node.callee.name === "Date" && + node.arguments.length === 0 + ); +} + +function isBareDateCall(node: TSESTree.CallExpression): boolean { + return ( + node.callee.type === "Identifier" && + node.callee.name === "Date" && + node.arguments.length === 0 + ); +} + +export const noBareDateNowRule = createRule<[INoBareDateNowOptions], MessageIds>({ + name: RULE_NAME, + meta: { + type: "problem", + docs: { + description: + "Disallow direct calls to non-deterministic time/random sources (`Date.now()`, `new Date()`, `Date()`, `Math.random()`) outside an allowlisted set of utility paths. Determinism is required for snapshot tests, workflow replays, and time-travel debugging — every consumer should route through a typed util that can be faked in tests.", + recommended: true + }, + schema: [ + { + type: "object", + properties: { + allowedPaths: { + type: "array", + items: { type: "string" } + } + }, + additionalProperties: false + } + ], + messages: { + bareDateNow: + "Direct `Date.now()` is non-deterministic. Import the project's `now()` util instead (or add the file to this rule's `allowedPaths` if it IS the util).", + bareNewDate: + "Direct `new Date()` (no args) is non-deterministic. Import the project's `now()` util and pass the millisecond timestamp explicitly, or add the file to `allowedPaths`.", + bareDateConstructor: + "Direct `Date()` (no args) is non-deterministic. Import the project's `now()` util and pass the millisecond timestamp explicitly, or add the file to `allowedPaths`.", + bareMathRandom: + "Direct `Math.random()` is non-deterministic. Import the project's random util (which can be seeded in tests) instead, or add the file to `allowedPaths`." + } + }, + defaultOptions: [DEFAULTS], + create(context, optionsArg) { + const options = optionsArg[0] ?? DEFAULTS; + const allowed = options.allowedPaths ?? DEFAULTS.allowedPaths; + if (fileMatchesAllowlist(context.filename, allowed)) { + return {}; + } + + return { + CallExpression(node: TSESTree.CallExpression) { + if (isDateNow(node)) { + context.report({ node, messageId: "bareDateNow" }); + return; + } + if (isMathRandom(node)) { + context.report({ node, messageId: "bareMathRandom" }); + return; + } + if (isBareDateCall(node)) { + context.report({ node, messageId: "bareDateConstructor" }); + } + }, + NewExpression(node: TSESTree.NewExpression) { + if (isBareNewDate(node)) { + context.report({ node, messageId: "bareNewDate" }); + } + } + }; + } +}); diff --git a/eslint-plugin-code-flow/tests/plugin.test.ts b/eslint-plugin-code-flow/tests/plugin.test.ts index f815cb4..c9d4606 100644 --- a/eslint-plugin-code-flow/tests/plugin.test.ts +++ b/eslint-plugin-code-flow/tests/plugin.test.ts @@ -11,6 +11,7 @@ describe("plugin shape", () => { it("exposes every rule under kebab-case keys", () => { expect(Object.keys(plugin.rules ?? {}).sort()).toEqual([ + "no-bare-date-now", "no-template-trim-empty-ternary", "prefer-early-return" ]); diff --git a/eslint-plugin-code-flow/tests/rules/noBareDateNow.test.ts b/eslint-plugin-code-flow/tests/rules/noBareDateNow.test.ts new file mode 100644 index 0000000..9b747f9 --- /dev/null +++ b/eslint-plugin-code-flow/tests/rules/noBareDateNow.test.ts @@ -0,0 +1,91 @@ +import { RULE_NAME, noBareDateNowRule } from "../../src/rules/noBareDateNow"; +import { ruleTester } from "../test-utils/ruleTester"; + +ruleTester.run(RULE_NAME, noBareDateNowRule, { + valid: [ + // Routed through a project util — the rule never sees the underlying + // Date.now() because the source file imports a named helper. + { + code: `import { now } from "./time"; const t = now();` + }, + // `new Date(timestamp)` with an explicit argument is fine — it's a + // pure parser, not a clock read. + { + code: `const t = new Date(1700000000000);` + }, + { + code: `const t = new Date("2026-01-01T00:00:00Z");` + }, + { + code: `const t = Date.parse("2026-01-01");` + }, + // Math.* calls that are NOT random are unaffected. + { + code: `const x = Math.floor(1.5);` + }, + // File-allowlist exempts the wrapper itself from the rule. + { + code: `export const now = () => Date.now();`, + filename: "src/lib/time/now.ts", + options: [{ allowedPaths: ["src/lib/time/"] }] + }, + { + code: `export const random = () => Math.random();`, + filename: "src/lib/random/index.ts", + options: [{ allowedPaths: ["src/lib/random/"] }] + }, + // Empty allowedPaths (the default) means the rule applies everywhere + // — the valid cases above demonstrate non-violating call shapes. + { + code: `const t = Date.parse(input);`, + options: [{}] + } + ], + invalid: [ + { + code: `const t = Date.now();`, + errors: [{ messageId: "bareDateNow" }] + }, + { + code: `const t = new Date();`, + errors: [{ messageId: "bareNewDate" }] + }, + { + code: `const t = Date();`, + errors: [{ messageId: "bareDateConstructor" }] + }, + { + code: `const r = Math.random();`, + errors: [{ messageId: "bareMathRandom" }] + }, + { + code: `function stamp() { return Date.now(); }`, + errors: [{ messageId: "bareDateNow" }] + }, + { + code: `const id = "id-" + Math.random().toString(36).slice(2);`, + errors: [{ messageId: "bareMathRandom" }] + }, + // File path is in scope but NOT covered by allowedPaths. + { + code: `const t = Date.now();`, + filename: "src/api/billing/billing.service.ts", + options: [{ allowedPaths: ["src/lib/time/"] }], + errors: [{ messageId: "bareDateNow" }] + }, + // Multiple offenders in one source. + { + code: ` + function pick() { + const id = Math.random(); + const at = Date.now(); + return { id, at }; + } + `, + errors: [ + { messageId: "bareMathRandom" }, + { messageId: "bareDateNow" } + ] + } + ] +}); diff --git a/eslint-plugin-comment-hygiene/docs/rules/no-historical-comments.md b/eslint-plugin-comment-hygiene/docs/rules/no-historical-comments.md new file mode 100644 index 0000000..2141105 --- /dev/null +++ b/eslint-plugin-comment-hygiene/docs/rules/no-historical-comments.md @@ -0,0 +1,52 @@ +# no-historical-comments + +Disallow comments that frame code relative to what it **used to do** or to a past incident: `// Codex flagged X`, `// before the fix`, `// after the refactor`, `// we used to`, `// no longer`, `// kept for backwards compat`, `// historically …`, `// Alpine-era workaround`. + +## Why + +Source comments should describe the **current invariant**. Comments that narrate history — what changed, why a fix was applied, what the code used to be — rot the moment the code changes again. The next refactor leaves a comment that lies about both the past and the present. + +History belongs in the **commit message** or **PR description**, where it's pinned to the diff it describes. Git blame surfaces it on demand. + +## Incorrect + +```ts +// Codex flagged this — we now check the body discriminator. +const ok = body.accessToken !== null; + +// Before the fix, this swallowed 401 silently. +if (!response.ok) { + return null; +} + +// After the auth refactor /refresh returns null for anon callers. +const refresh = await performRefresh(); + +// We used to read the JWT directly from headers. +const token = await readSessionCookie(req); + +// Kept for backwards compat with the v0 client. +export const legacyField = derived(state); + +// Alpine-era workaround for the missing libc symbol. +const buf = Buffer.from(input); +``` + +## Correct + +Describe what the code does **now**, or the constraint that makes it non-obvious: + +```ts +// Refresh succeeds only when the body carries a non-null accessToken +// — anonymous callers get { accessToken: null } and must NOT retry. +const ok = body.accessToken !== null; + +// JWT comes from the signed session cookie; never from headers. +const token = await readSessionCookie(req); +``` + +JSDoc-style block comments (`/** … */`) are not flagged. + +## When not to use + +If your codebase deliberately preserves history inline (CHANGELOG-style files, migration journals embedded as comments), disable this rule on those paths. For production source code: leave it on. diff --git a/eslint-plugin-comment-hygiene/src/configs/recommended.ts b/eslint-plugin-comment-hygiene/src/configs/recommended.ts index a1163e8..455d62d 100644 --- a/eslint-plugin-comment-hygiene/src/configs/recommended.ts +++ b/eslint-plugin-comment-hygiene/src/configs/recommended.ts @@ -1,4 +1,5 @@ export const recommendedRules = { + "comment-hygiene/no-historical-comments": "error", "comment-hygiene/no-narration-comments": "error", "comment-hygiene/no-pr-reference-comments": "error" } as const; diff --git a/eslint-plugin-comment-hygiene/src/index.ts b/eslint-plugin-comment-hygiene/src/index.ts index 356ef2e..6ae6bb3 100644 --- a/eslint-plugin-comment-hygiene/src/index.ts +++ b/eslint-plugin-comment-hygiene/src/index.ts @@ -2,6 +2,7 @@ import type { TSESLint } from "@typescript-eslint/utils"; import { recommendedRules } from "./configs/recommended"; import { rules } from "./rules"; +import { noHistoricalCommentsRule } from "./rules/no-historical-comments"; import { noNarrationCommentsRule } from "./rules/no-narration-comments"; import { noPrReferenceCommentsRule } from "./rules/no-pr-reference-comments"; @@ -25,7 +26,11 @@ plugin.configs.recommended = { rules: recommendedRules }; -export { noNarrationCommentsRule, noPrReferenceCommentsRule }; +export { + noHistoricalCommentsRule, + noNarrationCommentsRule, + noPrReferenceCommentsRule +}; export { rules }; export const configs = plugin.configs; export default plugin; diff --git a/eslint-plugin-comment-hygiene/src/rules/index.ts b/eslint-plugin-comment-hygiene/src/rules/index.ts index 3bcf7e2..80dd073 100644 --- a/eslint-plugin-comment-hygiene/src/rules/index.ts +++ b/eslint-plugin-comment-hygiene/src/rules/index.ts @@ -1,7 +1,9 @@ +import { noHistoricalCommentsRule } from "./no-historical-comments"; import { noNarrationCommentsRule } from "./no-narration-comments"; import { noPrReferenceCommentsRule } from "./no-pr-reference-comments"; export const rules = { + "no-historical-comments": noHistoricalCommentsRule, "no-narration-comments": noNarrationCommentsRule, "no-pr-reference-comments": noPrReferenceCommentsRule }; diff --git a/eslint-plugin-comment-hygiene/src/rules/no-historical-comments.ts b/eslint-plugin-comment-hygiene/src/rules/no-historical-comments.ts new file mode 100644 index 0000000..fee7b0f --- /dev/null +++ b/eslint-plugin-comment-hygiene/src/rules/no-historical-comments.ts @@ -0,0 +1,96 @@ +import type { TSESTree } from "@typescript-eslint/utils"; + +import { createRule } from "../utils/createRule"; + +export const RULE_NAME = "no-historical-comments"; + +type MessageIds = "historicalComment"; + +/* + * Patterns that frame code relative to what it USED TO be or to a past + * incident, instead of describing the current invariant. Each entry is + * deliberately narrow — bare "now" / "legacy" / "previously" appear in + * legitimate technical prose, so the rule only matches the specific + * historical-narration constructions that have to come out. + */ +const HISTORICAL_PATTERNS: readonly RegExp[] = [ + /\bcodex\s+flagged\b/iu, + /\bbefore\s+the\s+fix\b/iu, + /\bafter\s+the\s+fix\b/iu, + /\bbefore\s+the\s+refactor\b/iu, + /\bafter\s+the\s+refactor\b/iu, + /\bthe\s+[a-z][\w-]*\s+refactor\b/iu, + /\bwe\s+used\s+to\b/iu, + /\bthis\s+used\s+to\b/iu, + /\bused\s+to\s+be\b/iu, + /\bno\s+longer\b/iu, + /\bkept\s+for\s+(?:backwards|backward|legacy|compat)\b/iu, + /\b(?:was|were)\s+a\s+(?:footgun|bug)\b/iu, + /\bhistorical(?:ly)?\b/iu, + /\balpine[-\s]?era\b/iu +]; + +function commentText(comment: TSESTree.Comment): string { + if (comment.type === "Line") { + return comment.value.trim(); + } + return comment.value + .split("\n") + .map((line) => line.replace(/^\s*\*?/u, "")) + .join(" ") + .trim(); +} + +function looksLikeJsDoc(comment: TSESTree.Comment): boolean { + return comment.type === "Block" && comment.value.startsWith("*"); +} + +export const noHistoricalCommentsRule = createRule<[], MessageIds>({ + name: RULE_NAME, + meta: { + type: "suggestion", + docs: { + description: + "Disallow comments that frame code relative to what it used to do or to a past incident ('Codex flagged X', 'before the fix', 'after the refactor', 'we used to', 'no longer'). Source comments must describe the current invariant; history belongs in the commit message or PR description, where it doesn't rot when the code changes again." + }, + schema: [], + messages: { + historicalComment: + "Historical narration ({{snippet}}). Source comments describe the current invariant, not what the code used to do — move the history to the commit message or delete the comment." + } + }, + defaultOptions: [], + create(context) { + return { + Program() { + const comments = context.sourceCode.getAllComments(); + + for (const comment of comments) { + if (looksLikeJsDoc(comment)) { + continue; + } + const text = commentText(comment); + if (text === "") { + continue; + } + for (const pattern of HISTORICAL_PATTERNS) { + const match = pattern.exec(text); + if (match !== null) { + const matchedPhrase = match[0]; + const snippet = + matchedPhrase.length > 40 + ? `${matchedPhrase.slice(0, 40)}…` + : matchedPhrase; + context.report({ + loc: comment.loc, + messageId: "historicalComment", + data: { snippet } + }); + break; + } + } + } + } + }; + } +}); diff --git a/eslint-plugin-comment-hygiene/tests/rules/no-historical-comments.test.ts b/eslint-plugin-comment-hygiene/tests/rules/no-historical-comments.test.ts new file mode 100644 index 0000000..b5ca092 --- /dev/null +++ b/eslint-plugin-comment-hygiene/tests/rules/no-historical-comments.test.ts @@ -0,0 +1,88 @@ +import { + RULE_NAME, + noHistoricalCommentsRule +} from "../../src/rules/no-historical-comments"; +import { ruleTester } from "../test-utils/ruleTester"; + +ruleTester.run(RULE_NAME, noHistoricalCommentsRule, { + valid: [ + // Plain technical prose that names "now" or "legacy" without + // describing past code state. + { code: `// Active session cookies are signed with the rotating key.` }, + { code: `// The legacy /v1 route stays mounted for partner integrations.` }, + { code: `// Returns the user record matching the trimmed email.` }, + { + code: `// Sentry traces sample at 0 so OTel stays the single tracer.` + }, + // JSDoc is exempt — it describes the API surface, not history. + { + code: `/**\n * @param value the user-supplied URL\n */\nfunction f(v: string) { return v; }` + }, + // Mentioning a concept that happens to contain "fix" but isn't narration. + { code: `// Fix-rate ticks are debounced at the source.` }, + // "Now" as a real noun (current time semantics). + { code: `// now() reads the wall clock through the time util.` } + ], + invalid: [ + { + code: `// Codex flagged this — keep it simple.`, + errors: [{ messageId: "historicalComment" }] + }, + { + code: `// Before the fix, this swallowed the 401 silently.`, + errors: [{ messageId: "historicalComment" }] + }, + { + code: `// After the fix the discriminator branches on error type.`, + errors: [{ messageId: "historicalComment" }] + }, + { + code: `// After the auth refactor /refresh returns null for anon.`, + errors: [{ messageId: "historicalComment" }] + }, + { + code: `// The observability refactor moved Sentry to error-only.`, + errors: [{ messageId: "historicalComment" }] + }, + { + code: `// We used to read the JWT directly from headers.`, + errors: [{ messageId: "historicalComment" }] + }, + { + code: `// This used to be a string; it's now an enum.`, + errors: [{ messageId: "historicalComment" }] + }, + { + code: `// The userId used to be derived from the cookie payload.`, + errors: [{ messageId: "historicalComment" }] + }, + { + code: `// The store no longer caches the membership row.`, + errors: [{ messageId: "historicalComment" }] + }, + { + code: `// Kept for backwards compat with the v0 client.`, + errors: [{ messageId: "historicalComment" }] + }, + { + code: `// Silent fallback was a footgun: misspelled values went unnoticed.`, + errors: [{ messageId: "historicalComment" }] + }, + { + code: `// Historically the API returned the user object inline.`, + errors: [{ messageId: "historicalComment" }] + }, + { + code: `// Alpine-era workaround for the missing libc symbol.`, + errors: [{ messageId: "historicalComment" }] + }, + { + code: `/* Codex flagged this multi-line scenario as a real finding. */`, + errors: [{ messageId: "historicalComment" }] + }, + { + code: `/*\n * The error envelope used to be flat;\n * it now nests under success/error.\n */`, + errors: [{ messageId: "historicalComment" }] + } + ] +});