eslint-plugin-traceability 1.6.4 → 1.7.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 (42) hide show
  1. package/README.md +38 -1
  2. package/lib/src/index.d.ts +28 -25
  3. package/lib/src/index.js +49 -31
  4. package/lib/src/maintenance/cli.d.ts +12 -0
  5. package/lib/src/maintenance/cli.js +279 -0
  6. package/lib/src/maintenance/detect.js +30 -15
  7. package/lib/src/maintenance/update.js +42 -34
  8. package/lib/src/maintenance/utils.js +30 -30
  9. package/lib/src/rules/helpers/require-story-io.js +51 -15
  10. package/lib/src/rules/helpers/require-story-visitors.js +5 -16
  11. package/lib/src/rules/helpers/valid-annotation-options.d.ts +118 -0
  12. package/lib/src/rules/helpers/valid-annotation-options.js +167 -0
  13. package/lib/src/rules/helpers/valid-annotation-utils.d.ts +68 -0
  14. package/lib/src/rules/helpers/valid-annotation-utils.js +103 -0
  15. package/lib/src/rules/helpers/valid-story-reference-helpers.d.ts +67 -0
  16. package/lib/src/rules/helpers/valid-story-reference-helpers.js +92 -0
  17. package/lib/src/rules/require-story-annotation.js +9 -14
  18. package/lib/src/rules/valid-annotation-format.js +168 -180
  19. package/lib/src/rules/valid-req-reference.js +139 -29
  20. package/lib/src/rules/valid-story-reference.d.ts +7 -0
  21. package/lib/src/rules/valid-story-reference.js +38 -80
  22. package/lib/src/utils/annotation-checker.js +2 -145
  23. package/lib/src/utils/branch-annotation-helpers.js +12 -3
  24. package/lib/src/utils/reqAnnotationDetection.d.ts +6 -0
  25. package/lib/src/utils/reqAnnotationDetection.js +152 -0
  26. package/lib/tests/maintenance/cli.test.d.ts +1 -0
  27. package/lib/tests/maintenance/cli.test.js +172 -0
  28. package/lib/tests/maintenance/detect-isolated.test.js +68 -1
  29. package/lib/tests/maintenance/report.test.js +2 -2
  30. package/lib/tests/rules/require-branch-annotation.test.js +3 -2
  31. package/lib/tests/rules/require-req-annotation.test.js +57 -68
  32. package/lib/tests/rules/require-story-annotation.test.js +13 -28
  33. package/lib/tests/rules/require-story-core-edgecases.test.js +3 -58
  34. package/lib/tests/rules/require-story-core.autofix.test.js +5 -41
  35. package/lib/tests/rules/valid-annotation-format.test.js +328 -51
  36. package/lib/tests/utils/annotation-checker.test.d.ts +23 -0
  37. package/lib/tests/utils/annotation-checker.test.js +24 -17
  38. package/lib/tests/utils/require-story-core-test-helpers.d.ts +10 -0
  39. package/lib/tests/utils/require-story-core-test-helpers.js +75 -0
  40. package/lib/tests/utils/ts-language-options.d.ts +22 -0
  41. package/lib/tests/utils/ts-language-options.js +27 -0
  42. package/package.json +12 -3
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hasReqAnnotation = hasReqAnnotation;
4
+ /**
5
+ * Shared @req detection helpers used by annotation-checker utilities.
6
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
7
+ * @req REQ-ANNOTATION-REQ-DETECTION - Detect @req markers around function-like nodes
8
+ */
9
+ const require_story_io_1 = require("../rules/helpers/require-story-io");
10
+ /**
11
+ * Predicate helper to check whether a comment contains a @req annotation.
12
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
13
+ * @req REQ-ANNOTATION-REQ-DETECTION - Detect @req tag inside a comment
14
+ */
15
+ function commentContainsReq(c) {
16
+ return c && typeof c.value === "string" && c.value.includes("@req");
17
+ }
18
+ /**
19
+ * Line-based helper adapted from linesBeforeHasStory to detect @req.
20
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
21
+ * @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in preceding source lines
22
+ */
23
+ function linesBeforeHasReq(sourceCode, node) {
24
+ const lines = sourceCode && sourceCode.lines;
25
+ const startLine = node && node.loc && typeof node.loc.start?.line === "number"
26
+ ? node.loc.start.line
27
+ : null;
28
+ // Guard against missing or malformed source/loc information before scanning.
29
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
30
+ // @req REQ-ANNOTATION-REQ-DETECTION - Avoid false positives when sourceCode/loc is incomplete
31
+ if (!Array.isArray(lines) || typeof startLine !== "number") {
32
+ return false;
33
+ }
34
+ const from = Math.max(0, startLine - 1 - require_story_io_1.LOOKBACK_LINES);
35
+ const to = Math.max(0, startLine - 1);
36
+ // Scan each physical line in the configured lookback window for an @req marker.
37
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
38
+ // @req REQ-ANNOTATION-REQ-DETECTION - Search preceding lines for @req text
39
+ for (let i = from; i < to; i++) {
40
+ const text = lines[i];
41
+ // When a line contains @req we treat the function as already annotated.
42
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
43
+ // @req REQ-ANNOTATION-REQ-DETECTION - Detect @req marker in raw source lines
44
+ if (typeof text === "string" && text.includes("@req")) {
45
+ return true;
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+ /**
51
+ * Parent-chain helper adapted from parentChainHasStory to detect @req.
52
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
53
+ * @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in parent-chain comments
54
+ */
55
+ function parentChainHasReq(sourceCode, node) {
56
+ let p = node && node.parent;
57
+ // Walk up the parent chain and inspect comments attached to each ancestor.
58
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
59
+ // @req REQ-ANNOTATION-REQ-DETECTION - Traverse parent nodes when local comments are absent
60
+ while (p) {
61
+ const pComments = typeof sourceCode?.getCommentsBefore === "function"
62
+ ? sourceCode.getCommentsBefore(p) || []
63
+ : [];
64
+ // Look for @req in comments immediately preceding each parent node.
65
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
66
+ // @req REQ-ANNOTATION-REQ-DETECTION - Detect @req markers in parent comments
67
+ if (Array.isArray(pComments) && pComments.some(commentContainsReq)) {
68
+ return true;
69
+ }
70
+ const pLeading = p.leadingComments || [];
71
+ // Also inspect leadingComments attached directly to the parent node.
72
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
73
+ // @req REQ-ANNOTATION-REQ-DETECTION - Detect @req markers in parent leadingComments
74
+ if (Array.isArray(pLeading) && pLeading.some(commentContainsReq)) {
75
+ return true;
76
+ }
77
+ p = p.parent;
78
+ }
79
+ return false;
80
+ }
81
+ /**
82
+ * Fallback text window helper adapted from fallbackTextBeforeHasStory to detect @req.
83
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
84
+ * @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in fallback text window before node
85
+ */
86
+ function fallbackTextBeforeHasReq(sourceCode, node) {
87
+ // Guard against unsupported sourceCode or nodes without a usable range.
88
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
89
+ // @req REQ-ANNOTATION-REQ-DETECTION - Ensure we only inspect text when range information is available
90
+ if (typeof sourceCode?.getText !== "function" ||
91
+ !Array.isArray((node && node.range) || [])) {
92
+ return false;
93
+ }
94
+ const range = node.range;
95
+ // Guard when the node range cannot provide a numeric start index.
96
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
97
+ // @req REQ-ANNOTATION-REQ-DETECTION - Avoid scanning when range start is not a number
98
+ if (!Array.isArray(range) || typeof range[0] !== "number") {
99
+ return false;
100
+ }
101
+ try {
102
+ const start = Math.max(0, range[0] - require_story_io_1.FALLBACK_WINDOW);
103
+ const textBefore = sourceCode.getText().slice(start, range[0]);
104
+ // Detect @req in the bounded text window immediately preceding the node.
105
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
106
+ // @req REQ-ANNOTATION-REQ-DETECTION - Detect @req marker in fallback text window
107
+ if (typeof textBefore === "string" && textBefore.includes("@req")) {
108
+ return true;
109
+ }
110
+ }
111
+ catch {
112
+ // Swallow detection errors to avoid breaking lint runs due to malformed source.
113
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
114
+ // @req REQ-ANNOTATION-REQ-DETECTION - Treat IO/detection failures as "no annotation" instead of throwing
115
+ /* noop */
116
+ }
117
+ return false;
118
+ }
119
+ /**
120
+ * Helper to determine whether a JSDoc or any nearby comments contain a @req annotation.
121
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
122
+ * @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
123
+ */
124
+ function hasReqAnnotation(jsdoc, comments, context, node) {
125
+ try {
126
+ const sourceCode = context && typeof context.getSourceCode === "function"
127
+ ? context.getSourceCode()
128
+ : undefined;
129
+ // Prefer robust, location-based heuristics when sourceCode and node are available.
130
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
131
+ // @req REQ-ANNOTATION-REQ-DETECTION - Use multiple heuristics to detect @req markers around the node
132
+ if (sourceCode && node) {
133
+ if (linesBeforeHasReq(sourceCode, node) ||
134
+ parentChainHasReq(sourceCode, node) ||
135
+ fallbackTextBeforeHasReq(sourceCode, node)) {
136
+ return true;
137
+ }
138
+ }
139
+ }
140
+ catch {
141
+ // Swallow detection errors and fall through to simple checks.
142
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
143
+ // @req REQ-ANNOTATION-REQ-DETECTION - Fail gracefully when advanced detection heuristics throw
144
+ }
145
+ // BRANCH @req detection on JSDoc or comments
146
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
147
+ // @req REQ-ANNOTATION-REQ-DETECTION
148
+ return ((jsdoc &&
149
+ typeof jsdoc.value === "string" &&
150
+ jsdoc.value.includes("@req")) ||
151
+ comments.some(commentContainsReq));
152
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,172 @@
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/009.0-DEV-MAINTENANCE-TOOLS.story.md
8
+ * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
9
+ * @req REQ-MAINT-DETECT - CLI detection of stale annotations
10
+ * @req REQ-MAINT-VERIFY - CLI verification of annotations
11
+ * @req REQ-MAINT-REPORT - CLI reporting of stale annotations
12
+ * @req REQ-MAINT-UPDATE - CLI updating of annotation references
13
+ * @req REQ-MAINT-SAFE - Clear exit codes and non-destructive dry-run
14
+ */
15
+ const fs_1 = __importDefault(require("fs"));
16
+ const os_1 = __importDefault(require("os"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const cli_1 = require("../../src/maintenance/cli");
19
+ function withTempDir() {
20
+ const tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "maint-cli-"));
21
+ return tmpDir;
22
+ }
23
+ describe("Maintenance CLI (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
24
+ let originalCwd;
25
+ beforeAll(() => {
26
+ originalCwd = process.cwd();
27
+ });
28
+ afterAll(() => {
29
+ process.chdir(originalCwd);
30
+ });
31
+ it("[REQ-MAINT-DETECT] detect exits with code 0 and message when no stale annotations", () => {
32
+ const dir = withTempDir();
33
+ process.chdir(dir);
34
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
35
+ const code = (0, cli_1.runMaintenanceCli)(["node", "traceability-maint", "detect"]);
36
+ try {
37
+ expect(code).toBe(0);
38
+ expect(logSpy).toHaveBeenCalledWith("No stale @story annotations found.");
39
+ }
40
+ finally {
41
+ logSpy.mockRestore();
42
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
43
+ }
44
+ });
45
+ it("[REQ-MAINT-VERIFY] verify exits with code 0 when annotations valid", () => {
46
+ const dir = withTempDir();
47
+ process.chdir(dir);
48
+ const tsContent = `/**\n * @story my-story.story.md\n */`;
49
+ fs_1.default.writeFileSync(path_1.default.join(dir, "file.ts"), tsContent, "utf8");
50
+ fs_1.default.writeFileSync(path_1.default.join(dir, "my-story.story.md"), "# Dummy Story", "utf8");
51
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
52
+ const code = (0, cli_1.runMaintenanceCli)(["node", "traceability-maint", "verify"]);
53
+ try {
54
+ expect(code).toBe(0);
55
+ expect(logSpy).toHaveBeenCalledTimes(1);
56
+ }
57
+ finally {
58
+ logSpy.mockRestore();
59
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
60
+ }
61
+ });
62
+ it("[REQ-MAINT-REPORT] report prints human-readable summary and exits 0", () => {
63
+ const dir = withTempDir();
64
+ process.chdir(dir);
65
+ const tsContent = `/**\n * @story missing.story.md\n */`;
66
+ fs_1.default.writeFileSync(path_1.default.join(dir, "file.ts"), tsContent, "utf8");
67
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
68
+ const code = (0, cli_1.runMaintenanceCli)(["node", "traceability-maint", "report"]);
69
+ try {
70
+ expect(code).toBe(0);
71
+ const allMessages = logSpy.mock.calls.flat().join("\n");
72
+ expect(allMessages).toContain("Traceability Maintenance Report");
73
+ expect(allMessages).toContain("missing.story.md");
74
+ }
75
+ finally {
76
+ logSpy.mockRestore();
77
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
78
+ }
79
+ });
80
+ it("[REQ-MAINT-UPDATE] update performs replacements and exits 0", () => {
81
+ const dir = withTempDir();
82
+ process.chdir(dir);
83
+ const tsContent = `/**\n * @story old.path.md\n */`;
84
+ fs_1.default.writeFileSync(path_1.default.join(dir, "file.ts"), tsContent, "utf8");
85
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
86
+ const code = (0, cli_1.runMaintenanceCli)([
87
+ "node",
88
+ "traceability-maint",
89
+ "update",
90
+ "--from",
91
+ "old.path.md",
92
+ "--to",
93
+ "new.path.md",
94
+ ]);
95
+ try {
96
+ expect(code).toBe(0);
97
+ const updated = fs_1.default.readFileSync(path_1.default.join(dir, "file.ts"), "utf8");
98
+ expect(updated).toContain("@story new.path.md");
99
+ }
100
+ finally {
101
+ logSpy.mockRestore();
102
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
103
+ }
104
+ });
105
+ it("[REQ-MAINT-SAFE] update requires --from and --to and exits 2 when missing", () => {
106
+ const dir = withTempDir();
107
+ process.chdir(dir);
108
+ const errorSpy = jest.spyOn(console, "error").mockImplementation(() => { });
109
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
110
+ const code = (0, cli_1.runMaintenanceCli)(["node", "traceability-maint", "update"]);
111
+ try {
112
+ expect(code).toBe(2);
113
+ expect(errorSpy).toHaveBeenCalled();
114
+ expect(logSpy).toHaveBeenCalled();
115
+ }
116
+ finally {
117
+ errorSpy.mockRestore();
118
+ logSpy.mockRestore();
119
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
120
+ }
121
+ });
122
+ it("[REQ-MAINT-SAFE] dry-run does not modify files and exits 0", () => {
123
+ const dir = withTempDir();
124
+ process.chdir(dir);
125
+ const tsContent = `/**\n * @story old.path.md\n */`;
126
+ fs_1.default.writeFileSync(path_1.default.join(dir, "file.ts"), tsContent, "utf8");
127
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
128
+ const code = (0, cli_1.runMaintenanceCli)([
129
+ "node",
130
+ "traceability-maint",
131
+ "update",
132
+ "--from",
133
+ "old.path.md",
134
+ "--to",
135
+ "new.path.md",
136
+ "--dry-run",
137
+ ]);
138
+ try {
139
+ expect(code).toBe(0);
140
+ const contentAfter = fs_1.default.readFileSync(path_1.default.join(dir, "file.ts"), "utf8");
141
+ expect(contentAfter).toBe(tsContent);
142
+ }
143
+ finally {
144
+ logSpy.mockRestore();
145
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
146
+ }
147
+ });
148
+ it("[REQ-MAINT-DETECT] detect supports --json output", () => {
149
+ const dir = withTempDir();
150
+ process.chdir(dir);
151
+ const tsContent = `/**\n * @story stale.story.md\n */`;
152
+ fs_1.default.writeFileSync(path_1.default.join(dir, "file.ts"), tsContent, "utf8");
153
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
154
+ const code = (0, cli_1.runMaintenanceCli)([
155
+ "node",
156
+ "traceability-maint",
157
+ "detect",
158
+ "--json",
159
+ ]);
160
+ try {
161
+ expect(code).toBe(1);
162
+ expect(logSpy).toHaveBeenCalledTimes(1);
163
+ const payload = JSON.parse(String(logSpy.mock.calls[0][0]));
164
+ expect(Array.isArray(payload.stale)).toBe(true);
165
+ expect(payload.stale).toContain("stale.story.md");
166
+ }
167
+ finally {
168
+ logSpy.mockRestore();
169
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
170
+ }
171
+ });
172
+ });
@@ -38,10 +38,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
38
38
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
39
39
  * @req REQ-MAINT-DETECT - Detect stale annotation references
40
40
  */
41
- const fs = __importStar(require("fs"));
42
41
  const path = __importStar(require("path"));
43
42
  const os = __importStar(require("os"));
44
43
  const detect_1 = require("../../src/maintenance/detect");
44
+ const fs = require("fs");
45
45
  describe("detectStaleAnnotations isolated (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
46
46
  it("[REQ-MAINT-DETECT] returns empty array when directory does not exist", () => {
47
47
  const result = (0, detect_1.detectStaleAnnotations)("non-existent-dir");
@@ -107,4 +107,71 @@ describe("detectStaleAnnotations isolated (Story 009.0-DEV-MAINTENANCE-TOOLS)",
107
107
  }
108
108
  }
109
109
  });
110
+ /**
111
+ * [REQ-MAINT-DETECT]
112
+ * Ensure detectStaleAnnotations performs security validation for unsafe
113
+ * and invalid-extension story paths and does not perform filesystem checks
114
+ * for malicious @story paths that escape the workspace
115
+ * (Story 009.0-DEV-MAINTENANCE-TOOLS).
116
+ */
117
+ it("[REQ-MAINT-DETECT] performs security validation for unsafe and invalid-extension story paths without stat'ing outside workspace", () => {
118
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tmp-workspace-"));
119
+ const maliciousRelative = "../outside-project.story.md";
120
+ const maliciousAbsolute = "/etc/passwd.story.md";
121
+ const traversalInside = "nested/../inside.story.md";
122
+ const invalidExtension = "invalid.txt";
123
+ const filePath = path.join(tmpDir, "file.ts");
124
+ const content = `
125
+ /**
126
+ * @story ${maliciousRelative}
127
+ * @story ${maliciousAbsolute}
128
+ * @story ${traversalInside}
129
+ * @story ${invalidExtension}
130
+ * @story legitimate.story.md
131
+ */
132
+ `;
133
+ fs.writeFileSync(filePath, content, "utf8");
134
+ const existsCalls = [];
135
+ const originalExistsSync = fs.existsSync;
136
+ const existsSpy = jest
137
+ .spyOn(fs, "existsSync")
138
+ .mockImplementation((p) => {
139
+ const strPath = typeof p === "string" ? p : p.toString();
140
+ existsCalls.push(strPath);
141
+ return originalExistsSync(p);
142
+ });
143
+ try {
144
+ (0, detect_1.detectStaleAnnotations)(tmpDir);
145
+ const allPathsChecked = [...existsCalls];
146
+ // Ensure no raw malicious values were checked
147
+ expect(allPathsChecked).not.toContain(maliciousRelative);
148
+ expect(allPathsChecked).not.toContain(maliciousAbsolute);
149
+ expect(allPathsChecked).not.toContain(invalidExtension);
150
+ // Also ensure no resolved variants of these paths were checked
151
+ const resolvedRelative = path.resolve(tmpDir, maliciousRelative);
152
+ const resolvedAbsolute = path.resolve(maliciousAbsolute);
153
+ const resolvedInvalid = path.resolve(tmpDir, invalidExtension);
154
+ expect(allPathsChecked).not.toContain(resolvedRelative);
155
+ expect(allPathsChecked).not.toContain(resolvedAbsolute);
156
+ expect(allPathsChecked).not.toContain(resolvedInvalid);
157
+ expect(allPathsChecked.some((p) => p.includes("outside-project.story.md"))).toBe(false);
158
+ expect(allPathsChecked.some((p) => p.includes("passwd.story.md"))).toBe(false);
159
+ expect(allPathsChecked.some((p) => p.includes("invalid.txt"))).toBe(false);
160
+ // traversalInside normalizes within workspace and should be checked
161
+ const resolvedTraversalInside = path.resolve(tmpDir, traversalInside);
162
+ expect(allPathsChecked).toContain(resolvedTraversalInside);
163
+ // legitimate in-workspace .story.md path should also be checked
164
+ const resolvedLegit = path.resolve(tmpDir, "legitimate.story.md");
165
+ expect(allPathsChecked).toContain(resolvedLegit);
166
+ }
167
+ finally {
168
+ existsSpy.mockRestore();
169
+ try {
170
+ fs.rmSync(tmpDir, { recursive: true, force: true });
171
+ }
172
+ catch {
173
+ // ignore cleanup errors
174
+ }
175
+ }
176
+ });
110
177
  });
@@ -58,10 +58,10 @@ describe("generateMaintenanceReport (Story 009.0-DEV-MAINTENANCE-TOOLS)", () =>
58
58
  it("[REQ-MAINT-REPORT] should report stale story annotation", () => {
59
59
  const filePath = path.join(tmpDir, "stub.md");
60
60
  const content = `/**
61
- * @story non-existent.md
61
+ * @story non-existent.story.md
62
62
  */`;
63
63
  fs.writeFileSync(filePath, content);
64
64
  const report = (0, report_1.generateMaintenanceReport)(tmpDir);
65
- expect(report).toContain("non-existent.md");
65
+ expect(report).toContain("non-existent.story.md");
66
66
  });
67
67
  });
@@ -17,8 +17,9 @@ const require_branch_annotation_1 = __importDefault(require("../../src/rules/req
17
17
  const ruleTester = new eslint_1.RuleTester({
18
18
  languageOptions: { parserOptions: { ecmaVersion: 2020 } },
19
19
  });
20
+ const runRule = (tests) => ruleTester.run("require-branch-annotation", require_branch_annotation_1.default, tests);
20
21
  describe("Require Branch Annotation Rule (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () => {
21
- ruleTester.run("require-branch-annotation", require_branch_annotation_1.default, {
22
+ runRule({
22
23
  valid: [
23
24
  {
24
25
  name: "[REQ-BRANCH-DETECTION] valid fallback scanning comment detection",
@@ -277,7 +278,7 @@ for (let i = 0; i < 3; i++) {}`,
277
278
  },
278
279
  ],
279
280
  });
280
- ruleTester.run("require-branch-annotation", require_branch_annotation_1.default, {
281
+ runRule({
281
282
  valid: [],
282
283
  invalid: [
283
284
  {
@@ -9,14 +9,63 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  * @req REQ-ANNOTATION-REQUIRED - Verify require-req-annotation rule enforces @req on functions
10
10
  * @story docs/stories/007.0-DEV-ERROR-REPORTING.story.md
11
11
  * @req REQ-ERROR-SPECIFIC - Verify enhanced, specific error messaging behavior
12
+ *
13
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
14
+ * @req REQ-TYPESCRIPT-SUPPORT - Verify TypeScript declarations are checked via shared annotation checker helper
12
15
  */
13
16
  const eslint_1 = require("eslint");
14
17
  const require_req_annotation_1 = __importDefault(require("../../src/rules/require-req-annotation"));
18
+ const ts_language_options_1 = require("../utils/ts-language-options");
19
+ const annotation_checker_test_1 = require("../utils/annotation-checker.test");
15
20
  const ruleTester = new eslint_1.RuleTester({
16
21
  languageOptions: {
17
22
  parserOptions: { ecmaVersion: 2022, sourceType: "module" },
18
23
  },
19
24
  });
25
+ /**
26
+ * @trace Story 003.0-DEV-FUNCTION-ANNOTATIONS / REQ-TYPESCRIPT-SUPPORT
27
+ * Exercise the require-req-annotation rule's behavior on TSDeclareFunction and
28
+ * TSMethodSignature via the shared runAnnotationCheckerTests helper.
29
+ *
30
+ * The helper delegates to the real rule's TypeScript-specific visitors
31
+ * without changing its behavior; it provides the common TS parser
32
+ * configuration and assertion plumbing.
33
+ */
34
+ (0, annotation_checker_test_1.runAnnotationCheckerTests)("require-req-annotation", {
35
+ rule: require_req_annotation_1.default,
36
+ valid: [
37
+ {
38
+ name: "[REQ-TYPESCRIPT-SUPPORT] valid with @req annotation on TSDeclareFunction",
39
+ code: `/**\n * @req REQ-EXAMPLE\n */\ndeclare function foo(): void;`,
40
+ },
41
+ {
42
+ name: "[REQ-TYPESCRIPT-SUPPORT] valid with @req annotation on TSMethodSignature",
43
+ code: `interface I {\n /**\n * @req REQ-EXAMPLE\n */\n method(): void;\n}`,
44
+ },
45
+ ],
46
+ invalid: [
47
+ {
48
+ name: "[REQ-TYPESCRIPT-SUPPORT] missing @req on TSDeclareFunction",
49
+ code: `declare function baz(): void;`,
50
+ errors: [
51
+ {
52
+ messageId: "missingReq",
53
+ data: { name: "baz", functionName: "baz" },
54
+ },
55
+ ],
56
+ },
57
+ {
58
+ name: "[REQ-TYPESCRIPT-SUPPORT] missing @req on TSMethodSignature",
59
+ code: `interface I { method(): void; }`,
60
+ errors: [
61
+ {
62
+ messageId: "missingReq",
63
+ data: { name: "method", functionName: "method" },
64
+ },
65
+ ],
66
+ },
67
+ ],
68
+ });
20
69
  describe("Require Req Annotation Rule (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", () => {
21
70
  ruleTester.run("require-req-annotation", require_req_annotation_1.default, {
22
71
  valid: [
@@ -28,22 +77,6 @@ describe("Require Req Annotation Rule (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", (
28
77
  name: "[REQ-ANNOTATION-REQUIRED] valid with @story and @req annotations",
29
78
  code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-EXAMPLE\n */\nfunction bar() {}`,
30
79
  },
31
- {
32
- name: "[REQ-TYPESCRIPT-SUPPORT] valid with @req annotation on TSDeclareFunction",
33
- code: `/**\n * @req REQ-EXAMPLE\n */\ndeclare function foo(): void;`,
34
- languageOptions: {
35
- parser: require("@typescript-eslint/parser"),
36
- parserOptions: { ecmaVersion: 2022, sourceType: "module" },
37
- },
38
- },
39
- {
40
- name: "[REQ-TYPESCRIPT-SUPPORT] valid with @req annotation on TSMethodSignature",
41
- code: `interface I {\n /**\n * @req REQ-EXAMPLE\n */\n method(): void;\n}`,
42
- languageOptions: {
43
- parser: require("@typescript-eslint/parser"),
44
- parserOptions: { ecmaVersion: 2022, sourceType: "module" },
45
- },
46
- },
47
80
  {
48
81
  name: "[REQ-FUNCTION-DETECTION][Story 003.0] valid FunctionExpression with @req annotation",
49
82
  code: `const fn = /**\n * @req REQ-EXAMPLE\n */\nfunction() {};`,
@@ -52,22 +85,14 @@ describe("Require Req Annotation Rule (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", (
52
85
  name: "[REQ-FUNCTION-DETECTION][Story 003.0] valid MethodDefinition with @req annotation",
53
86
  code: `class C {\n /**\n * @req REQ-EXAMPLE\n */\n m() {}\n}`,
54
87
  },
55
- {
88
+ (0, ts_language_options_1.withTsLanguageOptions)({
56
89
  name: "[REQ-TYPESCRIPT-SUPPORT][REQ-FUNCTION-DETECTION][Story 003.0] valid TS FunctionExpression in variable declarator with @req",
57
90
  code: `const fn = /**\n * @req REQ-EXAMPLE\n */\nfunction () {};`,
58
- languageOptions: {
59
- parser: require("@typescript-eslint/parser"),
60
- parserOptions: { ecmaVersion: 2022, sourceType: "module" },
61
- },
62
- },
63
- {
91
+ }),
92
+ (0, ts_language_options_1.withTsLanguageOptions)({
64
93
  name: "[REQ-TYPESCRIPT-SUPPORT][REQ-FUNCTION-DETECTION][Story 003.0] valid exported TS FunctionExpression in variable declarator with @req",
65
94
  code: `export const fn = /**\n * @req REQ-EXAMPLE\n */\nfunction () {};`,
66
- languageOptions: {
67
- parser: require("@typescript-eslint/parser"),
68
- parserOptions: { ecmaVersion: 2022, sourceType: "module" },
69
- },
70
- },
95
+ }),
71
96
  {
72
97
  name: "[REQ-CONFIGURABLE-SCOPE][Story 003.0] FunctionExpression ignored when scope only includes FunctionDeclaration",
73
98
  code: `const fn = function () {};`,
@@ -125,34 +150,6 @@ describe("Require Req Annotation Rule (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", (
125
150
  },
126
151
  ],
127
152
  },
128
- {
129
- name: "[REQ-TYPESCRIPT-SUPPORT] missing @req on TSDeclareFunction",
130
- code: `declare function baz(): void;`,
131
- errors: [
132
- {
133
- messageId: "missingReq",
134
- data: { name: "baz", functionName: "baz" },
135
- },
136
- ],
137
- languageOptions: {
138
- parser: require("@typescript-eslint/parser"),
139
- parserOptions: { ecmaVersion: 2022, sourceType: "module" },
140
- },
141
- },
142
- {
143
- name: "[REQ-TYPESCRIPT-SUPPORT] missing @req on TSMethodSignature",
144
- code: `interface I { method(): void; }`,
145
- errors: [
146
- {
147
- messageId: "missingReq",
148
- data: { name: "method", functionName: "method" },
149
- },
150
- ],
151
- languageOptions: {
152
- parser: require("@typescript-eslint/parser"),
153
- parserOptions: { ecmaVersion: 2022, sourceType: "module" },
154
- },
155
- },
156
153
  {
157
154
  name: "[REQ-FUNCTION-DETECTION][Story 003.0] missing @req on FunctionExpression assigned to variable",
158
155
  code: `const fn = function () {};`,
@@ -193,7 +190,7 @@ describe("Require Req Annotation Rule (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", (
193
190
  },
194
191
  ],
195
192
  },
196
- {
193
+ (0, ts_language_options_1.withTsLanguageOptions)({
197
194
  name: "[REQ-TYPESCRIPT-SUPPORT][REQ-FUNCTION-DETECTION][Story 003.0] missing @req on TS FunctionExpression in variable declarator",
198
195
  code: `const fn = function () {};`,
199
196
  errors: [
@@ -202,12 +199,8 @@ describe("Require Req Annotation Rule (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", (
202
199
  data: { name: "fn", functionName: "fn" },
203
200
  },
204
201
  ],
205
- languageOptions: {
206
- parser: require("@typescript-eslint/parser"),
207
- parserOptions: { ecmaVersion: 2022, sourceType: "module" },
208
- },
209
- },
210
- {
202
+ }),
203
+ (0, ts_language_options_1.withTsLanguageOptions)({
211
204
  name: "[REQ-TYPESCRIPT-SUPPORT][REQ-FUNCTION-DETECTION][Story 003.0] missing @req on exported TS FunctionExpression in variable declarator",
212
205
  code: `export const fn = function () {};`,
213
206
  errors: [
@@ -216,11 +209,7 @@ describe("Require Req Annotation Rule (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", (
216
209
  data: { name: "fn", functionName: "fn" },
217
210
  },
218
211
  ],
219
- languageOptions: {
220
- parser: require("@typescript-eslint/parser"),
221
- parserOptions: { ecmaVersion: 2022, sourceType: "module" },
222
- },
223
- },
212
+ }),
224
213
  {
225
214
  name: "[REQ-CONFIGURABLE-SCOPE][Story 003.0] FunctionDeclaration still reported when scope only includes FunctionDeclaration",
226
215
  code: `function scoped() {}`,