eslint-plugin-traceability 1.15.0 → 1.16.1

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/README.md +61 -13
  3. package/lib/src/index.js +59 -0
  4. package/lib/src/rules/helpers/require-story-core.js +1 -1
  5. package/lib/src/rules/helpers/require-story-helpers.d.ts +10 -3
  6. package/lib/src/rules/helpers/require-story-helpers.js +84 -7
  7. package/lib/src/rules/no-redundant-annotation.js +73 -55
  8. package/lib/src/rules/require-branch-annotation.js +2 -2
  9. package/lib/src/rules/require-req-annotation.js +2 -2
  10. package/lib/src/rules/require-story-annotation.js +8 -3
  11. package/lib/src/rules/require-traceability.js +8 -9
  12. package/lib/src/utils/annotation-checker.js +23 -4
  13. package/lib/tests/cli-error-handling.test.js +10 -1
  14. package/lib/tests/integration/cli-integration.test.js +5 -0
  15. package/lib/tests/integration/require-traceability-aliases.integration.test.js +126 -0
  16. package/lib/tests/plugin-default-export-and-configs.test.js +23 -0
  17. package/lib/tests/rules/auto-fix-behavior-008.test.js +7 -7
  18. package/lib/tests/rules/error-reporting.test.js +1 -1
  19. package/lib/tests/rules/no-redundant-annotation.test.js +20 -0
  20. package/lib/tests/rules/require-story-annotation.test.js +75 -10
  21. package/lib/tests/rules/require-story-helpers.test.js +224 -0
  22. package/lib/tests/rules/require-story-utils.test.d.ts +7 -0
  23. package/lib/tests/rules/require-story-utils.test.js +158 -0
  24. package/lib/tests/utils/annotation-checker-branches.test.d.ts +5 -0
  25. package/lib/tests/utils/annotation-checker-branches.test.js +103 -0
  26. package/lib/tests/utils/annotation-scope-analyzer.test.js +134 -0
  27. package/lib/tests/utils/branch-annotation-helpers.test.js +66 -0
  28. package/package.json +2 -2
  29. package/user-docs/api-reference.md +71 -15
  30. package/user-docs/examples.md +24 -13
  31. package/user-docs/migration-guide.md +127 -4
  32. package/user-docs/traceability-overview.md +116 -0
  33. package/lib/tests/integration/dogfooding-validation.test.js +0 -129
  34. /package/lib/tests/integration/{dogfooding-validation.test.d.ts → require-traceability-aliases.integration.test.d.ts} +0 -0
@@ -6,6 +6,12 @@ describe("annotation-scope-analyzer helpers (Story 027.0-DEV-REDUNDANT-ANNOTATIO
6
6
  const key = (0, annotation_scope_analyzer_1.toStoryReqKey)("docs/stories/001.story.md", "REQ-ONE");
7
7
  expect(key).toBe("docs/stories/001.story.md|REQ-ONE");
8
8
  });
9
+ it("[REQ-DUPLICATION-DETECTION] normalizes missing story or requirement to empty segments", () => {
10
+ const noStory = (0, annotation_scope_analyzer_1.toStoryReqKey)(null, "REQ-ONE");
11
+ const noReq = (0, annotation_scope_analyzer_1.toStoryReqKey)("docs/stories/001.story.md", undefined);
12
+ expect(noStory).toBe("|REQ-ONE");
13
+ expect(noReq).toBe("docs/stories/001.story.md|");
14
+ });
9
15
  it("[REQ-DUPLICATION-DETECTION] extracts pairs from @story/@req sequences", () => {
10
16
  const text = `// @story docs/stories/001.story.md\n// @req REQ-ONE`;
11
17
  const pairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromText)(text);
@@ -13,6 +19,10 @@ describe("annotation-scope-analyzer helpers (Story 027.0-DEV-REDUNDANT-ANNOTATIO
13
19
  "docs/stories/001.story.md|REQ-ONE",
14
20
  ]);
15
21
  });
22
+ it("[REQ-DUPLICATION-DETECTION] returns empty set when text has no annotations", () => {
23
+ const pairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromText)("");
24
+ expect(pairs.size).toBe(0);
25
+ });
16
26
  it("[REQ-SCOPE-ANALYSIS] extracts pairs from @supports lines", () => {
17
27
  const text = `// @supports docs/stories/002.story.md REQ-A REQ-B OTHER`;
18
28
  const pairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromText)(text);
@@ -27,6 +37,10 @@ describe("annotation-scope-analyzer helpers (Story 027.0-DEV-REDUNDANT-ANNOTATIO
27
37
  const pairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromComments)(comments);
28
38
  expect(pairs.size).toBe(2);
29
39
  });
40
+ it("[REQ-DUPLICATION-DETECTION] returns empty set for empty comments list", () => {
41
+ const pairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromComments)([]);
42
+ expect(pairs.size).toBe(0);
43
+ });
30
44
  it("[REQ-DUPLICATION-DETECTION] determines full coverage correctly", () => {
31
45
  const parent = new Set([
32
46
  "story|REQ-ONE",
@@ -37,6 +51,11 @@ describe("annotation-scope-analyzer helpers (Story 027.0-DEV-REDUNDANT-ANNOTATIO
37
51
  expect((0, annotation_scope_analyzer_1.arePairsFullyCovered)(childCovered, parent)).toBe(true);
38
52
  expect((0, annotation_scope_analyzer_1.arePairsFullyCovered)(childNotCovered, parent)).toBe(false);
39
53
  });
54
+ it("[REQ-DUPLICATION-DETECTION] treats empty child or parent as not covered", () => {
55
+ const nonEmpty = new Set(["story|REQ-ONE"]);
56
+ expect((0, annotation_scope_analyzer_1.arePairsFullyCovered)(new Set(), nonEmpty)).toBe(false);
57
+ expect((0, annotation_scope_analyzer_1.arePairsFullyCovered)(nonEmpty, new Set())).toBe(false);
58
+ });
40
59
  it("[REQ-STATEMENT-SIGNIFICANCE] respects alwaysCovered and strictness levels", () => {
41
60
  const base = {
42
61
  strictness: "moderate",
@@ -49,6 +68,39 @@ describe("annotation-scope-analyzer helpers (Story 027.0-DEV-REDUNDANT-ANNOTATIO
49
68
  expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "ExpressionStatement" }, base, branchTypes)).toBe(true);
50
69
  expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "IfStatement" }, base, branchTypes)).toBe(false);
51
70
  });
71
+ it("[REQ-CONFIGURABLE-STRICTNESS] treats permissive mode as only honoring alwaysCovered list", () => {
72
+ const options = {
73
+ strictness: "permissive",
74
+ allowEmphasisDuplication: false,
75
+ maxScopeDepth: 3,
76
+ alwaysCovered: ["ReturnStatement"],
77
+ };
78
+ const branchTypes = ["IfStatement"];
79
+ expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "ReturnStatement" }, options, branchTypes)).toBe(true);
80
+ expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "ExpressionStatement" }, options, branchTypes)).toBe(false);
81
+ });
82
+ it("[REQ-CONFIGURABLE-STRICTNESS] treats strict mode as allowing any non-branch statement", () => {
83
+ const options = {
84
+ strictness: "strict",
85
+ allowEmphasisDuplication: false,
86
+ maxScopeDepth: 3,
87
+ alwaysCovered: [],
88
+ };
89
+ const branchTypes = ["IfStatement"];
90
+ expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "ExpressionStatement" }, options, branchTypes)).toBe(true);
91
+ expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "IfStatement" }, options, branchTypes)).toBe(false);
92
+ });
93
+ it("[REQ-STATEMENT-SIGNIFICANCE] returns false for null or non-node values", () => {
94
+ const options = {
95
+ strictness: "moderate",
96
+ allowEmphasisDuplication: false,
97
+ maxScopeDepth: 3,
98
+ alwaysCovered: [],
99
+ };
100
+ const branchTypes = [];
101
+ expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)(null, options, branchTypes)).toBe(false);
102
+ expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({}, options, branchTypes)).toBe(false);
103
+ });
52
104
  it("[REQ-SAFE-REMOVAL] computes removal range for full-line comment", () => {
53
105
  const source = `const x = 1;\n// @story docs/stories/001.story.md\nconst y = 2;\n`;
54
106
  const sourceCode = {
@@ -63,6 +115,77 @@ describe("annotation-scope-analyzer helpers (Story 027.0-DEV-REDUNDANT-ANNOTATIO
63
115
  const removed = source.slice(0, removalStart) + source.slice(removalEnd);
64
116
  expect(removed).toBe("const x = 1;\nconst y = 2;\n");
65
117
  });
118
+ it("[REQ-SAFE-REMOVAL] computes removal range for full-line comment with Windows newlines", () => {
119
+ const source = "const x = 1;\r\n// @story docs/stories/001.story.md\r\nconst y = 2;\r\n";
120
+ const sourceCode = {
121
+ getText() {
122
+ return source;
123
+ },
124
+ };
125
+ const start = source.indexOf("// @story");
126
+ const end = start + "// @story docs/stories/001.story.md".length;
127
+ const comment = { range: [start, end] };
128
+ const [removalStart, removalEnd] = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
129
+ const removed = source.slice(0, removalStart) + source.slice(removalEnd);
130
+ expect(removed).toBe("const x = 1;\r\nconst y = 2;\r\n");
131
+ });
132
+ it("[REQ-SAFE-REMOVAL] computes removal range for full-line comment with standalone CR newline", () => {
133
+ const source = "const x = 1;\r// @story docs/stories/001.story.md\rconst y = 2;\r";
134
+ const sourceCode = {
135
+ getText() {
136
+ return source;
137
+ },
138
+ };
139
+ const start = source.indexOf("// @story");
140
+ const end = start + "// @story docs/stories/001.story.md".length;
141
+ const comment = { range: [start, end] };
142
+ const [removalStart, removalEnd] = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
143
+ const removed = source.slice(0, removalStart) + source.slice(removalEnd);
144
+ expect(removed).toBe("const x = 1;\rconst y = 2;\r");
145
+ });
146
+ it("[REQ-SAFE-REMOVAL] computes removal range for inline comment", () => {
147
+ const source = "const x = 1; // @story docs/stories/001.story.md\nconst y = 2;\n";
148
+ const sourceCode = {
149
+ getText() {
150
+ return source;
151
+ },
152
+ };
153
+ const start = source.indexOf("// @story");
154
+ const end = start + "// @story docs/stories/001.story.md".length;
155
+ const comment = { range: [start, end] };
156
+ const [removalStart, removalEnd] = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
157
+ const removed = source.slice(0, removalStart) + source.slice(removalEnd);
158
+ expect(removed).toBe("const x = 1; \nconst y = 2;\n");
159
+ });
160
+ it("[REQ-SAFE-REMOVAL] consumes trailing spaces and tabs following a full-line comment", () => {
161
+ const source = "const x = 1;\n// @story docs/stories/001.story.md \nconst y = 2;\n";
162
+ const sourceCode = {
163
+ getText() {
164
+ return source;
165
+ },
166
+ };
167
+ const start = source.indexOf("// @story");
168
+ const end = start + "// @story docs/stories/001.story.md".length;
169
+ const comment = { range: [start, end] };
170
+ const [removalStart, removalEnd] = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
171
+ const removed = source.slice(0, removalStart) + source.slice(removalEnd);
172
+ expect(removed).toBe("const x = 1;\nconst y = 2;\n");
173
+ });
174
+ it("[REQ-SAFE-REMOVAL] handles full-line comment at end of file without trailing newline", () => {
175
+ const source = "const x = 1;\n// @story docs/stories/001.story.md";
176
+ const sourceCode = {
177
+ getText() {
178
+ return source;
179
+ },
180
+ };
181
+ const start = source.indexOf("// @story");
182
+ const end = start + "// @story docs/stories/001.story.md".length;
183
+ const comment = { range: [start, end] };
184
+ const [removalStart, removalEnd] = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
185
+ const removed = source.slice(0, removalStart) + source.slice(removalEnd);
186
+ expect(removed).toBe("const x = 1;\n");
187
+ expect(removalEnd).toBe(source.length);
188
+ });
66
189
  it("[REQ-SAFE-REMOVAL] returns [0, 0] for comments with invalid range length (EXPECTS EXPECTED_RANGE_LENGTH usage)", () => {
67
190
  const source = "const x = 1;";
68
191
  const sourceCode = {
@@ -74,4 +197,15 @@ describe("annotation-scope-analyzer helpers (Story 027.0-DEV-REDUNDANT-ANNOTATIO
74
197
  const range = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
75
198
  expect(range).toEqual([0, 0]);
76
199
  });
200
+ it("[REQ-SAFE-REMOVAL] returns [0, 0] when comment range is not an array", () => {
201
+ const source = "const x = 1;";
202
+ const sourceCode = {
203
+ getText() {
204
+ return source;
205
+ },
206
+ };
207
+ const comment = { range: null };
208
+ const range = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
209
+ expect(range).toEqual([0, 0]);
210
+ });
77
211
  });
@@ -44,4 +44,70 @@ describe("validateBranchTypes helper (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () =
44
44
  }));
45
45
  });
46
46
  });
47
+ it("should gather SwitchCase comment text via gatherBranchCommentText (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () => {
48
+ // Fake SourceCode-like object with lines aligned to PRE_COMMENT_OFFSET logic
49
+ const sourceCode = {
50
+ lines: [
51
+ "// @story first part",
52
+ "// continuation second part",
53
+ "case 1:",
54
+ ],
55
+ getCommentsBefore: () => [],
56
+ getText: jest.fn(),
57
+ };
58
+ // SwitchCase-like node with loc.start.line corresponding to "case 1:" line (line 3)
59
+ const switchCaseNode = {
60
+ type: "SwitchCase",
61
+ loc: {
62
+ start: { line: 3, column: 0 },
63
+ end: { line: 3, column: 7 },
64
+ },
65
+ };
66
+ const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, switchCaseNode);
67
+ // Expect combined text using space separator and preserving leading //
68
+ expect(text).toBe("// @story first part // continuation second part");
69
+ });
70
+ it("should gather comment text for CatchClause and loop nodes via gatherBranchCommentText (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () => {
71
+ // CatchClause: comments come from getCommentsBefore when beforeText already contains @story
72
+ const catchComments = [
73
+ { type: "Line", value: "@story catch branch story" },
74
+ { type: "Line", value: "additional info" },
75
+ ];
76
+ const sourceCodeCatch = {
77
+ getCommentsBefore: jest.fn().mockReturnValue(catchComments),
78
+ getText: jest.fn().mockReturnValue("@story existing beforeText"),
79
+ lines: [],
80
+ };
81
+ const catchNode = {
82
+ type: "CatchClause",
83
+ loc: {
84
+ start: { line: 10, column: 0 },
85
+ end: { line: 12, column: 1 },
86
+ },
87
+ };
88
+ const catchText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCodeCatch, catchNode);
89
+ expect(sourceCodeCatch.getCommentsBefore).toHaveBeenCalledWith(catchNode);
90
+ expect(catchText).toContain("@story catch branch story");
91
+ expect(catchText).toContain("additional info");
92
+ // Loop node: ForStatement currently uses beforeComments.map(extractCommentValue).join(" ")
93
+ const loopComments = [
94
+ { type: "Line", value: "@story loop branch story" },
95
+ { type: "Block", value: "loop details" },
96
+ ];
97
+ const sourceCodeLoop = {
98
+ getCommentsBefore: jest.fn().mockReturnValue(loopComments),
99
+ getText: jest.fn().mockReturnValue("@story loop beforeText"),
100
+ lines: [],
101
+ };
102
+ const forNode = {
103
+ type: "ForStatement",
104
+ loc: {
105
+ start: { line: 20, column: 0 },
106
+ end: { line: 25, column: 1 },
107
+ },
108
+ };
109
+ const loopText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCodeLoop, forNode);
110
+ expect(sourceCodeLoop.getCommentsBefore).toHaveBeenCalledWith(forNode);
111
+ expect(loopText).toBe("@story loop branch story loop details");
112
+ });
47
113
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.15.0",
3
+ "version": "1.16.1",
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",
@@ -90,7 +90,7 @@
90
90
  "lint-staged": "^16.2.7",
91
91
  "prettier": "^3.6.2",
92
92
  "semantic-release": "25.0.2",
93
- "ts-jest": "^29.4.5",
93
+ "ts-jest": "^29.4.6",
94
94
  "typescript": "^5.9.3",
95
95
  "secretlint": "11.2.5",
96
96
  "@secretlint/secretlint-rule-preset-recommend": "11.2.5"
@@ -11,19 +11,30 @@ Security and dependency hygiene for the published package are enforced by the sa
11
11
 
12
12
  Each rule enforces traceability conventions in your code. Below is a summary of each rule exposed by this plugin.
13
13
 
14
+ For function-level traceability, new configurations should treat `traceability/require-traceability` as the **canonical** rule: it composes both story and requirement checks and understands both the newer `@supports` style and the legacy `@story` / `@req` pairing. The older keys `traceability/require-story-annotation` and `traceability/require-req-annotation` remain available as **backward-compatible aliases** so existing configs keep working, but they are no longer the primary entry points and are mainly useful when you need to tune severities independently. For new and multi-story scenarios, prefer `@supports` annotations; `@story` and `@req` remain valid and are still appropriate for simple single-story code paths where a consolidated `@supports` tag is not yet required.
15
+
14
16
  In addition to the core `@story` and `@req` annotations, the plugin also understands `@supports` for code that fulfills requirements from multiple stories—for example, a consuming project might use a path like
15
17
  `@supports docs/stories/010.0-PAYMENTS.story.md#REQ-PAYMENTS-REFUND`
16
18
  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
19
 
18
20
  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 for maintainers; typical end users can rely on the high-level guidance in this API reference and the [Migration Guide](migration-guide.md).
19
21
 
22
+ ### Function-level rules overview
23
+
24
+ For function-level traceability, the plugin exposes a unified rule and two legacy keys:
25
+
26
+ - `traceability/require-traceability` is the **canonical function-level rule** for new configurations. It ensures functions and methods have both story coverage and requirement coverage, and it accepts either `@supports` (preferred) or legacy `@story` / `@req` annotations.
27
+ - `traceability/require-story-annotation` and `traceability/require-req-annotation` are **backward-compatible aliases** that focus on the story and requirement aspects separately. They are retained for existing configurations and share the same underlying implementation model as the unified rule, but new ESLint configs should normally rely on `traceability/require-traceability` rather than enabling these legacy keys directly.
28
+
29
+ All three rule keys can still be configured individually if you need fine-grained control (for example, to tune severities separately), but the recommended and strict presets enable `traceability/require-traceability` by default and keep the legacy keys primarily for projects that adopted them before the unified rule existed.
30
+
20
31
  ### traceability/require-traceability
21
32
 
22
33
  Description: Unified function-level traceability rule that composes the behavior of `traceability/require-story-annotation` and `traceability/require-req-annotation`. When enabled, it enforces that in‑scope functions and methods carry both a story reference (`@story` or an equivalent `@supports` tag) and at least one requirement reference (`@req` or, when configured, `@supports`). The recommended flat‑config presets in this plugin enable `traceability/require-traceability` by default alongside the legacy rule keys for backward compatibility, so that existing configurations referring to `traceability/require-story-annotation` or `traceability/require-req-annotation` continue to work without change.
23
34
 
24
35
  ### traceability/require-story-annotation
25
36
 
26
- Description: Ensures every function declaration has a JSDoc comment with an `@story` annotation referencing the related user story. When you adopt multi-story `@supports` annotations, this rule also accepts `@supports` as an alternative way to prove story coverage, so either `@story` or at least one `@supports` tag will satisfy the presence check. When run with `--fix`, the rule inserts a single-line placeholder JSDoc `@story` annotation above missing functions, methods, TypeScript declare functions, and interface method signatures using a built-in template aligned with Story 008.0. This template is now configurable on a per-rule basis, and the rule exposes an explicit auto-fix toggle so you can choose between diagnostic-only behavior and automatic placeholder insertion. The default template remains aligned with Story 008.0, but you can now customize it per rule configuration and optionally disable auto-fix entirely when you only want diagnostics without edits.
37
+ Description: **Legacy function-level key:** This rule key is retained for backward compatibility and conceptually composes the same checks as `traceability/require-traceability`. New configurations should normally enable `traceability/require-traceability` instead and rely on this key only when you need to tune it independently. Ensures every function declaration has a traceability annotation, preferring `@supports` for story coverage while still accepting legacy `@story` annotations referencing the related user story. When you adopt multi-story `@supports` annotations, this rule also accepts `@supports` as an alternative way to prove story coverage, so either `@story` or at least one `@supports` tag will satisfy the presence check. When run with `--fix`, the rule inserts a single-line placeholder JSDoc `@story` annotation above missing functions, methods, TypeScript declare functions, and interface method signatures using a built-in template aligned with Story 008.0. This template is now configurable on a per-rule basis, and the rule exposes an explicit auto-fix toggle so you can choose between diagnostic-only behavior and automatic placeholder insertion. The default template remains aligned with Story 008.0, but you can now customize it per rule configuration and optionally disable auto-fix entirely when you only want diagnostics without edits.
27
38
 
28
39
  Options:
29
40
 
@@ -32,6 +43,7 @@ Options:
32
43
  - `annotationTemplate` (string, optional) – Overrides the default placeholder JSDoc used when inserting missing `@story` annotations for functions and non-method constructs. When omitted or blank, the built-in template from Story 008.0 is used.
33
44
  - `methodAnnotationTemplate` (string, optional) – Overrides the default placeholder JSDoc used when inserting missing `@story` annotations for class methods and TypeScript method signatures. When omitted or blank, falls back to `annotationTemplate` if provided, otherwise the built-in template.
34
45
  - `autoFix` (boolean, optional) – When set to `false`, disables all automatic fix behavior for this rule while retaining its suggestions and diagnostics. When omitted or `true`, the rule behaves as before, inserting placeholder annotations in `--fix` mode.
46
+ - `excludeTestCallbacks` (boolean, optional) – When `true` (default), excludes anonymous arrow functions that are direct callbacks to common test framework functions (for example, Jest/Mocha/Vitest `describe`/`it`/`test`/`beforeEach`/`afterEach`/`beforeAll`/`afterAll`, plus focused/skipped/concurrent variants such as `fdescribe`, `xdescribe`, `fit`, `xit`, `test.concurrent`, `describe.concurrent`) from function-level annotation requirements. This assumes those test files are already covered by file-level `@supports` annotations and `traceability/require-test-traceability`. When set to `false`, these callbacks are treated like any other arrow function and must be annotated when in-scope.
35
47
 
36
48
  Default Severity: `error`
37
49
  Example:
@@ -46,9 +58,11 @@ function initAuth() {
46
58
  }
47
59
  ```
48
60
 
61
+ Among the supported scopes, anonymous callbacks passed directly to common test framework functions are excluded from annotation requirements by default via `excludeTestCallbacks`; projects that prefer stricter enforcement for these callbacks can disable this exclusion by setting `excludeTestCallbacks: false` in their rule configuration.
62
+
49
63
  ### traceability/require-req-annotation
50
64
 
51
- Description: Ensures that function-like constructs consistently declare their linked requirement using an `@req` annotation in JSDoc. The rule targets the same function-like node types as `traceability/require-story-annotation` (standard function declarations, non-arrow function expressions used as callbacks or assignments, class/object methods, TypeScript declare functions, and interface method signatures), and enforces that each of them has at least one `@req` tag in the nearest associated JSDoc comment. When you adopt multi-story `@supports` annotations, this rule also treats `@supports story-path REQ-ID...` tags as satisfying the requirement coverage check, although deep verification of requirement IDs continues to be handled by `traceability/valid-req-reference`. Arrow functions (`ArrowFunctionExpression`) are not currently checked by this rule.
65
+ Description: **Legacy function-level key:** This rule key is retained for backward compatibility and conceptually composes the same checks as `traceability/require-traceability`. New configurations should normally enable `traceability/require-traceability` instead and rely on this key only when you need to tune it independently. Ensures that function-like constructs consistently declare their linked requirements via traceability annotations, preferring `@supports` when possible while still accepting `@req`. The rule targets the same function-like node types as `traceability/require-story-annotation` (standard function declarations, non-arrow function expressions used as callbacks or assignments, class/object methods, TypeScript declare functions, and interface method signatures), and enforces that each of them has at least one `@req` tag in the nearest associated JSDoc comment. When you adopt multi-story `@supports` annotations, this rule also treats `@supports story-path REQ-ID...` tags as satisfying the requirement coverage check, although deep verification of requirement IDs continues to be handled by `traceability/valid-req-reference`. Arrow functions (`ArrowFunctionExpression`) are not currently checked by this rule.
52
66
 
53
67
  This rule is typically used alongside `traceability/require-story-annotation` so that each function-level traceability block includes both an `@story` and an `@req` annotation, but it can also be enabled independently if you only want to enforce requirement linkage. Unlike `traceability/require-story-annotation`, this rule does not currently provide an auto-fix mode for inserting placeholder `@req` annotations; it only reports missing or malformed requirement annotations on the configured scopes.
54
68
 
@@ -72,7 +86,7 @@ function initAuth() {
72
86
 
73
87
  ### traceability/require-branch-annotation
74
88
 
75
- Description: Ensures significant code branches (if/else chains, loops, switch cases, try/catch) have both `@story` and `@req` annotations in nearby comments. When you adopt multi-story `@supports` annotations, a single `@supports <storyPath> <REQ-ID>...` line placed in any of the valid branch comment locations is treated as satisfying both the story and requirement presence checks for that branch, while detailed format validation of the `@supports` value (including story paths and requirement IDs) continues to be handled by `traceability/valid-annotation-format`, `traceability/valid-story-reference`, and `traceability/valid-req-reference`.
89
+ Description: Ensures significant code branches (if/else chains, loops, switch cases, try/catch) have traceability coverage, typically via a single `@supports` line, while still accepting legacy `@story` and `@req` annotations in nearby comments. When you adopt multi-story `@supports` annotations, a single `@supports <storyPath> <REQ-ID>...` line placed in any of the valid branch comment locations is treated as satisfying both the story and requirement presence checks for that branch, while detailed format validation of the `@supports` value (including story paths and requirement IDs) continues to be handled by `traceability/valid-annotation-format`, `traceability/valid-story-reference`, and `traceability/valid-req-reference`.
76
90
 
77
91
  For most branches, the rule looks for annotations in comments immediately preceding the branch keyword (for example, the line above an `if` or `for` statement). For `catch` clauses and `else if` branches, the rule is formatter-aware and accepts annotations in additional positions that common formatters like Prettier use when they reflow code.
78
92
 
@@ -112,7 +126,7 @@ if (error) {
112
126
 
113
127
  ### traceability/valid-annotation-format
114
128
 
115
- Description: Validates that all traceability annotations (`@story`, `@req`) follow the correct JSDoc or inline comment format. When run with `--fix`, the rule limits changes to safe `@story` path suffix normalization only—for example, adding `.md` when the path ends with `.story`, or adding `.story.md` when the base path has no extension—using targeted replacements implemented in the `getFixedStoryPath` and `reportInvalidStoryFormatWithFix` helpers. It does not change directories, infer new story names, or modify any surrounding comment text or whitespace, in line with Story 008.0. Selective disabling of this suffix-normalization auto-fix behavior is available via the `autoFix` option, which defaults to `true` for backward compatibility.
129
+ Description: Validates that all traceability annotations (`@supports` as the preferred form for new code, plus legacy `@story` and `@req`) follow the correct JSDoc or inline comment format. When run with `--fix`, the rule limits changes to safe `@story` path suffix normalization only—for example, adding `.md` when the path ends with `.story`, or adding `.story.md` when the base path has no extension—using targeted replacements implemented in the `getFixedStoryPath` and `reportInvalidStoryFormatWithFix` helpers. It does not change directories, infer new story names, or modify any surrounding comment text or whitespace, in line with Story 008.0. Selective disabling of this suffix-normalization auto-fix behavior is available via the `autoFix` option, which defaults to `true` for backward compatibility.
116
130
 
117
131
  Options:
118
132
 
@@ -150,21 +164,34 @@ The `valid-annotation-format` rule is intentionally **backward compatible** with
150
164
  Deep requirement checking for both `@req` and `@supports` is handled by the `valid-req-reference` rule in the plugin's internal docs. Advanced edge cases and internal semantics are mainly of interest to maintainers; typical end users can rely on the options and examples in this API reference when configuring the rule for their projects.
151
165
 
152
166
  Default Severity: `error`
153
- Example:
167
+
168
+ Primary example (recommended `@supports` style):
169
+
170
+ ```javascript
171
+ /**
172
+ * @supports docs/stories/005.0-DEV-VALIDATION.story.md#REQ-FORMAT-VALIDATION
173
+ */
174
+ function example() {
175
+ // ...
176
+ }
177
+ ```
178
+
179
+ Legacy example (`@story` + `@req`, still valid but not preferred for new code):
154
180
 
155
181
  ```javascript
156
182
  /**
157
183
  * @story docs/stories/005.0-DEV-VALIDATION.story.md
158
184
  * @req REQ-FORMAT-VALIDATION
159
185
  */
160
- function example() {
186
+ function legacyExample() {
161
187
  // ...
162
188
  }
163
189
  ```
164
190
 
165
191
  ### traceability/valid-story-reference
166
192
 
167
- Description: Checks that the file paths in `@story` annotations point to existing story markdown files.
193
+ Description: Checks that the story file paths used by traceability annotations point to existing story markdown files in an `@supports`‑first world. Modern code typically encodes story paths in `@supports` tags (for example, `@supports docs/stories/010.0-PAYMENTS.story.md#REQ-ID`), but this rule continues to operate on the underlying `@story` values for file‑existence checks, keeping legacy annotations and mixed blocks fully supported.
194
+
168
195
  Options:
169
196
  Configure rule behavior using an options object with these properties:
170
197
 
@@ -190,31 +217,57 @@ Example configuration:
190
217
  ```
191
218
 
192
219
  Default Severity: `error`
193
- Example:
220
+
221
+ Primary example (recommended `@supports` style, with a companion `@story` used for existence checks):
222
+
223
+ ```javascript
224
+ /**
225
+ * @supports docs/stories/006.0-DEV-STORY-EXISTS.story.md#REQ-STORY-EXISTS
226
+ * @story docs/stories/006.0-DEV-STORY-EXISTS.story.md // used for file-existence validation; kept for backward compatibility
227
+ */
228
+ function example() {
229
+ // ...
230
+ }
231
+ ```
232
+
233
+ Legacy-only example (`@story` + `@req`, still supported as an input to this rule):
194
234
 
195
235
  ```javascript
196
236
  /**
197
237
  * @story docs/stories/006.0-DEV-STORY-EXISTS.story.md
198
238
  * @req REQ-STORY-EXISTS
199
239
  */
200
- function example() {
240
+ function legacyExample() {
201
241
  // ...
202
242
  }
203
243
  ```
204
244
 
205
245
  ### traceability/valid-req-reference
206
246
 
207
- Description: Verifies that the IDs used in `@req` annotations match known requirement identifiers.
247
+ Description: Verifies that the requirement IDs used in traceability annotations match known requirement identifiers, whether they appear in modern `@supports` tags or in legacy `@req` annotations.
248
+
208
249
  Options: None
209
250
  Default Severity: `error`
210
- Example:
251
+
252
+ Primary example (recommended `@supports` style with requirement IDs):
253
+
254
+ ```javascript
255
+ /**
256
+ * @supports docs/stories/007.0-DEV-REQ-REFERENCE.story.md#REQ-VALID-REFERENCE REQ-VALID-REFERENCE-EDGE
257
+ */
258
+ function example() {
259
+ // ...
260
+ }
261
+ ```
262
+
263
+ Legacy example (`@story` + `@req`, still valid for backward compatibility):
211
264
 
212
265
  ```javascript
213
266
  /**
214
267
  * @story docs/stories/007.0-DEV-REQ-REFERENCE.story.md
215
268
  * @req REQ-VALID-REFERENCE
216
269
  */
217
- function example() {
270
+ function legacyExample() {
218
271
  // ...
219
272
  }
220
273
  ```
@@ -296,12 +349,14 @@ Behavior notes:
296
349
 
297
350
  Default Severity: `warn`
298
351
 
299
- This rule is **not** enabled in the `recommended` or `strict` presets by default. To use it, add it explicitly to your ESLint configuration with an appropriate severity level:
352
+ This rule is enabled at severity `warn` in both the `recommended` and `strict` presets. You can override its behavior in your own configuration — for example, by raising it to `error` for stricter enforcement, or by explicitly disabling it if you prefer to keep statement-level duplication.
353
+
354
+ Configuration example (override preset severity from `warn` to `error`):
300
355
 
301
356
  ```jsonc
302
357
  {
303
358
  "rules": {
304
- "traceability/no-redundant-annotation": "warn",
359
+ "traceability/no-redundant-annotation": "error",
305
360
  },
306
361
  }
307
362
  ```
@@ -327,7 +382,7 @@ Main behaviors:
327
382
 
328
383
  ```js
329
384
  /**
330
- * @supports docs/stories/010.0-PAYMENTS.story.md#REQ-PAYMENTS-REFUND REQ-PAYMENTS-EDGE
385
+ * @supports docs/stories/010.0-PAYMENTS.story.md#REQ-PAYMENTS-REFUND REQ-PAYMENTS-REFUND-EDGE
331
386
  */
332
387
  ```
333
388
 
@@ -432,6 +487,7 @@ Core rules enabled by the `recommended` preset:
432
487
  - `traceability/valid-story-reference`: `error`
433
488
  - `traceability/valid-req-reference`: `error`
434
489
  - `traceability/require-test-traceability`: `error`
490
+ - `traceability/no-redundant-annotation`: `warn`
435
491
 
436
492
  Usage:
437
493
 
@@ -43,7 +43,21 @@ npx eslint "src/**/*.js"
43
43
 
44
44
  ## 3. CLI Invocation Example
45
45
 
46
- You can use the plugin without a config file by specifying rules inline:
46
+ You can use the plugin without a config file by specifying rules inline. The recommended approach for new setups is to use the unified `traceability/require-traceability` rule:
47
+
48
+ ```bash
49
+ npx eslint --no-eslintrc \
50
+ --rule "traceability/require-traceability:error" \
51
+ sample.js
52
+ ```
53
+
54
+ This unified function-level rule enforces both story and requirement coverage via `@supports` (preferred) or, for backward compatibility, via legacy `@story`/`@req` annotations.
55
+
56
+ Replace `sample.js` with your JavaScript or TypeScript file.
57
+
58
+ ### 3.1 Legacy aliases (for existing configurations)
59
+
60
+ If you have older configurations that already refer to the legacy keys `traceability/require-story-annotation` and `traceability/require-req-annotation`, you can still enable them explicitly to avoid breaking those setups:
47
61
 
48
62
  ```bash
49
63
  npx eslint --no-eslintrc \
@@ -53,9 +67,7 @@ npx eslint --no-eslintrc \
53
67
  ```
54
68
 
55
69
  - `--no-eslintrc` tells ESLint to ignore user configs.
56
- - `--rule` options enable the traceability rules you need.
57
-
58
- Replace `sample.js` with your JavaScript or TypeScript file.
70
+ - `--rule` options enable either the unified rule (recommended for new configurations) or the legacy aliases when you must preserve older setups.
59
71
 
60
72
  ## 4. Linting a Specific Directory
61
73
 
@@ -103,6 +115,9 @@ describe("Story 021.0-DEV-TEST-TRACEABILITY", () => {
103
115
  // Act
104
116
  const result = performOperation(input);
105
117
 
118
+ // Assert
119
+ const result = performOperation(input);
120
+
106
121
  // Assert
107
122
  expect(result).toBe("edge-ok");
108
123
  });
@@ -125,13 +140,11 @@ In this version, annotations are placed immediately before each significant bran
125
140
 
126
141
  ```ts
127
142
  function pickCategory(score: number): string {
128
- // @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
129
- // @req REQ-BRANCH-DETECTION
143
+ // @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-BRANCH-DETECTION
130
144
  if (score >= 80) {
131
145
  return "high";
132
146
  }
133
- // @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
134
- // @req REQ-DUAL-POSITION-DETECTION-ELSE-IF
147
+ // @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION-ELSE-IF
135
148
  else if (score >= 50) {
136
149
  return "medium";
137
150
  }
@@ -156,13 +169,11 @@ Prettier may reflow your `else if` line, wrap the condition, or move comments in
156
169
 
157
170
  ```ts
158
171
  function pickCategory(score: number): string {
159
- // @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
160
- // @req REQ-BRANCH-DETECTION
172
+ // @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-BRANCH-DETECTION
161
173
  if (score >= 80) {
162
174
  return "high";
163
175
  } else if (score >= 50) {
164
- // @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
165
- // @req REQ-DUAL-POSITION-DETECTION-ELSE-IF
176
+ // @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION-ELSE-IF
166
177
  return "medium";
167
178
  } else {
168
179
  return "low";
@@ -173,6 +184,6 @@ function pickCategory(score: number): string {
173
184
  Depending on your Prettier version and configuration, the exact layout of the `else if` line and braces may differ, but as long as your annotations are in one of the supported locations, the rule will accept them.
174
185
 
175
186
  - Notes:
176
- - For most branch types, `traceability/require-branch-annotation` associates comments immediately before the branch keyword (such as `if`, `else`, `switch`, `case`) with that branch.
187
+ - For most branch types, `traceability/require-branch-annotation` associates comments immediately before the branch keyword (such as `if`, `else`, `switch`, `case`) with that branch. Branches can be annotated either with a single `@supports` line (preferred), or with the older `@story`/`@req` pair for backward compatibility. The rule treats a valid `@supports` annotation as satisfying both the story and requirement presence checks.
177
188
  - For `catch` clauses and `else if` branches, the rule is formatter-aware and also looks at comments between the condition and the block, as well as the first comment-only lines inside the block body, so you do not need to fight Prettier if it moves your annotations.
178
189
  - When annotations exist in more than one place around an `else if` branch, the rule prefers comments immediately before the `else if` line, then comments between the condition and the block, and finally comments inside the block body, matching the behavior described in the API reference and stories `025.0` and `026.0`.