eslint-plugin-traceability 1.11.2 → 1.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,9 @@
1
- ## [1.11.2](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.11.1...v1.11.2) (2025-12-06)
1
+ ## [1.11.3](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.11.2...v1.11.3) (2025-12-06)
2
2
 
3
3
 
4
4
  ### Bug Fixes
5
5
 
6
- * ignore inline-code annotation references in comment normalization ([118d743](https://github.com/voder-ai/eslint-plugin-traceability/commit/118d743ff8c8bf1dcb0854b898480c641cc727c8))
6
+ * ensure catch clause annotations remain valid after prettier formatting ([ca38772](https://github.com/voder-ai/eslint-plugin-traceability/commit/ca3877248d344d7b94ed0059eca9b80b14a04772))
7
7
 
8
8
  # Changelog
9
9
 
package/README.md CHANGED
@@ -8,7 +8,7 @@ Created autonomously by [voder.ai](https://voder.ai).
8
8
 
9
9
  ## Installation
10
10
 
11
- Prerequisites: Node.js >=18.18.0 and ESLint v9+.
11
+ Prerequisites: Node.js 18.18.x, 20.x, 22.14.x, or 24.x and ESLint v9+.
12
12
 
13
13
  1. Using npm
14
14
  npm install --save-dev eslint-plugin-traceability
@@ -55,7 +55,7 @@ export default [
55
55
  - `traceability/valid-story-reference` Validates that `@story` references point to existing story files. (See the rule documentation in the plugin's user guide.)
56
56
  - `traceability/valid-req-reference` Validates that `@req` references point to existing requirement IDs. (See the rule documentation in the plugin's user guide.)
57
57
  - `traceability/require-test-traceability` Enforces traceability conventions in test files by requiring file-level `@supports` annotations, story references in `describe` blocks, and `[REQ-...]` prefixes in `it`/`test` names. (See the rule documentation in the plugin's user guide.)
58
- - `traceability/prefer-implements-annotation` Recommends migration from legacy `@story`/`@req` annotations to `@supports` (opt-in; disabled by default in the presets and must be explicitly enabled). (See the rule documentation in the plugin's user guide.)
58
+ - `traceability/prefer-supports-annotation` Recommends migration from legacy `@story`/`@req` annotations to `@supports` (opt-in; disabled by default in the presets and must be explicitly enabled). The legacy rule name `traceability/prefer-implements-annotation` remains available as a deprecated alias. (See the rule documentation in the plugin's user guide.)
59
59
 
60
60
  Configuration options: For detailed per-rule options (such as scopes, branch types, and story directory settings), see the individual rule docs in the plugin's user guide and the consolidated [API Reference](user-docs/api-reference.md).
61
61
 
@@ -10,13 +10,7 @@ import type { Rule } from "eslint";
10
10
  * @req REQ-MAINTENANCE-API-EXPORT - Expose maintenance utilities alongside core plugin exports
11
11
  */
12
12
  import { detectStaleAnnotations, updateAnnotationReferences, batchUpdateAnnotations, verifyAnnotations, generateMaintenanceReport } from "./maintenance";
13
- /**
14
- * @story docs/stories/002.0-DEV-ESLINT-CONFIG.story.md
15
- * @req REQ-RULE-LIST - Enumerate supported rule file names for plugin discovery
16
- */
17
- declare const RULE_NAMES: readonly ["require-story-annotation", "require-req-annotation", "require-branch-annotation", "valid-annotation-format", "valid-story-reference", "valid-req-reference", "prefer-implements-annotation", "require-test-traceability"];
18
- type RuleName = (typeof RULE_NAMES)[number];
19
- declare const rules: Record<RuleName, Rule.RuleModule>;
13
+ declare const rules: Record<string, Rule.RuleModule>;
20
14
  /**
21
15
  * Plugin metadata used by ESLint for debugging and caching.
22
16
  *
package/lib/src/index.js CHANGED
@@ -74,6 +74,34 @@ RULE_NAMES.forEach(
74
74
  };
75
75
  }
76
76
  });
77
+ /**
78
+ * @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-RULE-NAME
79
+ * Wire up traceability/prefer-supports-annotation as the primary rule name and
80
+ * traceability/prefer-implements-annotation as its deprecated alias.
81
+ */
82
+ {
83
+ const implementsRule = rules["prefer-implements-annotation"];
84
+ if (implementsRule) {
85
+ const originalMeta = implementsRule.meta ?? {};
86
+ const preferSupportsRule = {
87
+ ...implementsRule,
88
+ meta: {
89
+ ...originalMeta,
90
+ deprecated: false,
91
+ },
92
+ };
93
+ rules["prefer-supports-annotation"] = preferSupportsRule;
94
+ const implementsMeta = (implementsRule.meta =
95
+ implementsRule.meta ?? {});
96
+ implementsMeta.deprecated = true;
97
+ implementsMeta.replacedBy = ["prefer-supports-annotation"];
98
+ if (implementsMeta.docs &&
99
+ typeof implementsMeta.docs.description === "string") {
100
+ implementsMeta.docs.description +=
101
+ " (deprecated alias: use traceability/prefer-supports-annotation instead)";
102
+ }
103
+ }
104
+ }
77
105
  /**
78
106
  * Plugin metadata used by ESLint for debugging and caching.
79
107
  *
@@ -78,6 +78,61 @@ function validateBranchTypes(context) {
78
78
  ? options.branchTypes
79
79
  : Array.from(exports.DEFAULT_BRANCH_TYPES);
80
80
  }
81
+ /**
82
+ * Extract the raw value from a comment node.
83
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
84
+ * @req REQ-TRACEABILITY-MAP-CALLBACK - Trace mapping of comment nodes to their text values
85
+ */
86
+ function extractCommentValue(_c) {
87
+ return _c.value;
88
+ }
89
+ /**
90
+ * Gather annotation text for CatchClause nodes, supporting both before-catch and inside-catch positions.
91
+ * @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
92
+ * @req REQ-DUAL-POSITION-DETECTION
93
+ * @req REQ-FALLBACK-LOGIC
94
+ */
95
+ function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
96
+ if (/@story\b/.test(beforeText) || /@req\b/.test(beforeText)) {
97
+ return beforeText;
98
+ }
99
+ const getCommentsInside = sourceCode.getCommentsInside;
100
+ if (node.body && typeof getCommentsInside === "function") {
101
+ try {
102
+ const insideComments = getCommentsInside(node.body) || [];
103
+ const insideText = insideComments.map(extractCommentValue).join(" ");
104
+ if (insideText) {
105
+ return insideText;
106
+ }
107
+ }
108
+ catch {
109
+ // fall through to line-based fallback
110
+ }
111
+ }
112
+ if (node.body && node.body.loc && node.body.loc.start && node.body.loc.end) {
113
+ const lines = sourceCode.lines;
114
+ const startIndex = node.body.loc.start.line - 1;
115
+ const endIndex = node.body.loc.end.line - 1;
116
+ const comments = [];
117
+ let i = startIndex + 1;
118
+ while (i <= endIndex) {
119
+ const line = lines[i];
120
+ if (!line || !line.trim()) {
121
+ break;
122
+ }
123
+ if (!/^\s*(\/\/|\/\*)/.test(line)) {
124
+ break;
125
+ }
126
+ comments.push(line.trim());
127
+ i++;
128
+ }
129
+ const insideText = comments.join(" ");
130
+ if (insideText) {
131
+ return insideText;
132
+ }
133
+ }
134
+ return beforeText;
135
+ }
81
136
  /**
82
137
  * Gather leading comment text for a branch node.
83
138
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
@@ -102,16 +157,12 @@ function gatherBranchCommentText(sourceCode, node) {
102
157
  }
103
158
  return comments.join(" ");
104
159
  }
105
- const comments = sourceCode.getCommentsBefore(node) || [];
106
- /**
107
- * Mapper to extract the text value from a comment node.
108
- * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
109
- * @req REQ-TRACEABILITY-MAP-CALLBACK - Trace mapping of comment nodes to their text values
110
- */
111
- function commentToValue(c) {
112
- return c.value;
160
+ const beforeComments = sourceCode.getCommentsBefore(node) || [];
161
+ const beforeText = beforeComments.map(extractCommentValue).join(" ");
162
+ if (node.type === "CatchClause") {
163
+ return gatherCatchClauseCommentText(sourceCode, node, beforeText);
113
164
  }
114
- return comments.map(commentToValue).join(" ");
165
+ return beforeText;
115
166
  }
116
167
  /**
117
168
  * Report missing @story annotation tag on a branch node when that branch lacks a corresponding @story reference in its comments.
@@ -168,8 +219,8 @@ function reportMissingReq(context, node, options) {
168
219
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
169
220
  * @req REQ-TRACEABILITY-FIX-ARROW - Trace fixer function used to insert missing @req
170
221
  */
171
- function insertReqFixer(fixer) {
172
- return fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @req <REQ-ID>\n`);
222
+ function insertReqFixer(fxer) {
223
+ return fxer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @req <REQ-ID>\n`);
173
224
  }
174
225
  context.report({
175
226
  node,
@@ -195,11 +246,39 @@ function getBranchAnnotationInfo(sourceCode, node) {
195
246
  const text = gatherBranchCommentText(sourceCode, node);
196
247
  const missingStory = !/@story\b/.test(text);
197
248
  const missingReq = !/@req\b/.test(text);
198
- const indent = sourceCode.lines[node.loc.start.line - 1].match(/^(\s*)/)?.[1] || "";
199
- const insertPos = sourceCode.getIndexFromLoc({
249
+ let indent = sourceCode.lines[node.loc.start.line - 1].match(/^(\s*)/)?.[1] || "";
250
+ let insertPos = sourceCode.getIndexFromLoc({
200
251
  line: node.loc.start.line,
201
252
  column: 0,
202
253
  });
254
+ if (node.type === "CatchClause" && node.body) {
255
+ const bodyNode = node.body;
256
+ const bodyStatements = Array.isArray(bodyNode.body)
257
+ ? bodyNode.body
258
+ : undefined;
259
+ const firstStatement = bodyStatements && bodyStatements.length > 0
260
+ ? bodyStatements[0]
261
+ : undefined;
262
+ if (firstStatement && firstStatement.loc && firstStatement.loc.start) {
263
+ const firstLine = firstStatement.loc.start.line;
264
+ const innerIndent = sourceCode.lines[firstLine - 1].match(/^(\s*)/)?.[1] || "";
265
+ indent = innerIndent;
266
+ insertPos = sourceCode.getIndexFromLoc({
267
+ line: firstLine,
268
+ column: 0,
269
+ });
270
+ }
271
+ else if (bodyNode.loc && bodyNode.loc.start) {
272
+ const blockLine = bodyNode.loc.start.line;
273
+ const blockIndent = sourceCode.lines[blockLine - 1].match(/^(\s*)/)?.[1] || "";
274
+ const innerIndent = `${blockIndent} `;
275
+ indent = innerIndent;
276
+ insertPos = sourceCode.getIndexFromLoc({
277
+ line: blockLine,
278
+ column: 0,
279
+ });
280
+ }
281
+ }
203
282
  return { missingStory, missingReq, indent, insertPos };
204
283
  }
205
284
  /**
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ /**
7
+ * Prettier integration tests for CatchClause annotation positions.
8
+ * @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
9
+ * @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-PRETTIER-COMPATIBILITY
10
+ */
11
+ const path_1 = __importDefault(require("path"));
12
+ const child_process_1 = require("child_process");
13
+ describe("CatchClause annotations with Prettier (Story 025.0-DEV-CATCH-ANNOTATION-POSITION)", () => {
14
+ const eslintPkgDir = path_1.default.dirname(require.resolve("eslint/package.json"));
15
+ const eslintCliPath = path_1.default.join(eslintPkgDir, "bin", "eslint.js");
16
+ const configPath = path_1.default.resolve(__dirname, "../../eslint.config.js");
17
+ const prettierPackageJson = require.resolve("prettier/package.json");
18
+ const prettierCliPath = path_1.default.join(path_1.default.dirname(prettierPackageJson), "bin", "prettier.cjs");
19
+ function runEslintWithRequireBranchAnnotation(code) {
20
+ const args = [
21
+ "--no-config-lookup",
22
+ "--config",
23
+ configPath,
24
+ "--stdin",
25
+ "--stdin-filename",
26
+ "catch.js",
27
+ "--rule",
28
+ "no-unused-vars:off",
29
+ "--rule",
30
+ "no-magic-numbers:off",
31
+ "--rule",
32
+ "no-undef:off",
33
+ "--rule",
34
+ "no-console:off",
35
+ "--rule",
36
+ "traceability/require-branch-annotation:error",
37
+ ];
38
+ return (0, child_process_1.spawnSync)(process.execPath, [eslintCliPath, ...args], {
39
+ encoding: "utf-8",
40
+ input: code,
41
+ });
42
+ }
43
+ function formatWithPrettier(source) {
44
+ const result = (0, child_process_1.spawnSync)(process.execPath, [prettierCliPath, "--parser", "typescript"], {
45
+ encoding: "utf-8",
46
+ input: source,
47
+ });
48
+ if (result.status !== 0) {
49
+ throw new Error(`Prettier formatting failed: ${result.stderr || result.stdout}`);
50
+ }
51
+ return result.stdout;
52
+ }
53
+ it("[REQ-PRETTIER-COMPATIBILITY-BEFORE] accepts code where annotations start before catch but are moved inside by Prettier", () => {
54
+ const original = `
55
+ function doSomething() {
56
+ return 42;
57
+ }
58
+
59
+ function handleError(error) {
60
+ console.error(error);
61
+ }
62
+
63
+ // @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
64
+ // @req REQ-BRANCH-TRY
65
+ try {
66
+ doSomething();
67
+ }
68
+ // @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
69
+ // @req REQ-CATCH-PATH
70
+ catch (error) {
71
+ handleError(error);
72
+ }
73
+ `;
74
+ const formatted = formatWithPrettier(original);
75
+ // Sanity check: Prettier should move the branch annotations inside the catch body.
76
+ expect(formatted).toContain("catch (error) {");
77
+ const catchIndex = formatted.indexOf("catch (error) {");
78
+ const storyIndex = formatted.indexOf("@story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md");
79
+ expect(storyIndex).toBeGreaterThan(catchIndex);
80
+ const result = runEslintWithRequireBranchAnnotation(formatted);
81
+ expect(result.status).toBe(0);
82
+ });
83
+ it("[REQ-PRETTIER-COMPATIBILITY-INSIDE] accepts code where annotations start inside the catch body and are preserved by Prettier", () => {
84
+ const original = `
85
+ function doSomething() {
86
+ return 42;
87
+ }
88
+
89
+ function handleError(error) {
90
+ console.error(error);
91
+ }
92
+
93
+ // @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
94
+ // @req REQ-BRANCH-TRY
95
+ try {
96
+ doSomething();
97
+ } catch (error) {
98
+ // @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
99
+ // @req REQ-CATCH-INSIDE
100
+ handleError(error);
101
+ }
102
+ `;
103
+ const formatted = formatWithPrettier(original);
104
+ // Sanity: annotations should still be associated with the catch body after formatting.
105
+ expect(formatted).toContain("catch (error) {");
106
+ const catchIndex = formatted.indexOf("catch (error) {");
107
+ const storyIndex = formatted.indexOf("@story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md");
108
+ expect(storyIndex).toBeGreaterThan(catchIndex);
109
+ const result = runEslintWithRequireBranchAnnotation(formatted);
110
+ expect(result.status).toBe(0);
111
+ });
112
+ it("[REQ-PRETTIER-COMPATIBILITY-EMPTY] accepts empty catch blocks with inside-catch annotations after Prettier formatting", () => {
113
+ const original = `
114
+ function doSomething() {
115
+ return 42;
116
+ }
117
+
118
+ // @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
119
+ // @req REQ-BRANCH-TRY
120
+ try {
121
+ doSomething();
122
+ } catch (error) {
123
+ // @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
124
+ // @req REQ-CATCH-EMPTY
125
+ }
126
+ `;
127
+ const formatted = formatWithPrettier(original);
128
+ const result = runEslintWithRequireBranchAnnotation(formatted);
129
+ expect(result.status).toBe(0);
130
+ });
131
+ });
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ /**
7
+ * Performance tests for valid-annotation-format on large annotated files.
8
+ *
9
+ * @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-MULTILINE-SUPPORT REQ-FLEXIBLE-PARSING REQ-SYNTAX-VALIDATION
10
+ */
11
+ const eslint_1 = require("eslint");
12
+ const perf_hooks_1 = require("perf_hooks");
13
+ const valid_annotation_format_1 = __importDefault(require("../../src/rules/valid-annotation-format"));
14
+ /**
15
+ * Build a large source file containing many functions with traceability
16
+ * annotations in both line and block comments.
17
+ *
18
+ * The generated code mixes valid and invalid annotation formats to exercise
19
+ * parsing, multi-line handling, and error-reporting paths at scale without
20
+ * relying on auto-fix.
21
+ */
22
+ function buildLargeAnnotatedSource(functionCount, annotationsPerFunction) {
23
+ const lines = [];
24
+ for (let i = 0; i < functionCount; i += 1) {
25
+ // JSDoc-style block comment with multi-line @story/@req values.
26
+ lines.push("/**");
27
+ lines.push(" * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md");
28
+ lines.push(" * @req REQ-FORMAT-SPECIFICATION");
29
+ lines.push(" */");
30
+ // Additional line comments with a mix of valid and intentionally
31
+ // invalid formats (missing extensions, traversal, malformed IDs).
32
+ for (let j = 0; j < annotationsPerFunction; j += 1) {
33
+ const selector = (i + j) % 4;
34
+ if (selector === 0) {
35
+ lines.push("// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story");
36
+ }
37
+ else if (selector === 1) {
38
+ lines.push("// @req REQ-EXAMPLE-" + i.toString(10));
39
+ }
40
+ else if (selector === 2) {
41
+ lines.push("// @story ../outside-project.story.md");
42
+ }
43
+ else {
44
+ lines.push("// @req invalid-format-id");
45
+ }
46
+ }
47
+ lines.push(`function annotated_fn_${i}() {`);
48
+ lines.push(' return "ok";\n}');
49
+ }
50
+ return lines.join("\n");
51
+ }
52
+ describe("valid-annotation-format performance on large annotated files (Story 005.0-DEV-ANNOTATION-VALIDATION)", () => {
53
+ const ruleName = "traceability/valid-annotation-format";
54
+ it("[REQ-MULTILINE-SUPPORT][REQ-FLEXIBLE-PARSING] analyzes a large annotated file within a generous time budget", () => {
55
+ const linter = new eslint_1.Linter({ configType: "eslintrc" });
56
+ linter.defineRule(ruleName, valid_annotation_format_1.default);
57
+ // 150 functions each with several annotations provides a substantial
58
+ // volume of comments and annotation patterns without being extreme.
59
+ const source = buildLargeAnnotatedSource(150, 3);
60
+ const start = perf_hooks_1.performance.now();
61
+ const messages = linter.verify(source, {
62
+ parserOptions: { ecmaVersion: 2020, sourceType: "module" },
63
+ rules: {
64
+ [ruleName]: "error",
65
+ },
66
+ });
67
+ const durationMs = perf_hooks_1.performance.now() - start;
68
+ // Sanity check: we expect diagnostics for some invalid annotations so the
69
+ // rule is definitely executing its validation logic.
70
+ expect(messages.length).toBeGreaterThan(0);
71
+ // Guardrail: keep analysis comfortably under ~5 seconds on CI hardware.
72
+ expect(durationMs).toBeLessThan(5000);
73
+ });
74
+ });
@@ -59,6 +59,7 @@ describe("Plugin Default Export and Configs (Story 001.0-DEV-PLUGIN-SETUP)", ()
59
59
  "valid-req-reference",
60
60
  "prefer-implements-annotation",
61
61
  "require-test-traceability",
62
+ "prefer-supports-annotation",
62
63
  ];
63
64
  // Act: get actual rule names from plugin
64
65
  const actual = Object.keys(index_1.rules);
@@ -18,80 +18,87 @@ const ruleTester = new eslint_1.RuleTester({
18
18
  parserOptions: { ecmaVersion: 2020, sourceType: "module" },
19
19
  },
20
20
  });
21
- describe("prefer-implements-annotation rule (Story 010.3-DEV-MIGRATE-TO-SUPPORTS)", () => {
22
- ruleTester.run("prefer-implements-annotation", prefer_implements_annotation_1.default, {
23
- valid: [
24
- {
25
- name: "[REQ-BACKWARD-COMP-VALIDATION] comment with only @story is ignored",
26
- code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n */\nfunction onlyStory() {}`,
27
- },
28
- {
29
- name: "[REQ-BACKWARD-COMP-VALIDATION] comment with only @req is ignored",
30
- code: `/**\n * @req REQ-ONLY\n */\nfunction onlyReq() {}`,
31
- },
32
- {
33
- name: "[REQ-BACKWARD-COMP-VALIDATION] comment with @supports only is ignored",
34
- code: `/**\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction alreadyImplements() {}`,
35
- },
36
- {
37
- name: "[REQ-BACKWARD-COMP-VALIDATION] comment with @story and @supports but no @req is ignored",
38
- code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction storyAndSupportsNoReq() {}`,
39
- },
40
- {
41
- name: "[REQ-BACKWARD-COMP-VALIDATION] comment with @req and @supports but no @story is ignored",
42
- code: `/**\n * @req REQ-ANNOTATION-REQUIRED\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction reqAndSupportsNoStory() {}`,
43
- },
44
- ],
45
- invalid: [
46
- {
47
- name: "[REQ-OPTIONAL-WARNING] single-story @story + @req block triggers preferImplements message",
48
- code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction legacy() {}`,
49
- output: `/**\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction legacy() {}`,
50
- errors: [{ messageId: "preferImplements" }],
51
- },
52
- {
53
- name: "[REQ-MULTI-STORY-DETECT] mixed @story/@req and @supports triggers cannotAutoFix",
54
- code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction mixed() {}`,
55
- errors: [
56
- {
57
- messageId: "cannotAutoFix",
58
- data: {
59
- reason: "comment mixes @story/@req with existing @supports annotations",
60
- },
21
+ describe("prefer-supports-annotation / prefer-implements-annotation aliasing (Story 010.3-DEV-MIGRATE-TO-SUPPORTS)", () => {
22
+ const valid = [
23
+ {
24
+ name: "[REQ-BACKWARD-COMP-VALIDATION] comment with only @story is ignored",
25
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n */\nfunction onlyStory() {}`,
26
+ },
27
+ {
28
+ name: "[REQ-BACKWARD-COMP-VALIDATION] comment with only @req is ignored",
29
+ code: `/**\n * @req REQ-ONLY\n */\nfunction onlyReq() {}`,
30
+ },
31
+ {
32
+ name: "[REQ-BACKWARD-COMP-VALIDATION] comment with @supports only is ignored",
33
+ code: `/**\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction alreadyImplements() {}`,
34
+ },
35
+ {
36
+ name: "[REQ-BACKWARD-COMP-VALIDATION] comment with @story and @supports but no @req is ignored",
37
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction storyAndSupportsNoReq() {}`,
38
+ },
39
+ {
40
+ name: "[REQ-BACKWARD-COMP-VALIDATION] comment with @req and @supports but no @story is ignored",
41
+ code: `/**\n * @req REQ-ANNOTATION-REQUIRED\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction reqAndSupportsNoStory() {}`,
42
+ },
43
+ ];
44
+ const invalid = [
45
+ {
46
+ name: "[REQ-OPTIONAL-WARNING] single-story @story + @req block triggers preferImplements message",
47
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction legacy() {}`,
48
+ output: `/**\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction legacy() {}`,
49
+ errors: [{ messageId: "preferImplements" }],
50
+ },
51
+ {
52
+ name: "[REQ-MULTI-STORY-DETECT] mixed @story/@req and @supports triggers cannotAutoFix",
53
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction mixed() {}`,
54
+ errors: [
55
+ {
56
+ messageId: "cannotAutoFix",
57
+ data: {
58
+ reason: "comment mixes @story/@req with existing @supports annotations",
61
59
  },
62
- ],
63
- },
64
- {
65
- name: "[REQ-MULTI-STORY-DETECT] multiple @story paths in same block trigger multiStoryDetected",
66
- code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md\n * @req REQ-BRANCH-DETECTION\n */\nfunction multiStory() {}`,
67
- errors: [{ messageId: "multiStoryDetected" }],
68
- },
69
- {
70
- name: "[REQ-AUTO-FIX] single @story + single @req auto-fixes to single @supports line",
71
- code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction autoFixSingleReq() {}`,
72
- output: `/**\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction autoFixSingleReq() {}`,
73
- errors: [{ messageId: "preferImplements" }],
74
- },
75
- {
76
- name: "[REQ-SINGLE-STORY-FIX] single @story with multiple @req lines auto-fixes to single @supports line containing all REQ IDs",
77
- code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ONE\n * @req REQ-TWO\n * @req REQ-THREE\n */\nfunction autoFixMultiReq() {}`,
78
- output: `/**\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ONE REQ-TWO REQ-THREE\n */\nfunction autoFixMultiReq() {}`,
79
- errors: [{ messageId: "preferImplements" }],
80
- },
81
- {
82
- name: "[REQ-AUTO-FIX] complex @req content (extra description) does not auto-fix but still warns",
83
- code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED must handle extra description\n */\nfunction complexReqNoAutoFix() {}`,
84
- errors: [{ messageId: "preferImplements" }],
85
- },
86
- {
87
- name: "[REQ-AUTO-FIX] complex @story content (extra description) does not auto-fix but still warns",
88
- code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md additional descriptive text\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction complexStoryNoAutoFix() {}`,
89
- errors: [{ messageId: "preferImplements" }],
90
- },
91
- ],
60
+ },
61
+ ],
62
+ },
63
+ {
64
+ name: "[REQ-MULTI-STORY-DETECT] multiple @story paths in same block trigger multiStoryDetected",
65
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md\n * @req REQ-BRANCH-DETECTION\n */\nfunction multiStory() {}`,
66
+ errors: [{ messageId: "multiStoryDetected" }],
67
+ },
68
+ {
69
+ name: "[REQ-AUTO-FIX] single @story + single @req auto-fixes to single @supports line",
70
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction autoFixSingleReq() {}`,
71
+ output: `/**\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction autoFixSingleReq() {}`,
72
+ errors: [{ messageId: "preferImplements" }],
73
+ },
74
+ {
75
+ name: "[REQ-SINGLE-STORY-FIX] single @story with multiple @req lines auto-fixes to single @supports line containing all REQ IDs",
76
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ONE\n * @req REQ-TWO\n * @req REQ-THREE\n */\nfunction autoFixMultiReq() {}`,
77
+ output: `/**\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ONE REQ-TWO REQ-THREE\n */\nfunction autoFixMultiReq() {}`,
78
+ errors: [{ messageId: "preferImplements" }],
79
+ },
80
+ {
81
+ name: "[REQ-AUTO-FIX] complex @req content (extra description) does not auto-fix but still warns",
82
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED must handle extra description\n */\nfunction complexReqNoAutoFix() {}`,
83
+ errors: [{ messageId: "preferImplements" }],
84
+ },
85
+ {
86
+ name: "[REQ-AUTO-FIX] complex @story content (extra description) does not auto-fix but still warns",
87
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md additional descriptive text\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction complexStoryNoAutoFix() {}`,
88
+ errors: [{ messageId: "preferImplements" }],
89
+ },
90
+ ];
91
+ ruleTester.run("prefer-implements-annotation", prefer_implements_annotation_1.default, {
92
+ valid,
93
+ invalid,
94
+ });
95
+ ruleTester.run("prefer-supports-annotation", prefer_implements_annotation_1.default, {
96
+ valid,
97
+ invalid,
92
98
  });
93
99
  });
94
100
  describe("prefer-implements-annotation configuration severity (REQ-CONFIG-SEVERITY)", () => {
101
+ // Story 010.3 / REQ-RULE-NAME: verify aliasing semantics for new primary rule name and deprecated alias
95
102
  test("rule is disabled by default in recommended and strict presets (not present in preset rule maps)", () => {
96
103
  const recommended = src_1.configs.recommended;
97
104
  expect(Array.isArray(recommended)).toBe(true);
@@ -99,27 +106,34 @@ describe("prefer-implements-annotation configuration severity (REQ-CONFIG-SEVERI
99
106
  expect(firstConfig).toBeDefined();
100
107
  const rules = firstConfig.rules || {};
101
108
  expect(rules["traceability/prefer-implements-annotation"]).toBeUndefined();
109
+ expect(rules["traceability/prefer-supports-annotation"]).toBeUndefined();
102
110
  const strict = src_1.configs.strict;
103
111
  expect(Array.isArray(strict)).toBe(true);
104
112
  const strictFirstConfig = strict[0];
105
113
  expect(strictFirstConfig).toBeDefined();
106
114
  const strictRules = strictFirstConfig.rules || {};
107
115
  expect(strictRules["traceability/prefer-implements-annotation"]).toBeUndefined();
116
+ expect(strictRules["traceability/prefer-supports-annotation"]).toBeUndefined();
108
117
  });
109
118
  test("rule can be configured with severity 'warn' or 'error' in flat config", () => {
119
+ // Story 010.3 / REQ-RULE-NAME: both primary and alias rule keys must be accepted in flat config
110
120
  const flatWarnConfig = {
111
121
  files: ["**/*.ts"],
112
122
  rules: {
113
123
  "traceability/prefer-implements-annotation": "warn",
124
+ "traceability/prefer-supports-annotation": "warn",
114
125
  },
115
126
  };
116
127
  expect(flatWarnConfig.rules["traceability/prefer-implements-annotation"]).toBe("warn");
128
+ expect(flatWarnConfig.rules["traceability/prefer-supports-annotation"]).toBe("warn");
117
129
  const flatErrorConfig = {
118
130
  files: ["**/*.ts"],
119
131
  rules: {
120
132
  "traceability/prefer-implements-annotation": "error",
133
+ "traceability/prefer-supports-annotation": "error",
121
134
  },
122
135
  };
123
136
  expect(flatErrorConfig.rules["traceability/prefer-implements-annotation"]).toBe("error");
137
+ expect(flatErrorConfig.rules["traceability/prefer-supports-annotation"]).toBe("error");
124
138
  });
125
139
  });
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Unit tests for CatchClause insert position calculation.
5
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
6
+ * @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
7
+ * @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-PRETTIER-AUTOFIX
8
+ */
9
+ const branch_annotation_helpers_1 = require("../../src/utils/branch-annotation-helpers");
10
+ describe("CatchClause insert position (Story 025.0-DEV-CATCH-ANNOTATION-POSITION)", () => {
11
+ it("[REQ-PRETTIER-AUTOFIX] inserts annotations at the first statement inside the catch body", () => {
12
+ const lines = [
13
+ "try {",
14
+ " doSomething();",
15
+ "}",
16
+ "catch (error) {",
17
+ " handleError(error);",
18
+ "}",
19
+ ];
20
+ const fixer = {
21
+ insertTextBeforeRange: jest.fn((r, t) => ({ r, t })),
22
+ };
23
+ const context = {
24
+ getSourceCode() {
25
+ return {
26
+ lines,
27
+ getCommentsBefore() {
28
+ return [];
29
+ },
30
+ getIndexFromLoc({ line, column }) {
31
+ // simple line/column to index mapping for the test: assume each line ends with "\n"
32
+ const prefix = lines.slice(0, line - 1).join("\n");
33
+ return prefix.length + (line > 1 ? 1 : 0) + column;
34
+ },
35
+ };
36
+ },
37
+ report({ fix }) {
38
+ // immediately invoke the fixer to exercise the insert position
39
+ if (typeof fix === "function") {
40
+ fix(fixer);
41
+ }
42
+ },
43
+ };
44
+ const node = {
45
+ type: "CatchClause",
46
+ loc: { start: { line: 4 } },
47
+ body: {
48
+ type: "BlockStatement",
49
+ loc: { start: { line: 4 } },
50
+ body: [
51
+ {
52
+ type: "ExpressionStatement",
53
+ loc: { start: { line: 5 } },
54
+ },
55
+ ],
56
+ },
57
+ };
58
+ const storyFixCountRef = { count: 0 };
59
+ (0, branch_annotation_helpers_1.reportMissingAnnotations)(context, node, storyFixCountRef);
60
+ expect(fixer.insertTextBeforeRange).toHaveBeenCalledTimes(1);
61
+ const [range, text] = fixer.insertTextBeforeRange.mock.calls[0];
62
+ // ensure we are inserting before the first statement in the catch body (line 5)
63
+ const expectedIndex = context.getSourceCode().getIndexFromLoc({ line: 5, column: 0 });
64
+ expect(range).toEqual([expectedIndex, expectedIndex]);
65
+ // and that the inserted text is prefixed with the inner indentation from line 5
66
+ expect(text.startsWith(" ")).toBe(true);
67
+ });
68
+ });
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const branch_annotation_helpers_1 = require("../../src/utils/branch-annotation-helpers");
4
+ function createMockSourceCode(options) {
5
+ const { lines = [], commentsBefore = [], commentsInside = [] } = options;
6
+ return {
7
+ lines,
8
+ getCommentsBefore() {
9
+ return commentsBefore;
10
+ },
11
+ getCommentsInside(node) {
12
+ // exercise the code path that passes node.body into getCommentsInside
13
+ if (node && node.type === "BlockStatement") {
14
+ return commentsInside;
15
+ }
16
+ return [];
17
+ },
18
+ };
19
+ }
20
+ describe("gatherBranchCommentText CatchClause behavior (Story 025.0-DEV-CATCH-ANNOTATION-POSITION)", () => {
21
+ it("[REQ-DUAL-POSITION-DETECTION] prefers before-catch annotations when present", () => {
22
+ const sourceCode = createMockSourceCode({
23
+ commentsBefore: [
24
+ { value: "@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md" },
25
+ { value: "@req REQ-BRANCH-DETECTION" },
26
+ ],
27
+ commentsInside: [
28
+ { value: "@story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md" },
29
+ ],
30
+ });
31
+ const node = {
32
+ type: "CatchClause",
33
+ loc: { start: { line: 5 } },
34
+ body: { type: "BlockStatement" },
35
+ };
36
+ const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node);
37
+ expect(text).toContain("@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md");
38
+ expect(text).toContain("@req REQ-BRANCH-DETECTION");
39
+ });
40
+ it("[REQ-FALLBACK-LOGIC] falls back to inside-catch annotations when before-catch is missing", () => {
41
+ const sourceCode = createMockSourceCode({
42
+ commentsBefore: [],
43
+ commentsInside: [
44
+ { value: "@story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md" },
45
+ { value: "@req REQ-CATCH-PATH" },
46
+ ],
47
+ });
48
+ const node = {
49
+ type: "CatchClause",
50
+ loc: { start: { line: 10 } },
51
+ body: { type: "BlockStatement" },
52
+ };
53
+ const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node);
54
+ expect(text).toContain("@story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md");
55
+ expect(text).toContain("@req REQ-CATCH-PATH");
56
+ });
57
+ it("[REQ-FALLBACK-LOGIC] returns before-catch text when getCommentsInside is not available", () => {
58
+ const lines = [
59
+ "try {",
60
+ " doSomething();",
61
+ "}",
62
+ "catch (error) {",
63
+ " // body",
64
+ "}",
65
+ ];
66
+ const sourceCode = {
67
+ lines,
68
+ getCommentsBefore() {
69
+ return [
70
+ { value: "@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md" },
71
+ { value: "@req REQ-BRANCH-DETECTION" },
72
+ ];
73
+ },
74
+ // intentionally omit getCommentsInside so that the CatchClause path
75
+ // falls back to the before-catch comments.
76
+ };
77
+ const node = {
78
+ type: "CatchClause",
79
+ loc: { start: { line: 4 } },
80
+ body: { type: "BlockStatement" },
81
+ };
82
+ const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node);
83
+ expect(text).toContain("@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md");
84
+ expect(text).toContain("@req REQ-BRANCH-DETECTION");
85
+ });
86
+ it("[REQ-FALLBACK-LOGIC] collects inside-catch comments using line-based fallback when getCommentsInside is unavailable", () => {
87
+ const lines = [
88
+ "try {",
89
+ " doSomething();",
90
+ "} catch (error) {",
91
+ " // @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md",
92
+ " // @req REQ-CATCH-LINE-FALLBACK",
93
+ " handleError(error);",
94
+ "}",
95
+ ];
96
+ const sourceCode = {
97
+ lines,
98
+ getCommentsBefore() {
99
+ return [];
100
+ },
101
+ };
102
+ const node = {
103
+ type: "CatchClause",
104
+ loc: { start: { line: 3 } },
105
+ body: {
106
+ type: "BlockStatement",
107
+ loc: { start: { line: 3 }, end: { line: 7 } },
108
+ body: [],
109
+ },
110
+ };
111
+ const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node);
112
+ expect(text).toContain("@story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md");
113
+ expect(text).toContain("@req REQ-CATCH-LINE-FALLBACK");
114
+ });
115
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const reqAnnotationDetection_1 = require("../../src/utils/reqAnnotationDetection");
4
+ // Small helper to construct a minimal SourceCode-like object for the detection helpers.
5
+ function createMockSourceCode(options = {}) {
6
+ const { lines = null, text = "", commentsBefore = [] } = options;
7
+ return {
8
+ lines: lines ?? undefined,
9
+ getText() {
10
+ return text;
11
+ },
12
+ getCommentsBefore() {
13
+ return commentsBefore;
14
+ },
15
+ };
16
+ }
17
+ describe("reqAnnotationDetection advanced heuristics (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", () => {
18
+ it("[REQ-ANNOTATION-REQ-DETECTION] returns false when sourceCode is missing", () => {
19
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], undefined, {
20
+ loc: null,
21
+ });
22
+ expect(has).toBe(false);
23
+ });
24
+ it("[REQ-ANNOTATION-REQ-DETECTION] returns false when node is missing", () => {
25
+ const context = {
26
+ getSourceCode() {
27
+ return createMockSourceCode({ lines: ["/** @req REQ-TEST */"] });
28
+ },
29
+ };
30
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, undefined);
31
+ expect(has).toBe(false);
32
+ });
33
+ it("[REQ-ANNOTATION-REQ-DETECTION] inspects jsdoc and comments when advanced heuristics throw", () => {
34
+ const context = {
35
+ getSourceCode() {
36
+ // This object intentionally causes hasReqInAdvancedHeuristics to throw by
37
+ // providing a getCommentsBefore implementation that throws on access.
38
+ return {
39
+ getCommentsBefore() {
40
+ throw new Error("boom");
41
+ },
42
+ };
43
+ },
44
+ };
45
+ const jsdoc = { value: "/** @req REQ-FROM-JSDOC */" };
46
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(jsdoc, [], context, {
47
+ // Minimal shape – the helper will call into the mock sourceCode and trigger the throw
48
+ parent: {},
49
+ });
50
+ expect(has).toBe(true);
51
+ });
52
+ it("[REQ-ANNOTATION-REQ-DETECTION] treats @supports in comments as satisfying requirement", () => {
53
+ const context = {
54
+ getSourceCode() {
55
+ return createMockSourceCode();
56
+ },
57
+ };
58
+ const comments = [{ value: "// @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-X" }];
59
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, comments, context, {
60
+ parent: {},
61
+ });
62
+ expect(has).toBe(true);
63
+ });
64
+ it("[REQ-ANNOTATION-REQ-DETECTION] linesBeforeHasReq returns false when lines is not an array", () => {
65
+ const context = {
66
+ getSourceCode() {
67
+ // lines is null here, causing the helper to see a non-array and return false
68
+ return createMockSourceCode({ lines: null });
69
+ },
70
+ };
71
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, {
72
+ // Provide a minimal location so advanced heuristics try to use line info
73
+ loc: { start: { line: 5 } },
74
+ parent: {},
75
+ });
76
+ expect(has).toBe(false);
77
+ });
78
+ it("[REQ-ANNOTATION-REQ-DETECTION] linesBeforeHasReq returns false when startLine is not a number", () => {
79
+ const sourceCode = createMockSourceCode({ lines: ["// @req REQ-SHOULD-NOT-BE-SEEN"] });
80
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], { getSourceCode: () => sourceCode }, {
81
+ // loc is missing/undefined; startLine will not be a valid number
82
+ loc: undefined,
83
+ parent: {},
84
+ });
85
+ expect(has).toBe(false);
86
+ });
87
+ it("[REQ-ANNOTATION-REQ-DETECTION] parentChainHasReq returns false when getCommentsBefore is not a function and no leadingComments/parents have req", () => {
88
+ const context = {
89
+ getSourceCode() {
90
+ return {
91
+ // getCommentsBefore is not a function here
92
+ getCommentsBefore: 123,
93
+ };
94
+ },
95
+ };
96
+ const node = {
97
+ parent: {
98
+ leadingComments: [{ value: "no req here" }],
99
+ parent: {
100
+ leadingComments: [{ value: "still nothing" }],
101
+ },
102
+ },
103
+ };
104
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
105
+ expect(has).toBe(false);
106
+ });
107
+ it("[REQ-ANNOTATION-REQ-DETECTION] parentChainHasReq returns true when getCommentsBefore returns comments containing @req", () => {
108
+ const sourceCode = {
109
+ getCommentsBefore(n) {
110
+ if (n && n.isTargetParent) {
111
+ return [{ value: "/* @req REQ-FROM-PARENT */" }];
112
+ }
113
+ return [];
114
+ },
115
+ };
116
+ const context = {
117
+ getSourceCode() {
118
+ return sourceCode;
119
+ },
120
+ };
121
+ const node = {
122
+ parent: {
123
+ isTargetParent: true,
124
+ parent: {},
125
+ },
126
+ };
127
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
128
+ expect(has).toBe(true);
129
+ });
130
+ it("[REQ-ANNOTATION-REQ-DETECTION] fallbackTextBeforeHasReq returns false when getText is not a function", () => {
131
+ const context = {
132
+ getSourceCode() {
133
+ return {
134
+ // getText is not a function
135
+ getText: "not-a-function",
136
+ };
137
+ },
138
+ };
139
+ const node = {
140
+ range: [0, 10],
141
+ parent: {},
142
+ };
143
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
144
+ expect(has).toBe(false);
145
+ });
146
+ it("[REQ-ANNOTATION-REQ-DETECTION] fallbackTextBeforeHasReq returns false when node.range is not an array", () => {
147
+ const context = {
148
+ getSourceCode() {
149
+ return createMockSourceCode({ text: "/* @req REQ-IN-TEXT */" });
150
+ },
151
+ };
152
+ const node = {
153
+ // range is missing; helper should see non-array range and return false
154
+ range: null,
155
+ parent: {},
156
+ };
157
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
158
+ expect(has).toBe(false);
159
+ });
160
+ it("[REQ-ANNOTATION-REQ-DETECTION] fallbackTextBeforeHasReq returns true when text window contains @req", () => {
161
+ const fullText = `
162
+ // some header
163
+ /** @req REQ-IN-TEXT-WINDOW */
164
+ function foo() {}
165
+ `;
166
+ const context = {
167
+ getSourceCode() {
168
+ return createMockSourceCode({ text: fullText });
169
+ },
170
+ };
171
+ // Choose a range that starts after the @req comment so the "text before"
172
+ // window that the helper inspects includes the annotation.
173
+ const startIndex = fullText.indexOf("function foo");
174
+ const node = {
175
+ range: [startIndex, startIndex + 5],
176
+ parent: {},
177
+ };
178
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
179
+ expect(has).toBe(true);
180
+ });
181
+ it("[REQ-ANNOTATION-REQ-DETECTION] fallbackTextBeforeHasReq returns false when getText throws", () => {
182
+ const context = {
183
+ getSourceCode() {
184
+ return {
185
+ getText() {
186
+ throw new Error("boom from getText");
187
+ },
188
+ };
189
+ },
190
+ };
191
+ const node = {
192
+ range: [0, 10],
193
+ parent: {},
194
+ };
195
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
196
+ expect(has).toBe(false);
197
+ });
198
+ it("[REQ-ANNOTATION-REQ-DETECTION] hasReqInAdvancedHeuristics short-circuits and returns false when sourceCode is missing", () => {
199
+ const context = {
200
+ // No getSourceCode method at all – internal advanced heuristics
201
+ // should immediately return false and not throw.
202
+ };
203
+ const node = {
204
+ loc: { start: { line: 3 } },
205
+ range: [0, 10],
206
+ parent: {},
207
+ };
208
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, node);
209
+ expect(has).toBe(false);
210
+ });
211
+ it("[REQ-ANNOTATION-REQ-DETECTION] hasReqInAdvancedHeuristics short-circuits and returns false when node is missing", () => {
212
+ const context = {
213
+ getSourceCode() {
214
+ return createMockSourceCode({ text: "@req REQ-SHOULD-NOT-BE-SEEN" });
215
+ },
216
+ };
217
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(null, [], context, undefined);
218
+ expect(has).toBe(false);
219
+ });
220
+ it("[REQ-ANNOTATION-REQ-DETECTION] hasReqAnnotation returns true when jsdoc contains @supports and advanced heuristics are false", () => {
221
+ const context = {
222
+ getSourceCode() {
223
+ // Returning a sourceCode that will not satisfy any advanced heuristic
224
+ // (no lines, no comments, empty text).
225
+ return createMockSourceCode({ lines: [], text: "" });
226
+ },
227
+ };
228
+ const jsdoc = {
229
+ value: "/** @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-JSDOC-SUPPORTS */",
230
+ };
231
+ const node = {
232
+ parent: {},
233
+ };
234
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(jsdoc, [], context, node);
235
+ expect(has).toBe(true);
236
+ });
237
+ it("[REQ-ANNOTATION-REQ-DETECTION] falls back to jsdoc/comments when context.getSourceCode throws", () => {
238
+ const context = {
239
+ getSourceCode() {
240
+ throw new Error("boom from getSourceCode");
241
+ },
242
+ };
243
+ const jsdoc = { value: "/** @req REQ-FROM-GETSOURCECODE */" };
244
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(jsdoc, [], context, { parent: {} });
245
+ expect(has).toBe(true);
246
+ });
247
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.11.2",
3
+ "version": "1.11.3",
4
4
  "description": "A customizable ESLint plugin that enforces traceability annotations in your code, ensuring each implementation is linked to its requirement or test case.",
5
5
  "main": "lib/src/index.js",
6
6
  "types": "lib/src/index.d.ts",
@@ -41,7 +41,7 @@
41
41
  "safety:deps": "node scripts/ci-safety-deps.js",
42
42
  "audit:ci": "node scripts/ci-audit.js",
43
43
  "check:ci-artifacts": "node scripts/check-no-tracked-ci-artifacts.js",
44
- "security:secrets": "secretlint \"**/*\" --no-color",
44
+ "security:secrets": "secretlint \"**/*\"",
45
45
  "smoke-test": "./scripts/smoke-test.sh",
46
46
  "debug:cli": "node scripts/cli-debug.js",
47
47
  "debug:require-story": "node scripts/debug-require-story.js",
@@ -99,7 +99,7 @@
99
99
  "eslint": "^9.0.0"
100
100
  },
101
101
  "engines": {
102
- "node": ">=18.18.0"
102
+ "node": "^18.18.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
103
103
  },
104
104
  "overrides": {
105
105
  "glob": "12.0.0",
@@ -15,7 +15,7 @@ In addition to the core `@story` and `@req` annotations, the plugin also underst
15
15
  `@supports docs/stories/010.0-PAYMENTS.story.md#REQ-PAYMENTS-REFUND`
16
16
  to indicate that a given function supports a particular requirement from a payments story document within that project’s own `docs/stories` tree. For a detailed explanation of `@supports` behavior and validation, see [Migration Guide](migration-guide.md) (section **3.1 Multi-story @supports annotations**). Additional background on multi-story semantics is available in the project’s internal rule documentation, which is intended for maintainers rather than end users.
17
17
 
18
- The `prefer-implements-annotation` rule is an **opt-in migration helper** that is disabled by default and **not** part of any built-in preset. It can be enabled and given a severity like `"warn"` or `"error"` using normal ESLint rule configuration when you want to gradually encourage multi-story `@supports` usage. Detailed behavior and migration guidance are documented in the project’s internal rule documentation, which is targeted at maintainers; typical end users can rely on the high-level guidance in this API reference and the [Migration Guide](migration-guide.md).
18
+ The `prefer-supports-annotation` rule is an **opt-in migration helper** that is disabled by default and **not** part of any built-in preset. It can be enabled and given a severity like `"warn"` or `"error"` using normal ESLint rule configuration when you want to gradually encourage multi-story `@supports` usage. The legacy rule key `traceability/prefer-implements-annotation` remains available as a **deprecated alias** for backward compatibility, but new configurations should prefer `traceability/prefer-supports-annotation`. Detailed behavior and migration guidance are documented in the project’s internal rule documentation, which is targeted at maintainers; typical end users can rely on the high-level guidance in this API reference and the [Migration Guide](migration-guide.md).
19
19
 
20
20
  ### traceability/require-story-annotation
21
21
 
@@ -68,11 +68,17 @@ function initAuth() {
68
68
 
69
69
  ### traceability/require-branch-annotation
70
70
 
71
- Description: Ensures significant code branches (if/else, loops, switch cases, try/catch) have both `@story` and `@req` annotations in preceding comments.
71
+ Description: Ensures significant code branches (if/else, loops, switch cases, try/catch) have both `@story` and `@req` annotations in preceding comments. For `catch` clauses specifically, the rule accepts annotations either immediately before the `catch` keyword or as the first comment-only lines inside the catch block. This dual-position handling is designed to stay compatible with common formatters such as Prettier, which often move comments from before `catch` into the catch body.
72
+
72
73
  Options:
73
74
 
74
75
  - `branchTypes` (string[], optional) – AST node types that are treated as significant branches for annotation enforcement. Allowed values: "IfStatement", "SwitchCase", "TryStatement", "CatchClause", "ForStatement", "ForOfStatement", "ForInStatement", "WhileStatement", "DoWhileStatement". Default: ["IfStatement", "SwitchCase", "TryStatement", "CatchClause", "ForStatement", "ForOfStatement", "ForInStatement", "WhileStatement", "DoWhileStatement"]. If an invalid branch type is provided, the rule reports a configuration error for each invalid value.
75
76
 
77
+ Behavior notes:
78
+
79
+ - When both before-`catch` and inside-block annotations are present for the same catch clause, the comments immediately before `catch` take precedence for validation and reporting.
80
+ - When auto-fixing missing annotations on a catch clause, the rule inserts placeholder comments inside the catch body so that formatters like Prettier preserve them and do not move them to unexpected locations.
81
+
76
82
  Default Severity: `error`
77
83
  Example:
78
84
 
@@ -246,9 +252,9 @@ describe("Refunds flow docs/stories/010.0-PAYMENTS.story.md", () => {
246
252
  });
247
253
  ```
248
254
 
249
- ### traceability/prefer-implements-annotation
255
+ ### traceability/prefer-supports-annotation
250
256
 
251
- Description: An optional, opt-in migration helper that encourages converting legacy single‑story `@story` + `@req` JSDoc blocks into the newer multi‑story `@supports` format. The rule is **disabled by default** and is **not included in any built‑in preset**; you enable it explicitly and control its behavior entirely via ESLint severity (`"off" | "warn" | "error"`). It does not change what the core rules consider valid—it only adds migration recommendations and safe auto‑fixes on top of existing validation.
257
+ Description: An optional, opt-in migration helper that encourages converting legacy single‑story `@story` + `@req` JSDoc blocks into the newer multi‑story `@supports` format. The rule is **disabled by default** and is **not included in any built‑in preset**; you enable it explicitly and control its behavior entirely via ESLint severity (`"off" | "warn" | "error"`). It does not change what the core rules consider valid—it only adds migration recommendations and safe auto‑fixes on top of existing validation. The legacy rule key `traceability/prefer-implements-annotation` is still recognized as a **deprecated alias** for `traceability/prefer-supports-annotation` so that existing configurations continue to work unchanged.
252
258
 
253
259
  Options: None – this rule does not accept a configuration object. All tuning is done via the ESLint rule level (`"off"`, `"warn"`, `"error"`).
254
260
 
@@ -318,7 +324,7 @@ function issueRefund() {
318
324
  }
319
325
  ```
320
326
 
321
- With `traceability/prefer-implements-annotation` enabled and ESLint run with `--fix`, the rule rewrites it to:
327
+ With `traceability/prefer-supports-annotation` enabled (or its deprecated alias `traceability/prefer-implements-annotation`) and ESLint run with `--fix`, the rule rewrites it to:
322
328
 
323
329
  ```js
324
330
  /**
@@ -343,7 +349,9 @@ export default [
343
349
  traceability.configs.recommended,
344
350
  {
345
351
  rules: {
346
- "traceability/prefer-implements-annotation": "warn",
352
+ "traceability/prefer-supports-annotation": "warn",
353
+ // The deprecated alias is still honored if you prefer:
354
+ // "traceability/prefer-implements-annotation": "warn",
347
355
  },
348
356
  },
349
357
  ];
@@ -358,7 +366,7 @@ The plugin provides two built-in presets for easy configuration:
358
366
  Enables the **six core traceability rules** with severities tuned for common usage (most at `error`, with
359
367
  `traceability/valid-annotation-format` at `warn` to reduce noise). This `warn` level for `traceability/valid-annotation-format` is intentional to keep early adoption noise low, but you can safely raise it to `error` in projects that want strict enforcement of annotation formatting.
360
368
 
361
- The `prefer-implements-annotation` migration rule is **not included** in this (or any) preset and remains disabled by default. If you want to encourage or enforce multi-story `@supports` annotations, you must enable `traceability/prefer-implements-annotation` explicitly in your ESLint configuration and choose an appropriate severity (for example, `"warn"` during migration or `"error"` once fully adopted).
369
+ The `prefer-supports-annotation` migration rule (and its deprecated alias key `traceability/prefer-implements-annotation`) is **not included** in this (or any) preset and remains disabled by default. If you want to encourage or enforce multi-story `@supports` annotations, you must enable `traceability/prefer-supports-annotation` explicitly in your ESLint configuration and choose an appropriate severity (for example, `"warn"` during migration or `"error"` once fully adopted).
362
370
 
363
371
  Core rules enabled by the `recommended` preset:
364
372
 
@@ -381,7 +389,7 @@ export default [js.configs.recommended, traceability.configs.recommended];
381
389
 
382
390
  ### strict
383
391
 
384
- Currently mirrors the **recommended** preset, reserved for future stricter policies. As with the `recommended` preset, the `traceability/prefer-implements-annotation` rule is **not** enabled here by default and must be configured manually if desired.
392
+ Currently mirrors the **recommended** preset, reserved for future stricter policies. As with the `recommended` preset, the `traceability/prefer-supports-annotation` rule is **not** enabled here by default, and the deprecated alias key `traceability/prefer-implements-annotation` continues to be honored only when you opt into it manually in your own configuration.
385
393
 
386
394
  Usage:
387
395
 
@@ -57,18 +57,22 @@ function integrate() {}
57
57
 
58
58
  You **do not** need to change existing, single-story annotations that already use `@story` and `@req`. Migration to `@supports` is only recommended when a function or module genuinely implements requirements from more than one story file.
59
59
 
60
- #### Optional `prefer-implements-annotation` migration rule
60
+ #### Optional `prefer-supports-annotation` migration rule
61
61
 
62
- For teams that want to gradually migrate from `@story` + `@req` to `@supports`, the plugin provides an optional rule: `traceability/prefer-implements-annotation`.
62
+ For teams that want to gradually migrate from `@story` + `@req` to `@supports`, the plugin provides an optional rule: `traceability/prefer-supports-annotation`.
63
63
 
64
- - This rule is **disabled by default** and is **not** included in any built-in presets.
64
+ - This is the canonical rule name starting in v1.x.
65
+ - The legacy key `traceability/prefer-implements-annotation` remains supported as a **deprecated alias** for backward compatibility, but should not be used in new configurations.
66
+
67
+ - This rule is **disabled by default** and is **not** included in any built-in presets (the deprecated alias is also not enabled by any presets).
65
68
  - You can enable it with any standard ESLint severity (`"off"`, `"warn"`, or `"error"`) in your config, for example:
66
69
 
67
70
  ```js
68
71
  // excerpt from eslint.config.js
69
72
  {
70
73
  rules: {
71
- "traceability/prefer-implements-annotation": "warn",
74
+ "traceability/prefer-supports-annotation": "warn",
75
+ // "traceability/prefer-implements-annotation": "warn", // deprecated alias
72
76
  },
73
77
  }
74
78
  ```
@@ -101,7 +105,7 @@ Aligned with the internal rule behavior, the key cases are:
101
105
  - Comments that contain only `@supports` lines, and
102
106
  - Line comments such as `// @story ...`.
103
107
 
104
- These forms are still supported by the plugin and are not modified by `traceability/prefer-implements-annotation`.
108
+ These forms are still supported by the plugin and are not modified by `traceability/prefer-supports-annotation`.
105
109
 
106
110
  A typical migration path is:
107
111