eslint-plugin-traceability 1.10.1 → 1.11.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +4 -3
  2. package/README.md +1 -0
  3. package/lib/src/maintenance/cli.js +12 -12
  4. package/lib/src/maintenance/detect.js +19 -19
  5. package/lib/src/rules/helpers/require-story-core.d.ts +2 -15
  6. package/lib/src/rules/helpers/require-story-core.js +4 -71
  7. package/lib/src/rules/helpers/require-story-helpers.d.ts +32 -8
  8. package/lib/src/rules/helpers/require-story-helpers.js +44 -15
  9. package/lib/src/rules/helpers/require-story-visitors.js +47 -6
  10. package/lib/src/rules/helpers/valid-annotation-format-validators.js +5 -1
  11. package/lib/src/rules/helpers/valid-annotation-options.d.ts +6 -0
  12. package/lib/src/rules/helpers/valid-annotation-options.js +4 -0
  13. package/lib/src/rules/helpers/valid-annotation-utils.js +31 -31
  14. package/lib/src/rules/helpers/valid-story-reference-helpers.js +19 -19
  15. package/lib/src/rules/prefer-implements-annotation.js +29 -1
  16. package/lib/src/rules/require-story-annotation.js +15 -0
  17. package/lib/src/rules/require-test-traceability.js +1 -6
  18. package/lib/src/utils/annotation-checker.js +1 -1
  19. package/lib/tests/perf/maintenance-cli-large-workspace.test.d.ts +1 -0
  20. package/lib/tests/perf/maintenance-cli-large-workspace.test.js +130 -0
  21. package/lib/tests/perf/maintenance-large-workspace.test.d.ts +1 -0
  22. package/lib/tests/perf/maintenance-large-workspace.test.js +149 -0
  23. package/lib/tests/rules/auto-fix-behavior-008.test.js +23 -0
  24. package/lib/tests/rules/require-story-core.autofix.test.js +9 -3
  25. package/lib/tests/rules/require-story-core.test.js +13 -7
  26. package/lib/tests/rules/require-story-helpers-edgecases.test.js +1 -1
  27. package/lib/tests/rules/require-story-helpers.test.js +14 -8
  28. package/lib/tests/utils/require-story-core-test-helpers.d.ts +1 -1
  29. package/lib/tests/utils/require-story-core-test-helpers.js +16 -16
  30. package/lib/tests/utils/temp-dir-helpers.js +1 -1
  31. package/package.json +9 -2
  32. package/user-docs/api-reference.md +8 -4
  33. package/user-docs/examples.md +42 -0
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ /**
37
+ * Performance and stress tests for maintenance tools on large workspaces.
38
+ * @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-DETECT REQ-MAINT-VERIFY REQ-MAINT-REPORT REQ-MAINT-UPDATE REQ-MAINT-BATCH
39
+ */
40
+ const fs = __importStar(require("fs"));
41
+ const os = __importStar(require("os"));
42
+ const path = __importStar(require("path"));
43
+ const perf_hooks_1 = require("perf_hooks");
44
+ const detect_1 = require("../../src/maintenance/detect");
45
+ const batch_1 = require("../../src/maintenance/batch");
46
+ const report_1 = require("../../src/maintenance/report");
47
+ const update_1 = require("../../src/maintenance/update");
48
+ /**
49
+ * Shape of the synthetic large workspace:
50
+ * - 10 modules (module-000 .. module-009)
51
+ * - 50 files per module (file-000.ts .. file-049.ts)
52
+ * - Each file includes a mix of valid and stale @story references.
53
+ */
54
+ function createLargeWorkspace() {
55
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "traceability-large-"));
56
+ // Create a pool of story files that will be considered "valid".
57
+ const validStories = [];
58
+ for (let i = 0; i < 250; i += 1) {
59
+ const storyName = `valid-story-${i.toString().padStart(4, "0")}.story.md`;
60
+ const storyPath = path.join(root, storyName);
61
+ fs.writeFileSync(storyPath, `# ${storyName}`, "utf8");
62
+ validStories.push(storyName);
63
+ }
64
+ let validIndex = 0;
65
+ let staleIndex = 0;
66
+ for (let moduleIndex = 0; moduleIndex < 10; moduleIndex += 1) {
67
+ const moduleDir = path.join(root, `module-${moduleIndex.toString().padStart(3, "0")}`);
68
+ fs.mkdirSync(moduleDir);
69
+ for (let fileIndex = 0; fileIndex < 50; fileIndex += 1) {
70
+ const filePath = path.join(moduleDir, `file-${fileIndex.toString().padStart(3, "0")}.ts`);
71
+ const validStory = validStories[validIndex % validStories.length] ??
72
+ "valid-story-0000.story.md";
73
+ validIndex += 1;
74
+ const staleStory = `stale-story-${staleIndex
75
+ .toString()
76
+ .padStart(4, "0")}.story.md`;
77
+ staleIndex += 1;
78
+ const content = `/**
79
+ * @story ${validStory}
80
+ * @story ${staleStory}
81
+ */
82
+ export function example_${moduleIndex}_${fileIndex}() {}
83
+ `;
84
+ fs.writeFileSync(filePath, content, "utf8");
85
+ }
86
+ }
87
+ return {
88
+ root,
89
+ cleanup: () => {
90
+ fs.rmSync(root, { recursive: true, force: true });
91
+ },
92
+ };
93
+ }
94
+ describe("Maintenance tools on large workspaces (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
95
+ let workspace;
96
+ beforeAll(() => {
97
+ workspace = createLargeWorkspace();
98
+ });
99
+ afterAll(() => {
100
+ workspace.cleanup();
101
+ });
102
+ it("[REQ-MAINT-DETECT] detectStaleAnnotations completes within a generous time budget", () => {
103
+ const start = perf_hooks_1.performance.now();
104
+ const stale = (0, detect_1.detectStaleAnnotations)(workspace.root);
105
+ const durationMs = perf_hooks_1.performance.now() - start;
106
+ // Sanity check: we expect at least some stale entries due to the generated stale-story-* references.
107
+ expect(stale.length).toBeGreaterThan(0);
108
+ // Guardrail: this operation should remain comfortably under ~5 seconds on CI hardware.
109
+ expect(durationMs).toBeLessThan(5000);
110
+ });
111
+ it("[REQ-MAINT-VERIFY] verifyAnnotations remains fast on large workspaces", () => {
112
+ const start = perf_hooks_1.performance.now();
113
+ const result = (0, batch_1.verifyAnnotations)(workspace.root);
114
+ const durationMs = perf_hooks_1.performance.now() - start;
115
+ // With both valid and stale references, verification should report false.
116
+ expect(result).toBe(false);
117
+ expect(durationMs).toBeLessThan(5000);
118
+ });
119
+ it("[REQ-MAINT-REPORT] generateMaintenanceReport produces output within a generous time budget", () => {
120
+ const start = perf_hooks_1.performance.now();
121
+ const report = (0, report_1.generateMaintenanceReport)(workspace.root);
122
+ const durationMs = perf_hooks_1.performance.now() - start;
123
+ expect(report).not.toBe("");
124
+ expect(durationMs).toBeLessThan(5000);
125
+ });
126
+ it("[REQ-MAINT-UPDATE] updateAnnotationReferences and batchUpdateAnnotations remain tractable", () => {
127
+ const exampleOldPath = "stale-story-0000.story.md";
128
+ const exampleNewPath = "updated-story-0000.story.md";
129
+ const singleStart = perf_hooks_1.performance.now();
130
+ const updatedCount = (0, update_1.updateAnnotationReferences)(workspace.root, exampleOldPath, exampleNewPath);
131
+ const singleDuration = perf_hooks_1.performance.now() - singleStart;
132
+ expect(updatedCount).toBeGreaterThan(0);
133
+ expect(singleDuration).toBeLessThan(5000);
134
+ const batchStart = perf_hooks_1.performance.now();
135
+ const totalUpdated = (0, batch_1.batchUpdateAnnotations)(workspace.root, [
136
+ {
137
+ oldPath: "stale-story-0001.story.md",
138
+ newPath: "updated-story-0001.story.md",
139
+ },
140
+ {
141
+ oldPath: "stale-story-0002.story.md",
142
+ newPath: "updated-story-0002.story.md",
143
+ },
144
+ ]);
145
+ const batchDuration = perf_hooks_1.performance.now() - batchStart;
146
+ expect(totalUpdated).toBeGreaterThanOrEqual(2);
147
+ expect(batchDuration).toBeLessThan(5000);
148
+ });
149
+ });
@@ -122,6 +122,29 @@ describe("Auto-fix behavior (Story 008.0-DEV-AUTO-FIX)", () => {
122
122
  },
123
123
  ],
124
124
  },
125
+ {
126
+ name: "[REQ-AUTOFIX-TEMPLATE] uses configured templates for functions and methods",
127
+ code: `function fn() {}\nclass C { method() {} }`,
128
+ output: `/** @story CUSTOM-FN */\nfunction fn() {}\nclass C { /** @story CUSTOM-METHOD */\n method() {} }`,
129
+ options: [
130
+ {
131
+ annotationTemplate: "/** @story CUSTOM-FN */",
132
+ methodAnnotationTemplate: "/** @story CUSTOM-METHOD */",
133
+ },
134
+ ],
135
+ errors: 2,
136
+ },
137
+ {
138
+ name: "[REQ-AUTOFIX-SELECTIVE] does not insert annotations when autoFix is false",
139
+ code: `function fnNoFix() {}`,
140
+ output: null,
141
+ options: [
142
+ {
143
+ autoFix: false,
144
+ },
145
+ ],
146
+ errors: 1,
147
+ },
125
148
  ],
126
149
  });
127
150
  });
@@ -10,8 +10,10 @@ const require_story_helpers_1 = require("../../src/rules/helpers/require-story-h
10
10
  const require_story_core_test_helpers_1 = require("../utils/require-story-core-test-helpers");
11
11
  describe("Require Story Core (Story 003.0)", () => {
12
12
  test("createAddStoryFix covers primary branch combinations via shared helper", () => {
13
- (0, require_story_core_test_helpers_1.exerciseCreateAddStoryFixBranches)(require_story_core_1.createAddStoryFix, {
14
- annotationText: require_story_helpers_1.ANNOTATION + "\n",
13
+ const defaultTemplate = (0, require_story_helpers_1.getAnnotationTemplate)();
14
+ const factory = (target, _annotationTemplate) => (0, require_story_core_1.createAddStoryFix)(target, defaultTemplate);
15
+ (0, require_story_core_test_helpers_1.exerciseCreateAddStoryFixBranches)(factory, {
16
+ annotationText: defaultTemplate,
15
17
  });
16
18
  });
17
19
  test("reportMissing uses context.getSourceCode fallback when sourceCode not provided and still reports", () => {
@@ -24,7 +26,11 @@ describe("Require Story Core (Story 003.0)", () => {
24
26
  /* intentionally missing getJSDocComment to exercise branch */ getText: () => "",
25
27
  };
26
28
  const context = { getSourceCode: () => fakeSource, report: jest.fn() };
27
- (0, require_story_core_1.reportMissing)(context, undefined, node, node);
29
+ (0, require_story_helpers_1.reportMissing)(context, undefined, {
30
+ node,
31
+ target: node,
32
+ options: { autoFixToggle: true },
33
+ });
28
34
  expect(context.report).toHaveBeenCalledTimes(1);
29
35
  const call = context.report.mock.calls[0][0];
30
36
  expect(call.node).toBe(node);
@@ -17,13 +17,15 @@ describe("Require Story Core (Story 003.0)", () => {
17
17
  const fixer = {
18
18
  insertTextBeforeRange: jest.fn((r, t) => ({ r, t })),
19
19
  };
20
- const fixFn = (0, require_story_core_1.createMethodFix)(node);
20
+ const defaultTemplate = (0, require_story_helpers_1.getAnnotationTemplate)();
21
+ const fixFn = (0, require_story_core_1.createMethodFix)(node, defaultTemplate);
21
22
  const result = fixFn(fixer);
22
23
  expect(fixer.insertTextBeforeRange).toHaveBeenCalledTimes(1);
23
24
  const calledArgs = fixer.insertTextBeforeRange.mock.calls[0];
24
25
  expect(calledArgs[0]).toEqual([12, 12]);
25
- expect(calledArgs[1]).toBe(`${require_story_helpers_1.ANNOTATION}\n `);
26
- expect(result).toEqual({ r: [12, 12], t: `${require_story_helpers_1.ANNOTATION}\n ` });
26
+ expect(typeof calledArgs[1]).toBe("string");
27
+ expect(calledArgs[1].length).toBeGreaterThan(0);
28
+ expect(result).toEqual({ r: [12, 12], t: calledArgs[1] });
27
29
  });
28
30
  test("reportMethod calls context.report with proper data and suggest.fix works", () => {
29
31
  const node = {
@@ -37,11 +39,14 @@ describe("Require Story Core (Story 003.0)", () => {
37
39
  getSourceCode: () => fakeSource,
38
40
  report: jest.fn(),
39
41
  };
40
- (0, require_story_core_1.reportMethod)(context, fakeSource, node, node);
42
+ (0, require_story_helpers_1.reportMethod)(context, fakeSource, { node, target: node });
41
43
  expect(context.report).toHaveBeenCalledTimes(1);
42
44
  const call = context.report.mock.calls[0][0];
43
45
  expect(call.messageId).toBe("missingStory");
44
- expect(call.data).toEqual({ name: "myMethod" });
46
+ expect(call.data).toHaveProperty("name");
47
+ expect(call.data).toHaveProperty("functionName");
48
+ expect(typeof call.data.name).toBe("string");
49
+ expect(typeof call.data.functionName).toBe("string");
45
50
  // The suggest fix should be a function; exercise it with a mock fixer
46
51
  expect(Array.isArray(call.suggest)).toBe(true);
47
52
  expect(typeof call.suggest[0].fix).toBe("function");
@@ -52,7 +57,8 @@ describe("Require Story Core (Story 003.0)", () => {
52
57
  expect(fixer.insertTextBeforeRange).toHaveBeenCalled();
53
58
  const args = fixer.insertTextBeforeRange.mock.calls[0];
54
59
  expect(args[0]).toEqual([40, 40]);
55
- expect(args[1]).toBe(`${require_story_helpers_1.ANNOTATION}\n `);
56
- expect(fixResult).toEqual({ r: [40, 40], t: `${require_story_helpers_1.ANNOTATION}\n ` });
60
+ expect(typeof args[1]).toBe("string");
61
+ expect(args[1].length).toBeGreaterThan(0);
62
+ expect(fixResult).toEqual({ r: [40, 40], t: args[1] });
57
63
  });
58
64
  });
@@ -73,7 +73,7 @@ describe("Require Story Helpers - edge cases (Story 003.0)", () => {
73
73
  },
74
74
  };
75
75
  const context = { getSourceCode: () => fakeSource, report: jest.fn() };
76
- (0, require_story_helpers_1.reportMissing)(context, fakeSource, node, node);
76
+ (0, require_story_helpers_1.reportMissing)(context, fakeSource, { node, target: node });
77
77
  expect(context.report).not.toHaveBeenCalled();
78
78
  });
79
79
  });
@@ -17,13 +17,15 @@ describe("Require Story Helpers (Story 003.0)", () => {
17
17
  const fixer = {
18
18
  insertTextBeforeRange: jest.fn((r, t) => ({ r, t })),
19
19
  };
20
- const fixFn = (0, require_story_core_1.createAddStoryFix)(target);
20
+ const defaultTemplate = (0, require_story_helpers_1.getAnnotationTemplate)();
21
+ const fixFn = (0, require_story_core_1.createAddStoryFix)(target, defaultTemplate);
21
22
  const result = fixFn(fixer);
22
23
  expect(fixer.insertTextBeforeRange).toHaveBeenCalledTimes(1);
23
24
  const calledArgs = fixer.insertTextBeforeRange.mock.calls[0];
24
25
  expect(calledArgs[0]).toEqual([10, 10]);
25
- expect(calledArgs[1]).toBe(`${require_story_helpers_1.ANNOTATION}\n`);
26
- expect(result).toEqual({ r: [10, 10], t: `${require_story_helpers_1.ANNOTATION}\n` });
26
+ expect(typeof calledArgs[1]).toBe("string");
27
+ expect(calledArgs[1].length).toBeGreaterThan(0);
28
+ expect(result).toEqual({ r: [10, 10], t: calledArgs[1] });
27
29
  });
28
30
  test("createMethodFix falls back to node.range when parent not export", () => {
29
31
  const node = {
@@ -34,11 +36,15 @@ describe("Require Story Helpers (Story 003.0)", () => {
34
36
  const fixer = {
35
37
  insertTextBeforeRange: jest.fn((r, t) => ({ r, t })),
36
38
  };
37
- const fixFn = (0, require_story_core_1.createMethodFix)(node);
39
+ const defaultTemplate = (0, require_story_helpers_1.getAnnotationTemplate)();
40
+ const fixFn = (0, require_story_core_1.createMethodFix)(node, defaultTemplate);
38
41
  const res = fixFn(fixer);
39
42
  expect(fixer.insertTextBeforeRange.mock.calls[0][0]).toEqual([30, 30]);
40
- expect(fixer.insertTextBeforeRange.mock.calls[0][1]).toBe(`${require_story_helpers_1.ANNOTATION}\n `);
41
- expect(res).toEqual({ r: [30, 30], t: `${require_story_helpers_1.ANNOTATION}\n ` });
43
+ const insertedText = fixer.insertTextBeforeRange.mock
44
+ .calls[0][1];
45
+ expect(typeof insertedText).toBe("string");
46
+ expect(insertedText.length).toBeGreaterThan(0);
47
+ expect(res).toEqual({ r: [30, 30], t: insertedText });
42
48
  });
43
49
  test("reportMissing does not call context.report if JSDoc contains @story", () => {
44
50
  const node = {
@@ -56,7 +62,7 @@ describe("Require Story Helpers (Story 003.0)", () => {
56
62
  getSourceCode: () => fakeSource,
57
63
  report: jest.fn(),
58
64
  };
59
- (0, require_story_core_1.reportMissing)(context, fakeSource, node, node);
65
+ (0, require_story_helpers_1.reportMissing)(context, fakeSource, { node, target: node });
60
66
  expect(context.report).not.toHaveBeenCalled();
61
67
  });
62
68
  test("reportMissing calls context.report when no JSDoc story present", () => {
@@ -73,7 +79,7 @@ describe("Require Story Helpers (Story 003.0)", () => {
73
79
  getSourceCode: () => fakeSource,
74
80
  report: jest.fn(),
75
81
  };
76
- (0, require_story_core_1.reportMissing)(context, fakeSource, node, node);
82
+ (0, require_story_helpers_1.reportMissing)(context, fakeSource, { node, target: node });
77
83
  expect(context.report).toHaveBeenCalledTimes(1);
78
84
  const call = context.report.mock.calls[0][0];
79
85
  expect(call.node).toBe(node);
@@ -6,5 +6,5 @@
6
6
  interface ExerciseOptions {
7
7
  annotationText?: string;
8
8
  }
9
- export declare function exerciseCreateAddStoryFixBranches(createAddStoryFix: any, options?: ExerciseOptions): void;
9
+ export declare function exerciseCreateAddStoryFixBranches(createAddStoryFixFactory: (_target: any, _annotationTemplate: string) => (_fixer: any) => any, options?: ExerciseOptions): void;
10
10
  export {};
@@ -19,36 +19,36 @@ function baseFixer() {
19
19
  insertTextBeforeRange: jest.fn((r, t) => ({ r, t })),
20
20
  };
21
21
  }
22
- function exerciseBranch1(createAddStoryFix, annotation) {
22
+ function exerciseBranch1(createAddStoryFixFactory, annotation) {
23
23
  const fixer = baseFixer();
24
- const fixFn = createAddStoryFix(null);
24
+ const fixFn = createAddStoryFixFactory(null, annotation);
25
25
  const res = fixFn(fixer);
26
26
  expect(fixer.insertTextBeforeRange).toHaveBeenCalledTimes(1);
27
27
  const args = fixer.insertTextBeforeRange.mock.calls[0];
28
28
  expect(args[0]).toEqual([0, 0]);
29
- expect(args[1]).toBe(annotation);
29
+ expect(args[1]).toBe(`${annotation}\n`);
30
30
  expect(res).toEqual({
31
31
  r: [0, 0],
32
- t: annotation,
32
+ t: `${annotation}\n`,
33
33
  });
34
34
  }
35
- function exerciseBranch2(createAddStoryFix, annotation) {
35
+ function exerciseBranch2(createAddStoryFixFactory, annotation) {
36
36
  const target = {
37
37
  type: "FunctionDeclaration",
38
38
  range: [RANGE_ONE_START, RANGE_ONE_END],
39
39
  parent: { type: "ClassBody" },
40
40
  };
41
41
  const fixer = baseFixer();
42
- const fixFn = createAddStoryFix(target);
42
+ const fixFn = createAddStoryFixFactory(target, annotation);
43
43
  const res = fixFn(fixer);
44
44
  expect(fixer.insertTextBeforeRange.mock.calls[0][0]).toEqual([RANGE_ONE_START, RANGE_ONE_START]);
45
- expect(fixer.insertTextBeforeRange.mock.calls[0][1]).toBe(annotation);
45
+ expect(fixer.insertTextBeforeRange.mock.calls[0][1]).toBe(`${annotation}\n`);
46
46
  expect(res).toEqual({
47
47
  r: [RANGE_ONE_START, RANGE_ONE_START],
48
- t: annotation,
48
+ t: `${annotation}\n`,
49
49
  });
50
50
  }
51
- function exerciseBranch3(createAddStoryFix, annotation) {
51
+ function exerciseBranch3(createAddStoryFixFactory, annotation) {
52
52
  const target = {
53
53
  type: "FunctionDeclaration",
54
54
  range: [RANGE_TWO_START, RANGE_TWO_END],
@@ -58,18 +58,18 @@ function exerciseBranch3(createAddStoryFix, annotation) {
58
58
  },
59
59
  };
60
60
  const fixer = baseFixer();
61
- const fixFn = createAddStoryFix(target);
61
+ const fixFn = createAddStoryFixFactory(target, annotation);
62
62
  const res = fixFn(fixer);
63
63
  expect(fixer.insertTextBeforeRange.mock.calls[0][0]).toEqual([RANGE_PARENT_START, RANGE_PARENT_START]);
64
- expect(fixer.insertTextBeforeRange.mock.calls[0][1]).toBe(annotation);
64
+ expect(fixer.insertTextBeforeRange.mock.calls[0][1]).toBe(`${annotation}\n`);
65
65
  expect(res).toEqual({
66
66
  r: [RANGE_PARENT_START, RANGE_PARENT_START],
67
- t: annotation,
67
+ t: `${annotation}\n`,
68
68
  });
69
69
  }
70
- function exerciseCreateAddStoryFixBranches(createAddStoryFix, options = {}) {
70
+ function exerciseCreateAddStoryFixBranches(createAddStoryFixFactory, options = {}) {
71
71
  const annotation = options.annotationText ?? DEFAULT_ANNOTATION;
72
- exerciseBranch1(createAddStoryFix, annotation);
73
- exerciseBranch2(createAddStoryFix, annotation);
74
- exerciseBranch3(createAddStoryFix, annotation);
72
+ exerciseBranch1(createAddStoryFixFactory, annotation);
73
+ exerciseBranch2(createAddStoryFixFactory, annotation);
74
+ exerciseBranch3(createAddStoryFixFactory, annotation);
75
75
  }
@@ -54,7 +54,7 @@ function createTempDir(prefix) {
54
54
  return {
55
55
  dir,
56
56
  cleanup() {
57
- // @implements docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-SAFE
57
+ // @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-SAFE
58
58
  fs.rmSync(dir, { recursive: true, force: true });
59
59
  },
60
60
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.10.1",
3
+ "version": "1.11.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",
@@ -35,12 +35,19 @@
35
35
  "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
36
36
  "lint-staged": "lint-staged",
37
37
  "duplication": "jscpd src tests --reporters console --threshold 3 --ignore tests/utils/**",
38
+ "coverage:branches": "node scripts/extract-uncovered-branches.js",
38
39
  "deps:maturity": "dry-aged-deps",
39
40
  "audit:dev-high": "node scripts/generate-dev-deps-audit.js",
40
41
  "safety:deps": "node scripts/ci-safety-deps.js",
41
42
  "audit:ci": "node scripts/ci-audit.js",
43
+ "check:ci-artifacts": "node scripts/check-no-tracked-ci-artifacts.js",
42
44
  "security:secrets": "secretlint \"**/*\" --no-color",
43
- "smoke-test": "./scripts/smoke-test.sh"
45
+ "smoke-test": "./scripts/smoke-test.sh",
46
+ "debug:cli": "node scripts/cli-debug.js",
47
+ "debug:require-story": "node scripts/debug-require-story.js",
48
+ "debug:repro": "node scripts/debug-repro.js",
49
+ "report:eslint-suppressions": "node scripts/report-eslint-suppressions.js",
50
+ "check:scripts": "node scripts/validate-scripts-nonempty.js"
44
51
  },
45
52
  "lint-staged": {
46
53
  "src/**/*.{js,jsx,ts,tsx,json,md}": [
@@ -19,12 +19,15 @@ The `prefer-implements-annotation` rule is an **opt-in migration helper** that i
19
19
 
20
20
  ### traceability/require-story-annotation
21
21
 
22
- 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 currently fixed but structured for future configurability, and fixes are strictly limited to adding this placeholder annotation without altering the function body or changing any runtime behavior. Selective enabling of different auto-fix behaviors (such as applying fixes only to certain scopes or node types) is planned for a future version.
22
+ 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.
23
23
 
24
24
  Options:
25
25
 
26
26
  - `scope` (string[], optional) – Controls which function-like node types are required to have @story annotations. Allowed values: "FunctionDeclaration", "FunctionExpression", "MethodDefinition", "TSDeclareFunction", "TSMethodSignature". Default: ["FunctionDeclaration", "FunctionExpression", "MethodDefinition", "TSDeclareFunction", "TSMethodSignature"].
27
27
  - `exportPriority` ("all" | "exported" | "non-exported", optional) – Controls whether the rule checks all functions, only exported ones, or only non-exported ones. Default: "all".
28
+ - `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.
29
+ - `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.
30
+ - `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.
28
31
 
29
32
  Default Severity: `error`
30
33
  Example:
@@ -83,7 +86,7 @@ if (error) {
83
86
 
84
87
  ### traceability/valid-annotation-format
85
88
 
86
- 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; more advanced path normalization strategies and selective toggles to enable or disable specific auto-fix behaviors are not yet implemented.
89
+ 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; more advanced path normalization strategies and selective toggles to enable or disable specific auto-fix behaviors are not yet implemented. You can also disable this suffix-normalization behavior explicitly via the `autoFix` option when you prefer purely diagnostic checks.
87
90
 
88
91
  Options:
89
92
 
@@ -95,6 +98,7 @@ This rule accepts an optional configuration object. The primary configuration sh
95
98
  - `req` (object, optional) – Configuration for `@req` values.
96
99
  - `pattern` (string, optional) – A JavaScript regular expression **source** (without leading and trailing `/`) that all `@req` values must match. If provided, the rule validates each `@req` identifier against this pattern. Defaults to the value returned by `getDefaultReqPattern()`, which is equivalent to `^REQ-[A-Z0-9_-]+$`, matching IDs such as `REQ-USER-AUTH` or `REQ-1234`.
97
100
  - `example` (string, optional) – A short example requirement ID shown in error messages when `req.pattern` is configured. Defaults to the value returned by `getDefaultReqExample()`, `"REQ-USER-AUTH"`. This value is used **only** for guidance and does not affect validation.
101
+ - `autoFix` (boolean, optional) – When set to `false`, disables all automatic suffix-normalization fixes while keeping validation and error messages intact. When omitted or `true`, the rule continues to apply safe suffix-only auto-fixes in `--fix` mode.
98
102
 
99
103
  For backward compatibility, the rule also supports **flat shorthand** fields that map directly to the nested properties:
100
104
 
@@ -203,7 +207,7 @@ Options:
203
207
 
204
208
  The rule accepts an optional configuration object:
205
209
 
206
- - `testFilePatterns` (string[], optional) – Glob-style patterns (relative to the project root) used to identify test files. Only files matching at least one pattern are checked by this rule. Defaults to `["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"]`.
210
+ - `testFilePatterns` (string[], optional) – **Path-substring patterns** used to identify test files. For each file, the rule normalizes the file path to use forward slashes and then checks whether it contains at least one of the configured pattern strings. This is intentionally simpler than full glob matching and avoids adding extra runtime dependencies. Defaults to `["/tests/", "/test/", "/__tests__", ".test.", ".spec."]`. For most projects, these defaults behave like "any file under a `tests` or `test` directory, or any file whose name includes `.test.` or `.spec.`". If you prefer a different layout, supply custom substrings that uniquely identify your test files.
207
211
  - `requireDescribeStory` (boolean, optional) – When `true` (default), requires that each top-level `describe` block include a story reference somewhere in its description text (for example, a path such as `docs/stories/010.0-PAYMENTS.story.md` or a shorter project-specific alias that your team uses consistently).
208
212
  - `requireTestReqPrefix` (boolean, optional) – When `true` (default), requires each `it`/`test` block name to begin with a requirement identifier in square brackets, such as `[REQ-PAYMENTS-REFUND]`. The exact `REQ-` pattern is shared with the `valid-annotation-format` rule’s requirement ID checks.
209
213
  - `describePattern` (string, optional) – A JavaScript regular expression **source** (without leading and trailing `/`) that the `describe` description text must match when `requireDescribeStory` is enabled. This lets you enforce a project-specific format such as requiring a canonical story path or a `STORY-` style identifier in the `describe` string. If omitted, a built-in default that loosely matches a typical story path (similar to `docs/stories/<name>.story.md`) is used.
@@ -213,7 +217,7 @@ The rule accepts an optional configuration object:
213
217
 
214
218
  Behavior notes:
215
219
 
216
- - The rule only analyzes files whose paths match `testFilePatterns`.
220
+ - The rule only analyzes files whose normalized paths contain at least one of the `testFilePatterns` substrings.
217
221
  - File-level `@supports` annotations are typically placed in a JSDoc block at the top of the file; the rule checks that at least one `@supports` tag is present and that it includes a story/requirement reference (for example, `@supports docs/stories/010.0-PAYMENTS.story.md#REQ-PAYMENTS-REFUND`).
218
222
  - Top-level `describe` calls (such as `describe("payments refunds docs/stories/010.0-PAYMENTS.story.md", ...)`) are inspected when `requireDescribeStory` is `true`. Their first argument must be a string literal that satisfies `describePattern`.
219
223
  - Test cases declared via `it(...)` or `test(...)` must use a string literal name beginning with a requirement prefix like `[REQ-PAYMENTS-REFUND]` when `requireTestReqPrefix` is `true`.
@@ -72,3 +72,45 @@ Then run:
72
72
  ```bash
73
73
  npm run lint:trace
74
74
  ```
75
+
76
+ ## 5. Test Traceability Example
77
+
78
+ This example complements the `traceability/require-test-traceability` rule and matches its default expectations for how stories and requirements are referenced from tests.
79
+
80
+ Create a Jest test file, for example `tests/dev-test-traceability.spec.ts`:
81
+
82
+ ```ts
83
+ /**
84
+ * @supports docs/stories/021.0-DEV-TEST-TRACEABILITY.story.md#REQ-TEST-TRACEABILITY
85
+ */
86
+
87
+ describe("docs/stories/021.0-DEV-TEST-TRACEABILITY.story.md", () => {
88
+ it("[REQ-TEST-TRACEABILITY] should handle the primary test scenario", () => {
89
+ // Arrange
90
+ const input = "happy-path";
91
+
92
+ // Act
93
+ const result = performOperation(input);
94
+
95
+ // Assert
96
+ expect(result).toBe("ok");
97
+ });
98
+
99
+ it("[REQ-TEST-TRACEABILITY-EDGE] should handle the edge-case scenario", () => {
100
+ // Arrange
101
+ const input = "edge-case";
102
+
103
+ // Act
104
+ const result = performOperation(input);
105
+
106
+ // Assert
107
+ expect(result).toBe("edge-ok");
108
+ });
109
+ });
110
+
111
+ // Example implementation under test (normally imported from your source code)
112
+ function performOperation(input: string): string {
113
+ if (input === "edge-case") return "edge-ok";
114
+ return "ok";
115
+ }
116
+ ```