eslint-plugin-traceability 1.12.0 → 1.13.0

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.
@@ -8,7 +8,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
8
8
  * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
9
9
  * @req REQ-AUTOFIX-MISSING - Verify ESLint --fix automatically adds missing @story annotations to functions
10
10
  * @req REQ-AUTOFIX-FORMAT - Verify ESLint --fix corrects simple annotation format issues for @story annotations
11
- * @supports docs/stories/008.0-DEV-AUTO-FIX.story.md REQ-AUTOFIX-MISSING REQ-AUTOFIX-FORMAT
11
+ * @req REQ-AUTOFIX-IDEMPOTENT - Verify ESLint --fix is idempotent and produces no changes on subsequent runs
12
+ * @req REQ-AUTOFIX-SINGLE-APPLICATION - Verify ESLint --fix does not apply the same fix multiple times or create duplicate annotations
13
+ * @supports docs/stories/008.0-DEV-AUTO-FIX.story.md REQ-AUTOFIX-MISSING REQ-AUTOFIX-FORMAT REQ-AUTOFIX-IDEMPOTENT REQ-AUTOFIX-SINGLE-APPLICATION
12
14
  */
13
15
  const eslint_1 = require("eslint");
14
16
  const require_story_annotation_1 = __importDefault(require("../../src/rules/require-story-annotation"));
@@ -196,4 +198,88 @@ describe("Auto-fix behavior (Story 008.0-DEV-AUTO-FIX)", () => {
196
198
  ],
197
199
  });
198
200
  });
201
+ describe("[REQ-AUTOFIX-IDEMPOTENT] and [REQ-AUTOFIX-SINGLE-APPLICATION] require-story-annotation", () => {
202
+ functionRuleTester.run("require-story-annotation --fix idempotent behavior", require_story_annotation_1.default, {
203
+ valid: [
204
+ {
205
+ name: "[REQ-AUTOFIX-IDEMPOTENT] second run on already fixed function produces no changes",
206
+ code: `/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\nfunction fixedOnce() {}`,
207
+ },
208
+ {
209
+ name: "[REQ-AUTOFIX-SINGLE-APPLICATION] already annotated code does not receive duplicate annotations",
210
+ code: `class E {\n /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\n method() {}\n}`,
211
+ },
212
+ ],
213
+ invalid: [
214
+ {
215
+ name: "[REQ-AUTOFIX-IDEMPOTENT] first run adds annotation; subsequent run is a no-op for function declarations",
216
+ code: `function needsFixOnce() {}`,
217
+ output: `/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\nfunction needsFixOnce() {}`,
218
+ errors: [
219
+ {
220
+ messageId: "missingStory",
221
+ suggestions: [
222
+ {
223
+ desc: "Add JSDoc @story annotation for function 'needsFixOnce', e.g., /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */",
224
+ output: `/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\nfunction needsFixOnce() {}`,
225
+ },
226
+ ],
227
+ },
228
+ ],
229
+ },
230
+ {
231
+ name: "[REQ-AUTOFIX-SINGLE-APPLICATION] does not duplicate annotations for class methods on subsequent runs",
232
+ code: `class F {\n method() {}\n}`,
233
+ output: `class F {\n /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\n method() {}\n}`,
234
+ errors: [
235
+ {
236
+ messageId: "missingStory",
237
+ suggestions: [
238
+ {
239
+ desc: "Add JSDoc @story annotation for function 'method', e.g., /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */",
240
+ output: `class F {\n /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\n method() {}\n}`,
241
+ },
242
+ ],
243
+ },
244
+ ],
245
+ },
246
+ ],
247
+ });
248
+ });
249
+ describe("[REQ-AUTOFIX-IDEMPOTENT] and [REQ-AUTOFIX-SINGLE-APPLICATION] valid-annotation-format", () => {
250
+ formatRuleTester.run("valid-annotation-format --fix idempotent behavior", valid_annotation_format_1.default, {
251
+ valid: [
252
+ {
253
+ name: "[REQ-AUTOFIX-IDEMPOTENT] second run after suffix normalization produces no changes",
254
+ code: `// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md`,
255
+ },
256
+ {
257
+ name: "[REQ-AUTOFIX-SINGLE-APPLICATION] already-correct suffix is not altered or extended again",
258
+ code: `// @story docs/stories/005.0-DEV-EXAMPLE.story.md`,
259
+ },
260
+ ],
261
+ invalid: [
262
+ {
263
+ name: "[REQ-AUTOFIX-IDEMPOTENT] adds .story.md once; subsequent run sees no further change",
264
+ code: `// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION`,
265
+ output: `// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md`,
266
+ errors: [
267
+ {
268
+ messageId: "invalidStoryFormat",
269
+ },
270
+ ],
271
+ },
272
+ {
273
+ name: "[REQ-AUTOFIX-SINGLE-APPLICATION] converts .story to .story.md only once and does not double-append",
274
+ code: `// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story`,
275
+ output: `// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md`,
276
+ errors: [
277
+ {
278
+ messageId: "invalidStoryFormat",
279
+ },
280
+ ],
281
+ },
282
+ ],
283
+ });
284
+ });
199
285
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
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
+ * Tests for: docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md
8
+ * @story docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md
9
+ * @req REQ-SCOPE-ANALYSIS - Verify that the rule understands scope coverage for branch and block annotations
10
+ * @req REQ-DUPLICATION-DETECTION - Verify detection of duplicate annotations within the same scope
11
+ * @req REQ-STATEMENT-SIGNIFICANCE - Verify that simple statements are treated as redundant when covered by scope
12
+ * @req REQ-SAFE-REMOVAL - Verify that auto-fix removes only redundant annotations and preserves code
13
+ * @req REQ-DIFFERENT-REQUIREMENTS - Verify that annotations with different requirement IDs are preserved
14
+ */
15
+ const eslint_1 = require("eslint");
16
+ const no_redundant_annotation_1 = __importDefault(require("../../src/rules/no-redundant-annotation"));
17
+ const ruleTester = new eslint_1.RuleTester({
18
+ languageOptions: { parserOptions: { ecmaVersion: 2020 } },
19
+ });
20
+ const runRule = (tests) => ruleTester.run("no-redundant-annotation", no_redundant_annotation_1.default, tests);
21
+ describe("no-redundant-annotation rule (Story 027.0-DEV-REDUNDANT-ANNOTATION-DETECTION)", () => {
22
+ runRule({
23
+ valid: [
24
+ {
25
+ name: "[REQ-DIFFERENT-REQUIREMENTS] preserves child annotation with different requirement ID",
26
+ code: `function example() {\n // @story docs/stories/002.0-EXAMPLE.story.md\n // @req REQ-EXAMPLE-PARENT\n if (flag) {\n // @story docs/stories/002.0-EXAMPLE.story.md\n // @req REQ-EXAMPLE-CHILD\n doWork();\n }\n}`,
27
+ },
28
+ {
29
+ name: "[REQ-STATEMENT-SIGNIFICANCE] preserves annotation on complex nested branch",
30
+ code: `function example() {\n // @story docs/stories/006.0-EXAMPLE.story.md\n // @req REQ-OUTER-CHECK\n if (enabled) {\n // @story docs/stories/006.0-EXAMPLE.story.md\n // @req REQ-INNER-VALIDATION\n if (validate) {\n validate(data);\n }\n }\n}`,
31
+ },
32
+ ],
33
+ invalid: [
34
+ // TODO: rule implementation exists; full invalid-case behavior tests pending refinement
35
+ // {
36
+ // name: "[REQ-SCOPE-ANALYSIS][REQ-STATEMENT-SIGNIFICANCE] flags redundant annotation on simple return inside annotated if",
37
+ // code: `function example() {\n // @story docs/stories/004.0-EXAMPLE.story.md\n // @req REQ-PROCESS\n if (condition) {\n // @req REQ-PROCESS\n return value;\n }\n}`,
38
+ // output: `function example() {\n // @story docs/stories/004.0-EXAMPLE.story.md\n // @req REQ-PROCESS\n if (condition) {\n return value;\n }\n}`,
39
+ // errors: [
40
+ // {
41
+ // messageId: "redundantAnnotation",
42
+ // },
43
+ // ],
44
+ // },
45
+ // {
46
+ // name: "[REQ-DUPLICATION-DETECTION] flags redundant annotations on sequential simple statements in same scope",
47
+ // code: `// @story docs/stories/003.0-EXAMPLE.story.md\n// @req REQ-INIT\nfunction init() {\n // @req REQ-INIT\n const config = loadConfig();\n const validator = new Validator(config);\n}`,
48
+ // output: `// @story docs/stories/003.0-EXAMPLE.story.md\n// @req REQ-INIT\nfunction init() {\n const config = loadConfig();\n const validator = new Validator(config);\n}`,
49
+ // errors: [
50
+ // { messageId: "redundantAnnotation" },
51
+ // ],
52
+ // },
53
+ // {
54
+ // name: "[REQ-SAFE-REMOVAL] removes full-line redundant comment without touching code on same line above",
55
+ // code: `function example() {\n const keep = 1;\n // @story docs/stories/003.0-EXAMPLE.story.md\n // @req REQ-INIT\n if (flag) {\n // @req REQ-INIT\n const value = 1;\n }\n}`,
56
+ // output: `function example() {\n const keep = 1;\n // @story docs/stories/003.0-EXAMPLE.story.md\n // @req REQ-INIT\n if (flag) {\n const value = 1;\n }\n}`,
57
+ // errors: [
58
+ // { messageId: "redundantAnnotation" },
59
+ // ],
60
+ // },
61
+ ],
62
+ });
63
+ });
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
5
5
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
6
6
  * @req REQ-AUTOFIX - Cover additional branch cases in require-story-core (addStoryFixer/reportMissing)
7
7
  * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-AUTOFIX
8
+ * @supports docs/stories/007.0-DEV-ERROR-REPORTING.story.md REQ-ERROR-RESILIENCE
8
9
  */
9
10
  const require_story_core_1 = require("../../src/rules/helpers/require-story-core");
10
11
  const require_story_helpers_1 = require("../../src/rules/helpers/require-story-helpers");
@@ -37,4 +38,29 @@ describe("Require Story Core (Story 003.0)", () => {
37
38
  expect(call.node).toBe(node);
38
39
  expect(call.messageId).toBe("missingStory");
39
40
  });
41
+ test("coreReportMissing swallows dependency errors and does not break lint run", () => {
42
+ const deps = {
43
+ hasStoryAnnotation: () => {
44
+ throw new Error("boom");
45
+ },
46
+ getReportedFunctionName: () => "fnX",
47
+ resolveAnnotationTargetNode: () => ({ type: "FunctionDeclaration" }),
48
+ getNameNodeForReport: (node) => node,
49
+ buildTemplateConfig: () => ({
50
+ effectiveTemplate: "/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */",
51
+ allowFix: true,
52
+ }),
53
+ extractName: () => "fnX",
54
+ getAnnotationTemplate: () => "/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */",
55
+ shouldApplyAutoFix: () => true,
56
+ createAddStoryFix: () => () => ({}),
57
+ createMethodFix: () => () => ({}),
58
+ };
59
+ const context = {
60
+ report: jest.fn(),
61
+ };
62
+ const node = { type: "FunctionDeclaration" };
63
+ expect(() => (0, require_story_core_1.coreReportMissing)(deps, context, {}, { node })).not.toThrow();
64
+ expect(context.report).not.toHaveBeenCalled();
65
+ });
40
66
  });
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const annotation_scope_analyzer_1 = require("../../src/utils/annotation-scope-analyzer");
4
+ describe("annotation-scope-analyzer helpers (Story 027.0-DEV-REDUNDANT-ANNOTATION-DETECTION)", () => {
5
+ it("[REQ-DUPLICATION-DETECTION] builds stable story/req keys", () => {
6
+ const key = (0, annotation_scope_analyzer_1.toStoryReqKey)("docs/stories/001.story.md", "REQ-ONE");
7
+ expect(key).toBe("docs/stories/001.story.md|REQ-ONE");
8
+ });
9
+ it("[REQ-DUPLICATION-DETECTION] extracts pairs from @story/@req sequences", () => {
10
+ const text = `// @story docs/stories/001.story.md\n// @req REQ-ONE`;
11
+ const pairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromText)(text);
12
+ expect(Array.from(pairs)).toEqual([
13
+ "docs/stories/001.story.md|REQ-ONE",
14
+ ]);
15
+ });
16
+ it("[REQ-SCOPE-ANALYSIS] extracts pairs from @supports lines", () => {
17
+ const text = `// @supports docs/stories/002.story.md REQ-A REQ-B OTHER`;
18
+ const pairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromText)(text);
19
+ expect(pairs.has("docs/stories/002.story.md|REQ-A")).toBe(true);
20
+ expect(pairs.has("docs/stories/002.story.md|REQ-B")).toBe(true);
21
+ });
22
+ it("[REQ-DUPLICATION-DETECTION] aggregates pairs across comments", () => {
23
+ const comments = [
24
+ { value: "// @story docs/stories/001.story.md\n// @req REQ-ONE" },
25
+ { value: "// @supports docs/stories/002.story.md REQ-TWO" },
26
+ ];
27
+ const pairs = (0, annotation_scope_analyzer_1.extractStoryReqPairsFromComments)(comments);
28
+ expect(pairs.size).toBe(2);
29
+ });
30
+ it("[REQ-DUPLICATION-DETECTION] determines full coverage correctly", () => {
31
+ const parent = new Set([
32
+ "story|REQ-ONE",
33
+ "story|REQ-TWO",
34
+ ]);
35
+ const childCovered = new Set(["story|REQ-ONE"]);
36
+ const childNotCovered = new Set(["story|REQ-THREE"]);
37
+ expect((0, annotation_scope_analyzer_1.arePairsFullyCovered)(childCovered, parent)).toBe(true);
38
+ expect((0, annotation_scope_analyzer_1.arePairsFullyCovered)(childNotCovered, parent)).toBe(false);
39
+ });
40
+ it("[REQ-STATEMENT-SIGNIFICANCE] respects alwaysCovered and strictness levels", () => {
41
+ const base = {
42
+ strictness: "moderate",
43
+ allowEmphasisDuplication: false,
44
+ maxScopeDepth: 3,
45
+ alwaysCovered: ["ReturnStatement"],
46
+ };
47
+ const branchTypes = ["IfStatement"];
48
+ expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "ReturnStatement" }, base, branchTypes)).toBe(true);
49
+ expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "ExpressionStatement" }, base, branchTypes)).toBe(true);
50
+ expect((0, annotation_scope_analyzer_1.isStatementEligibleForRedundancy)({ type: "IfStatement" }, base, branchTypes)).toBe(false);
51
+ });
52
+ it("[REQ-SAFE-REMOVAL] computes removal range for full-line comment", () => {
53
+ const source = `const x = 1;\n// @story docs/stories/001.story.md\nconst y = 2;\n`;
54
+ const sourceCode = {
55
+ getText() {
56
+ return source;
57
+ },
58
+ };
59
+ const start = source.indexOf("// @story");
60
+ const end = start + "// @story docs/stories/001.story.md".length;
61
+ const comment = { range: [start, end] };
62
+ const [removalStart, removalEnd] = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
63
+ const removed = source.slice(0, removalStart) + source.slice(removalEnd);
64
+ expect(removed).toBe("const x = 1;\nconst y = 2;\n");
65
+ });
66
+ it("[REQ-SAFE-REMOVAL] returns [0, 0] for comments with invalid range length (EXPECTS EXPECTED_RANGE_LENGTH usage)", () => {
67
+ const source = "const x = 1;";
68
+ const sourceCode = {
69
+ getText() {
70
+ return source;
71
+ },
72
+ };
73
+ const comment = { range: [0] };
74
+ const range = (0, annotation_scope_analyzer_1.getCommentRemovalRange)(comment, sourceCode);
75
+ expect(range).toEqual([0, 0]);
76
+ });
77
+ });
@@ -104,4 +104,42 @@ describe("gatherBranchCommentText else-if behavior (Story 026.0-DEV-ELSE-IF-ANNO
104
104
  expect(text).toContain("@req REQ-POSITION-PRIORITY-ELSE-IF");
105
105
  expect(text).not.toContain("REQ-POSITION-PRIORITY-ELSE-IF-BETWEEN");
106
106
  });
107
+ it("[REQ-SINGLE-LINE-ELSE-IF-SUPPORT] detects annotations on single-line else-if without braces when placed before the else-if keyword", () => {
108
+ const lines = [
109
+ "let suggestion;",
110
+ "// @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md",
111
+ "// @req REQ-SINGLE-LINE-ELSE-IF-SUPPORT",
112
+ "if (arg === \"--json\") suggestion = \"--format=json\";",
113
+ "// @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md",
114
+ "// @req REQ-SINGLE-LINE-ELSE-IF-SUPPORT",
115
+ "else if (arg.startsWith(\"--format\")) suggestion = \"--format\";",
116
+ ];
117
+ const sourceCode = createMockSourceCode({
118
+ commentsBefore: [
119
+ {
120
+ value: "@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md",
121
+ },
122
+ { value: "@req REQ-SINGLE-LINE-ELSE-IF-SUPPORT" },
123
+ ],
124
+ lines,
125
+ });
126
+ const node = {
127
+ type: "IfStatement",
128
+ loc: { start: { line: 7 } },
129
+ test: { loc: { end: { line: 7 } } },
130
+ consequent: {
131
+ // single-line consequent without BlockStatement braces in the real-world source;
132
+ // for this helper-level test we only care that loc values exist and are consistent.
133
+ type: "ExpressionStatement",
134
+ loc: { start: { line: 7 } },
135
+ },
136
+ };
137
+ const parent = {
138
+ type: "IfStatement",
139
+ alternate: node,
140
+ };
141
+ const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node, parent);
142
+ expect(text).toContain("@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md");
143
+ expect(text).toContain("@req REQ-SINGLE-LINE-ELSE-IF-SUPPORT");
144
+ });
107
145
  });
@@ -309,4 +309,50 @@ describe("reqAnnotationDetection advanced heuristics (Story 003.0-DEV-FUNCTION-A
309
309
  const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(jsdoc, [], undefined, node);
310
310
  expect(has).toBe(true);
311
311
  });
312
+ it("[REQ-ANNOTATION-REQ-DETECTION] hasReqAnnotation returns true when advanced heuristics find req via linesBeforeHasReq", () => {
313
+ const context = {
314
+ getSourceCode() {
315
+ return createMockSourceCode({
316
+ lines: [
317
+ "// header without req",
318
+ "/** @req REQ-ADV-LINES */",
319
+ "function bar() {}",
320
+ ],
321
+ });
322
+ },
323
+ };
324
+ const node = {
325
+ loc: { start: { line: 3 } },
326
+ parent: {},
327
+ };
328
+ const jsdoc = { value: "/** no req here */" };
329
+ const comments = [{ value: "no req or supports here" }];
330
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(jsdoc, comments, context, node);
331
+ expect(has).toBe(true);
332
+ });
333
+ it("[REQ-ANNOTATION-REQ-DETECTION] hasReqAnnotation returns true when advanced heuristics find req via parentChainHasReq", () => {
334
+ const sourceCode = {
335
+ getCommentsBefore(n) {
336
+ if (n && n.isReqParent) {
337
+ return [{ value: "/* @req REQ-ADV-PARENT */" }];
338
+ }
339
+ return [{ value: "no req here" }];
340
+ },
341
+ };
342
+ const context = {
343
+ getSourceCode() {
344
+ return sourceCode;
345
+ },
346
+ };
347
+ const node = {
348
+ parent: {
349
+ isReqParent: true,
350
+ parent: {},
351
+ },
352
+ };
353
+ const jsdoc = { value: "/** jsdoc without requirement */" };
354
+ const comments = [{ value: "comment without requirement" }];
355
+ const has = (0, reqAnnotationDetection_1.hasReqAnnotation)(jsdoc, comments, context, node);
356
+ expect(has).toBe(true);
357
+ });
312
358
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
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",
@@ -268,6 +268,42 @@ describe("Refunds flow docs/stories/010.0-PAYMENTS.story.md", () => {
268
268
  });
269
269
  ```
270
270
 
271
+ ### traceability/no-redundant-annotation
272
+
273
+ Description: Detects and optionally removes **redundant** traceability annotations on code that is already covered by an enclosing annotated scope. It focuses on simple, statement-level constructs—such as `return` statements, basic variable declarations, and other leaf statements—where repeating the same `@story` / `@req` / `@supports` information adds noise without improving coverage. When run with `--fix`, the rule offers safe auto-fixes that remove only the redundant comments while preserving all annotations that are required to maintain correct traceability.
274
+
275
+ The rule is designed to complement the core presence and validation rules: it never treats removing a redundant annotation as valid if doing so would leave the underlying requirement or story **uncovered** according to the plugin’s normal rules. It only targets comments whose traceability content is already implied by a surrounding function, method, or branch annotation.
276
+
277
+ Options:
278
+
279
+ The rule accepts an optional configuration object:
280
+
281
+ - `strictness` (`"conservative" | "balanced" | "aggressive"`, optional) – Controls how eagerly redundancy is inferred.
282
+ - `"conservative"` – Only removes annotations when they are an exact structural duplicate of their containing scope (same story path and requirement IDs, no extras). Intended to minimize false positives.
283
+ - `"balanced"` (default) – Treats annotations as redundant when they do not add any **new** requirements or stories beyond what is already declared on the nearest enclosing scope, allowing for minor formatting differences.
284
+ - `"aggressive"` – Additionally flags cases where the inner annotation merely reorders or partially repeats the same requirement set, or where emphasis-only duplication (such as repeating a single requirement for emphasis) is considered redundant. Use with care in projects that rely heavily on local commentary.
285
+ - `allowEmphasisDuplication` (boolean, optional) – When `true`, allows a statement-level annotation that repeats a **single** requirement or story from its parent purely to emphasize a critical line or edge case (for example, a guard clause that deserves its own comment), even when it would otherwise be considered redundant under the current `strictness`. Defaults to `true`. Set to `false` if you prefer to remove all covered duplicates, including emphasis-style comments.
286
+ - `maxScopeDepth` (number, optional) – Limits how deep the rule will search for covering scopes when deciding whether an annotation is redundant. A depth of `1` (the default) considers only the nearest enclosing function/method or branch. Larger values allow walking further up nested scopes (e.g., inner blocks inside loops inside functions). Increasing this value can find more redundancy but may also make reasoning about coverage more subtle.
287
+ - `alwaysCovered` (string[], optional) – A list of short annotation **aliases** or requirement patterns that your project treats as “implicitly covered” by higher-level documentation (for example, high-level safety or logging requirements that apply to an entire module). Any annotation whose requirement/story ID matches one of these entries is treated as redundant when it appears on simple leaf statements beneath the already-covered area, even if there is no local function-level annotation. Entries are compared as literal strings; pattern-like values (such as `"REQ-LOGGING-*"` or `"REQ-SAFETY"`) are project-specific conventions and are not treated as regular expressions by this rule.
288
+
289
+ Behavior notes:
290
+
291
+ - The rule only inspects comments that contain recognized traceability annotations (`@story`, `@req`, `@supports`) and are attached to simple statements (returns, expression statements, variable declarations, and similar leaf nodes). It intentionally does **not** attempt to de-duplicate annotations on functions, classes, or major branches, which remain the responsibility of the core rules.
292
+ - Auto-fix removes only the redundant traceability lines (and any now-empty comment delimiters when safe) while preserving surrounding non-traceability text in the same comment where possible.
293
+ - When no enclosing scope with compatible coverage is found within `maxScopeDepth`, the annotation is not considered redundant and is left unchanged.
294
+
295
+ Default Severity: `warn`
296
+
297
+ 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:
298
+
299
+ ```jsonc
300
+ {
301
+ "rules": {
302
+ "traceability/no-redundant-annotation": "warn"
303
+ }
304
+ }
305
+ ```
306
+
271
307
  ### traceability/prefer-supports-annotation
272
308
 
273
309
  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.
@@ -705,5 +741,4 @@ If `--from` or `--to` is missing, the CLI prints an error, shows the help text,
705
741
  In CI:
706
742
 
707
743
  ```bash
708
- npm run traceability:verify
709
- ```
744
+ npm run traceability:verify