eslint-plugin-traceability 1.7.0 → 1.8.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 (107) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +73 -32
  3. package/docs/ci-cd-pipeline.md +224 -0
  4. package/docs/cli-integration.md +22 -0
  5. package/docs/code-quality-refactor-opportunities-2025-12-03.md +78 -0
  6. package/docs/config-presets.md +38 -0
  7. package/docs/conventional-commits-guide.md +185 -0
  8. package/docs/custom-rules-development-guide.md +659 -0
  9. package/docs/decisions/0001-allow-dynamic-require-for-built-plugins.md +26 -0
  10. package/docs/decisions/001-typescript-for-eslint-plugin.accepted.md +111 -0
  11. package/docs/decisions/002-jest-for-eslint-testing.accepted.md +137 -0
  12. package/docs/decisions/003-code-quality-ratcheting-plan.md +48 -0
  13. package/docs/decisions/004-automated-version-bumping-for-ci-cd.md +196 -0
  14. package/docs/decisions/005-github-actions-validation-tooling.accepted.md +144 -0
  15. package/docs/decisions/006-semantic-release-for-automated-publishing.accepted.md +227 -0
  16. package/docs/decisions/007-github-releases-over-changelog.accepted.md +216 -0
  17. package/docs/decisions/008-ci-audit-flags.accepted.md +60 -0
  18. package/docs/decisions/009-security-focused-lint-rules.accepted.md +64 -0
  19. package/docs/decisions/010-implements-annotation-for-multi-story-requirements.proposed.md +184 -0
  20. package/docs/decisions/adr-0001-console-usage-for-cli-guards.md +190 -0
  21. package/docs/decisions/adr-accept-dev-dep-risk-glob.md +40 -0
  22. package/docs/decisions/adr-commit-branch-tests.md +54 -0
  23. package/docs/decisions/adr-maintenance-cli-interface.md +140 -0
  24. package/docs/decisions/adr-pre-push-parity.md +112 -0
  25. package/docs/decisions/code-quality-ratcheting-plan.md +53 -0
  26. package/docs/dependency-health.md +238 -0
  27. package/docs/eslint-9-setup-guide.md +517 -0
  28. package/docs/eslint-plugin-development-guide.md +487 -0
  29. package/docs/functionality-coverage-2025-12-03.md +250 -0
  30. package/docs/jest-testing-guide.md +100 -0
  31. package/docs/rules/prefer-implements-annotation.md +219 -0
  32. package/docs/rules/require-branch-annotation.md +71 -0
  33. package/docs/rules/require-req-annotation.md +203 -0
  34. package/docs/rules/require-story-annotation.md +159 -0
  35. package/docs/rules/valid-annotation-format.md +418 -0
  36. package/docs/rules/valid-req-reference.md +153 -0
  37. package/docs/rules/valid-story-reference.md +120 -0
  38. package/docs/security-incidents/2025-11-17-glob-cli-incident.md +45 -0
  39. package/docs/security-incidents/2025-11-18-brace-expansion-redos.md +45 -0
  40. package/docs/security-incidents/2025-11-18-bundled-dev-deps-accepted-risk.md +93 -0
  41. package/docs/security-incidents/2025-11-18-tar-race-condition.md +43 -0
  42. package/docs/security-incidents/2025-12-03-dependency-health-review.md +58 -0
  43. package/docs/security-incidents/SECURITY-INCIDENT-2025-11-18-semantic-release-bundled-npm.known-error.md +104 -0
  44. package/docs/security-incidents/SECURITY-INCIDENT-TEMPLATE.md +37 -0
  45. package/docs/security-incidents/dependency-override-rationale.md +57 -0
  46. package/docs/security-incidents/dev-deps-high.json +116 -0
  47. package/docs/security-incidents/handling-procedure.md +54 -0
  48. package/docs/stories/001.0-DEV-PLUGIN-SETUP.story.md +92 -0
  49. package/docs/stories/002.0-DEV-ESLINT-CONFIG.story.md +82 -0
  50. package/docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md +112 -0
  51. package/docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md +153 -0
  52. package/docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md +138 -0
  53. package/docs/stories/006.0-DEV-FILE-VALIDATION.story.md +144 -0
  54. package/docs/stories/007.0-DEV-ERROR-REPORTING.story.md +163 -0
  55. package/docs/stories/008.0-DEV-AUTO-FIX.story.md +150 -0
  56. package/docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md +117 -0
  57. package/docs/stories/010.0-DEV-DEEP-VALIDATION.story.md +124 -0
  58. package/docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md +149 -0
  59. package/docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md +216 -0
  60. package/docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md +236 -0
  61. package/docs/stories/developer-story.map.md +120 -0
  62. package/docs/ts-jest-presets-guide.md +548 -0
  63. package/lib/src/index.d.ts +2 -2
  64. package/lib/src/index.js +2 -0
  65. package/lib/src/maintenance/batch.d.ts +5 -0
  66. package/lib/src/maintenance/batch.js +5 -0
  67. package/lib/src/maintenance/cli.js +34 -212
  68. package/lib/src/maintenance/commands.d.ts +32 -0
  69. package/lib/src/maintenance/commands.js +139 -0
  70. package/lib/src/maintenance/detect.d.ts +2 -0
  71. package/lib/src/maintenance/detect.js +4 -0
  72. package/lib/src/maintenance/flags.d.ts +99 -0
  73. package/lib/src/maintenance/flags.js +121 -0
  74. package/lib/src/maintenance/report.d.ts +2 -0
  75. package/lib/src/maintenance/report.js +2 -0
  76. package/lib/src/maintenance/update.d.ts +4 -0
  77. package/lib/src/maintenance/update.js +4 -0
  78. package/lib/src/rules/helpers/require-story-io.d.ts +3 -0
  79. package/lib/src/rules/helpers/require-story-io.js +20 -6
  80. package/lib/src/rules/helpers/valid-annotation-format-internal.d.ts +30 -0
  81. package/lib/src/rules/helpers/valid-annotation-format-internal.js +36 -0
  82. package/lib/src/rules/helpers/valid-annotation-options.js +15 -4
  83. package/lib/src/rules/helpers/valid-annotation-utils.js +5 -0
  84. package/lib/src/rules/helpers/valid-implements-utils.d.ts +75 -0
  85. package/lib/src/rules/helpers/valid-implements-utils.js +149 -0
  86. package/lib/src/rules/helpers/valid-story-reference-helpers.d.ts +3 -4
  87. package/lib/src/rules/prefer-implements-annotation.d.ts +39 -0
  88. package/lib/src/rules/prefer-implements-annotation.js +276 -0
  89. package/lib/src/rules/valid-annotation-format.js +87 -28
  90. package/lib/src/rules/valid-req-reference.js +71 -0
  91. package/lib/src/utils/reqAnnotationDetection.d.ts +4 -1
  92. package/lib/src/utils/reqAnnotationDetection.js +43 -15
  93. package/lib/tests/maintenance/cli.test.js +89 -0
  94. package/lib/tests/plugin-default-export-and-configs.test.js +3 -0
  95. package/lib/tests/rules/prefer-implements-annotation.test.d.ts +1 -0
  96. package/lib/tests/rules/prefer-implements-annotation.test.js +84 -0
  97. package/lib/tests/rules/require-req-annotation.test.js +8 -1
  98. package/lib/tests/rules/require-story-annotation.test.js +9 -4
  99. package/lib/tests/rules/valid-annotation-format.test.js +78 -0
  100. package/lib/tests/rules/valid-req-reference.test.js +34 -0
  101. package/lib/tests/utils/ts-language-options.d.ts +1 -7
  102. package/lib/tests/utils/ts-language-options.js +8 -5
  103. package/package.json +7 -3
  104. package/user-docs/api-reference.md +507 -0
  105. package/user-docs/eslint-9-setup-guide.md +639 -0
  106. package/user-docs/examples.md +74 -0
  107. package/user-docs/migration-guide.md +158 -0
@@ -14,6 +14,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
14
14
  */
15
15
  const fs_1 = __importDefault(require("fs"));
16
16
  const path_1 = __importDefault(require("path"));
17
+ /**
18
+ * Token index configuration for @implements annotations.
19
+ * This clarifies the expected positions of the story path and first requirement ID
20
+ * and avoids hard-coded "magic number" indices in parsing logic.
21
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
22
+ */
23
+ const IMPLEMENTS_TOKENS = {
24
+ STORY_INDEX: 1,
25
+ FIRST_REQ_INDEX: 2,
26
+ };
17
27
  /**
18
28
  * Extract the story path from a JSDoc comment.
19
29
  * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
@@ -164,11 +174,68 @@ function validateReqLine(opts) {
164
174
  reqSet,
165
175
  });
166
176
  }
177
+ /**
178
+ * Parse an @implements annotation line into its story path and requirement IDs.
179
+ * Expects the format: "@implements <storyPath> <REQ-ID-1> <REQ-ID-2> ..."
180
+ * Invalid formats (missing storyPath or reqIds) are ignored by this deep rule.
181
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
182
+ * @req REQ-IMPLEMENTS-VALIDATE - Support validation of @implements annotations
183
+ * @req REQ-MIXED-SUPPORT - Allow mixed @story/@req/@implements usage in the same comment
184
+ * @req REQ-SCOPED-IDS - Treat requirement IDs as scoped to the referenced story file
185
+ */
186
+ function parseImplementsLine(line) {
187
+ const parts = line.split(/\s+/);
188
+ const storyPath = parts[IMPLEMENTS_TOKENS.STORY_INDEX];
189
+ const reqIds = parts.slice(IMPLEMENTS_TOKENS.FIRST_REQ_INDEX);
190
+ if (!storyPath || reqIds.length === 0) {
191
+ return null;
192
+ }
193
+ return { storyPath, reqIds };
194
+ }
195
+ /**
196
+ * Validate an @implements annotation line against the referenced story content.
197
+ * Performs path validation, file reading, caching, and requirement existence checks
198
+ * for each requirement ID listed on the line.
199
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
200
+ * @req REQ-IMPLEMENTS-VALIDATE - Validate that all @implements requirement IDs exist
201
+ * @req REQ-MIXED-SUPPORT - Ensure @implements can coexist with @story/@req annotations
202
+ * @req REQ-SCOPED-IDS - Validate requirement IDs in the scope of their explicit story
203
+ */
204
+ function validateImplementsLine(opts) {
205
+ const { comment, context, line, cwd, reqCache } = opts;
206
+ const parsed = parseImplementsLine(line);
207
+ if (!parsed) {
208
+ return;
209
+ }
210
+ const { storyPath, reqIds } = parsed;
211
+ const { reqSet } = resolveStoryAndRequirements({
212
+ comment,
213
+ context,
214
+ storyPath,
215
+ cwd,
216
+ reqCache,
217
+ });
218
+ if (!reqSet) {
219
+ return;
220
+ }
221
+ for (const reqId of reqIds) {
222
+ checkRequirementExists({
223
+ comment,
224
+ context,
225
+ reqId,
226
+ storyPath,
227
+ reqSet,
228
+ });
229
+ }
230
+ }
167
231
  /**
168
232
  * Handle a single annotation line for story or requirement metadata.
169
233
  * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
170
234
  * @req REQ-DEEP-PARSE - Parse annotation lines for @story and @req tags
171
235
  * @req REQ-DEEP-MATCH - Dispatch @req lines for validation against story requirements
236
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
237
+ * @req REQ-IMPLEMENTS-VALIDATE - Dispatch @implements lines for validation
238
+ * @req REQ-MIXED-SUPPORT - Support mixed annotation types without interfering with each other
172
239
  */
173
240
  function handleAnnotationLine(opts) {
174
241
  const { line, comment, context, cwd, reqCache, storyPath } = opts;
@@ -180,6 +247,10 @@ function handleAnnotationLine(opts) {
180
247
  validateReqLine({ comment, context, line, storyPath, cwd, reqCache });
181
248
  return storyPath;
182
249
  }
250
+ else if (line.startsWith("@implements")) {
251
+ validateImplementsLine({ comment, context, line, cwd, reqCache });
252
+ return storyPath;
253
+ }
183
254
  return storyPath;
184
255
  }
185
256
  /**
@@ -1,6 +1,9 @@
1
1
  /**
2
- * Helper to determine whether a JSDoc or any nearby comments contain a @req annotation.
2
+ * Helper to determine whether a JSDoc or any nearby comments contain a requirement annotation.
3
+ * Treats both @req and @implements annotations as evidence of requirement coverage.
3
4
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
5
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
4
6
  * @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
7
+ * @req REQ-REQUIRE-ACCEPTS-IMPLEMENTS - Accept @implements as requirement coverage
5
8
  */
6
9
  export declare function hasReqAnnotation(jsdoc: any, comments: any[], context?: any, node?: any): boolean;
@@ -8,17 +8,24 @@ exports.hasReqAnnotation = hasReqAnnotation;
8
8
  */
9
9
  const require_story_io_1 = require("../rules/helpers/require-story-io");
10
10
  /**
11
- * Predicate helper to check whether a comment contains a @req annotation.
11
+ * Predicate helper to check whether a comment contains a requirement annotation.
12
+ * Treats both @req and @implements annotations as satisfying requirement presence checks.
12
13
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
14
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
13
15
  * @req REQ-ANNOTATION-REQ-DETECTION - Detect @req tag inside a comment
16
+ * @req REQ-REQUIRE-ACCEPTS-IMPLEMENTS - Accept @implements as requirement annotation
14
17
  */
15
18
  function commentContainsReq(c) {
16
- return c && typeof c.value === "string" && c.value.includes("@req");
19
+ return (c &&
20
+ typeof c.value === "string" &&
21
+ (c.value.includes("@req") || c.value.includes("@implements")));
17
22
  }
18
23
  /**
19
- * Line-based helper adapted from linesBeforeHasStory to detect @req.
24
+ * Line-based helper adapted from linesBeforeHasStory to detect requirement annotations.
25
+ * Lines containing either @req or @implements are treated as annotated.
20
26
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
21
27
  * @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in preceding source lines
28
+ * @req REQ-REQUIRE-ACCEPTS-IMPLEMENTS - Accept @implements in preceding source lines
22
29
  */
23
30
  function linesBeforeHasReq(sourceCode, node) {
24
31
  const lines = sourceCode && sourceCode.lines;
@@ -33,44 +40,53 @@ function linesBeforeHasReq(sourceCode, node) {
33
40
  }
34
41
  const from = Math.max(0, startLine - 1 - require_story_io_1.LOOKBACK_LINES);
35
42
  const to = Math.max(0, startLine - 1);
36
- // Scan each physical line in the configured lookback window for an @req marker.
43
+ // Scan each physical line in the configured lookback window for @req or @implements markers.
37
44
  // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
38
45
  // @req REQ-ANNOTATION-REQ-DETECTION - Search preceding lines for @req text
46
+ // @req REQ-REQUIRE-ACCEPTS-IMPLEMENTS - Search preceding lines for @implements text
39
47
  for (let i = from; i < to; i++) {
40
48
  const text = lines[i];
41
- // When a line contains @req we treat the function as already annotated.
49
+ // When a line contains @req or @implements we treat the function as already annotated.
42
50
  // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
43
51
  // @req REQ-ANNOTATION-REQ-DETECTION - Detect @req marker in raw source lines
44
- if (typeof text === "string" && text.includes("@req")) {
52
+ // @req REQ-REQUIRE-ACCEPTS-IMPLEMENTS - Detect @implements marker in raw source lines
53
+ if (typeof text === "string" &&
54
+ (text.includes("@req") || text.includes("@implements"))) {
45
55
  return true;
46
56
  }
47
57
  }
48
58
  return false;
49
59
  }
50
60
  /**
51
- * Parent-chain helper adapted from parentChainHasStory to detect @req.
61
+ * Parent-chain helper adapted from parentChainHasStory to detect requirement annotations.
62
+ * Accepts both @req and @implements in parent-chain comments as satisfying requirement presence.
52
63
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
53
64
  * @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in parent-chain comments
65
+ * @req REQ-REQUIRE-ACCEPTS-IMPLEMENTS - Accept @implements in parent-chain comments
54
66
  */
55
67
  function parentChainHasReq(sourceCode, node) {
56
68
  let p = node && node.parent;
57
69
  // Walk up the parent chain and inspect comments attached to each ancestor.
70
+ // Accept both @req and @implements markers when local comments are absent.
58
71
  // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
59
72
  // @req REQ-ANNOTATION-REQ-DETECTION - Traverse parent nodes when local comments are absent
73
+ // @req REQ-REQUIRE-ACCEPTS-IMPLEMENTS - Allow @implements to satisfy requirement on parents
60
74
  while (p) {
61
75
  const pComments = typeof sourceCode?.getCommentsBefore === "function"
62
76
  ? sourceCode.getCommentsBefore(p) || []
63
77
  : [];
64
- // Look for @req in comments immediately preceding each parent node.
78
+ // Look for @req or @implements in comments immediately preceding each parent node.
65
79
  // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
66
80
  // @req REQ-ANNOTATION-REQ-DETECTION - Detect @req markers in parent comments
81
+ // @req REQ-REQUIRE-ACCEPTS-IMPLEMENTS - Detect @implements markers in parent comments
67
82
  if (Array.isArray(pComments) && pComments.some(commentContainsReq)) {
68
83
  return true;
69
84
  }
70
85
  const pLeading = p.leadingComments || [];
71
- // Also inspect leadingComments attached directly to the parent node.
86
+ // Also inspect leadingComments attached directly to the parent node, accepting @req or @implements.
72
87
  // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
73
88
  // @req REQ-ANNOTATION-REQ-DETECTION - Detect @req markers in parent leadingComments
89
+ // @req REQ-REQUIRE-ACCEPTS-IMPLEMENTS - Detect @implements markers in parent leadingComments
74
90
  if (Array.isArray(pLeading) && pLeading.some(commentContainsReq)) {
75
91
  return true;
76
92
  }
@@ -79,9 +95,12 @@ function parentChainHasReq(sourceCode, node) {
79
95
  return false;
80
96
  }
81
97
  /**
82
- * Fallback text window helper adapted from fallbackTextBeforeHasStory to detect @req.
98
+ * Fallback text window helper adapted from fallbackTextBeforeHasStory to detect requirement annotations.
99
+ * Treats both @req and @implements in the fallback text window as requirement presence.
83
100
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
101
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
84
102
  * @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in fallback text window before node
103
+ * @req REQ-REQUIRE-ACCEPTS-IMPLEMENTS - Accept @implements in fallback text window before node
85
104
  */
86
105
  function fallbackTextBeforeHasReq(sourceCode, node) {
87
106
  // Guard against unsupported sourceCode or nodes without a usable range.
@@ -101,10 +120,13 @@ function fallbackTextBeforeHasReq(sourceCode, node) {
101
120
  try {
102
121
  const start = Math.max(0, range[0] - require_story_io_1.FALLBACK_WINDOW);
103
122
  const textBefore = sourceCode.getText().slice(start, range[0]);
104
- // Detect @req in the bounded text window immediately preceding the node.
123
+ // Detect @req or @implements in the bounded text window immediately preceding the node.
105
124
  // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
125
+ // @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
106
126
  // @req REQ-ANNOTATION-REQ-DETECTION - Detect @req marker in fallback text window
107
- if (typeof textBefore === "string" && textBefore.includes("@req")) {
127
+ // @req REQ-REQUIRE-ACCEPTS-IMPLEMENTS - Detect @implements marker in fallback text window
128
+ if (typeof textBefore === "string" &&
129
+ (textBefore.includes("@req") || textBefore.includes("@implements"))) {
108
130
  return true;
109
131
  }
110
132
  }
@@ -117,9 +139,12 @@ function fallbackTextBeforeHasReq(sourceCode, node) {
117
139
  return false;
118
140
  }
119
141
  /**
120
- * Helper to determine whether a JSDoc or any nearby comments contain a @req annotation.
142
+ * Helper to determine whether a JSDoc or any nearby comments contain a requirement annotation.
143
+ * Treats both @req and @implements annotations as evidence of requirement coverage.
121
144
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
145
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
122
146
  * @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
147
+ * @req REQ-REQUIRE-ACCEPTS-IMPLEMENTS - Accept @implements as requirement coverage
123
148
  */
124
149
  function hasReqAnnotation(jsdoc, comments, context, node) {
125
150
  try {
@@ -129,6 +154,7 @@ function hasReqAnnotation(jsdoc, comments, context, node) {
129
154
  // Prefer robust, location-based heuristics when sourceCode and node are available.
130
155
  // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
131
156
  // @req REQ-ANNOTATION-REQ-DETECTION - Use multiple heuristics to detect @req markers around the node
157
+ // @req REQ-REQUIRE-ACCEPTS-IMPLEMENTS - Use multiple heuristics to detect @implements markers around the node
132
158
  if (sourceCode && node) {
133
159
  if (linesBeforeHasReq(sourceCode, node) ||
134
160
  parentChainHasReq(sourceCode, node) ||
@@ -142,11 +168,13 @@ function hasReqAnnotation(jsdoc, comments, context, node) {
142
168
  // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
143
169
  // @req REQ-ANNOTATION-REQ-DETECTION - Fail gracefully when advanced detection heuristics throw
144
170
  }
145
- // BRANCH @req detection on JSDoc or comments
171
+ // BRANCH requirement detection on JSDoc or comments, accepting both @req and @implements.
146
172
  // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
173
+ // @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
147
174
  // @req REQ-ANNOTATION-REQ-DETECTION
175
+ // @req REQ-REQUIRE-ACCEPTS-IMPLEMENTS
148
176
  return ((jsdoc &&
149
177
  typeof jsdoc.value === "string" &&
150
- jsdoc.value.includes("@req")) ||
178
+ (jsdoc.value.includes("@req") || jsdoc.value.includes("@implements"))) ||
151
179
  comments.some(commentContainsReq));
152
180
  }
@@ -145,6 +145,31 @@ describe("Maintenance CLI (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
145
145
  fs_1.default.rmSync(dir, { recursive: true, force: true });
146
146
  }
147
147
  });
148
+ it("[REQ-MAINT-SAFE] report exits 2 and prints error on invalid --format value", () => {
149
+ const dir = withTempDir();
150
+ process.chdir(dir);
151
+ const errorSpy = jest.spyOn(console, "error").mockImplementation(() => { });
152
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
153
+ const code = (0, cli_1.runMaintenanceCli)([
154
+ "node",
155
+ "traceability-maint",
156
+ "report",
157
+ "--format",
158
+ "yaml",
159
+ ]);
160
+ try {
161
+ expect(code).toBe(2);
162
+ expect(errorSpy).toHaveBeenCalledTimes(1);
163
+ const message = String(errorSpy.mock.calls[0][0]);
164
+ expect(message).toContain("Invalid format: yaml");
165
+ expect(message).toContain("Expected 'text' or 'json'");
166
+ }
167
+ finally {
168
+ errorSpy.mockRestore();
169
+ logSpy.mockRestore();
170
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
171
+ }
172
+ });
148
173
  it("[REQ-MAINT-DETECT] detect supports --json output", () => {
149
174
  const dir = withTempDir();
150
175
  process.chdir(dir);
@@ -169,4 +194,68 @@ describe("Maintenance CLI (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
169
194
  fs_1.default.rmSync(dir, { recursive: true, force: true });
170
195
  }
171
196
  });
197
+ it("[REQ-MAINT-DETECT] detect with non-existent --root exits 0 and reports no stale annotations", () => {
198
+ const dir = withTempDir();
199
+ process.chdir(dir);
200
+ const missingRoot = path_1.default.join(dir, "missing-root");
201
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
202
+ const code = (0, cli_1.runMaintenanceCli)([
203
+ "node",
204
+ "traceability-maint",
205
+ "detect",
206
+ "--root",
207
+ missingRoot,
208
+ ]);
209
+ try {
210
+ expect(code).toBe(0);
211
+ expect(logSpy).toHaveBeenCalledWith("No stale @story annotations found.");
212
+ }
213
+ finally {
214
+ logSpy.mockRestore();
215
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
216
+ }
217
+ });
218
+ it("[REQ-MAINT-SAFE] prints help and exits 0 when no subcommand is provided", () => {
219
+ const dir = withTempDir();
220
+ process.chdir(dir);
221
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
222
+ const errorSpy = jest.spyOn(console, "error").mockImplementation(() => { });
223
+ const code = (0, cli_1.runMaintenanceCli)(["node", "traceability-maint"]);
224
+ try {
225
+ expect(code).toBe(0);
226
+ expect(logSpy).toHaveBeenCalled();
227
+ const allMessages = logSpy.mock.calls.flat().join("\n");
228
+ expect(allMessages).toContain("traceability-maint - Traceability annotation maintenance tools");
229
+ expect(errorSpy).not.toHaveBeenCalled();
230
+ }
231
+ finally {
232
+ logSpy.mockRestore();
233
+ errorSpy.mockRestore();
234
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
235
+ }
236
+ });
237
+ it("[REQ-MAINT-SAFE] detect catches filesystem permission errors and exits 2 with prefixed error message", () => {
238
+ const dir = withTempDir();
239
+ process.chdir(dir);
240
+ const errorSpy = jest.spyOn(console, "error").mockImplementation(() => { });
241
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
242
+ const statSpy = jest.spyOn(fs_1.default, "statSync").mockImplementation(() => {
243
+ const err = new Error("EACCES simulated");
244
+ err.code = "EACCES";
245
+ throw err;
246
+ });
247
+ const code = (0, cli_1.runMaintenanceCli)(["node", "traceability-maint", "detect"]);
248
+ try {
249
+ expect(code).toBe(2);
250
+ expect(errorSpy).toHaveBeenCalled();
251
+ const message = String(errorSpy.mock.calls[0][0]);
252
+ expect(message).toContain("traceability-maint failed:");
253
+ }
254
+ finally {
255
+ statSpy.mockRestore();
256
+ errorSpy.mockRestore();
257
+ logSpy.mockRestore();
258
+ fs_1.default.rmSync(dir, { recursive: true, force: true });
259
+ }
260
+ });
172
261
  });
@@ -55,6 +55,7 @@ describe("Plugin Default Export and Configs (Story 001.0-DEV-PLUGIN-SETUP)", ()
55
55
  "valid-annotation-format",
56
56
  "valid-story-reference",
57
57
  "valid-req-reference",
58
+ "prefer-implements-annotation",
58
59
  ];
59
60
  // Act: get actual rule names from plugin
60
61
  const actual = Object.keys(index_1.rules);
@@ -79,10 +80,12 @@ describe("Plugin Default Export and Configs (Story 001.0-DEV-PLUGIN-SETUP)", ()
79
80
  expect(recommendedRules).toHaveProperty("traceability/require-branch-annotation", "error");
80
81
  expect(recommendedRules).toHaveProperty("traceability/valid-story-reference", "error");
81
82
  expect(recommendedRules).toHaveProperty("traceability/valid-req-reference", "error");
83
+ expect(recommendedRules).toHaveProperty("traceability/prefer-implements-annotation", "warn");
82
84
  });
83
85
  it("[REQ-ERROR-SEVERITY] configs.strict uses same severity mapping as recommended", () => {
84
86
  const strictRules = index_1.configs.strict[0].rules;
85
87
  const recommendedRules = index_1.configs.recommended[0].rules;
86
88
  expect(strictRules).toEqual(recommendedRules);
89
+ expect(strictRules).toHaveProperty("traceability/prefer-implements-annotation", "warn");
87
90
  });
88
91
  });
@@ -0,0 +1,84 @@
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/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
8
+ * @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
9
+ * @req REQ-OPTIONAL-WARNING - Verify rule emits recommendations for legacy @story/@req usage
10
+ * @req REQ-MULTI-STORY-DETECT - Verify rule detects multi-story and mixed-annotation patterns
11
+ * @req REQ-CONFIG-SEVERITY - Verify rule is disabled by default and can be enabled as warn/error
12
+ */
13
+ const eslint_1 = require("eslint");
14
+ const prefer_implements_annotation_1 = __importDefault(require("../../src/rules/prefer-implements-annotation"));
15
+ const ruleTester = new eslint_1.RuleTester({
16
+ languageOptions: {
17
+ parserOptions: { ecmaVersion: 2020, sourceType: "module" },
18
+ },
19
+ });
20
+ describe("prefer-implements-annotation rule (Story 010.3-DEV-MIGRATE-TO-IMPLEMENTS)", () => {
21
+ ruleTester.run("prefer-implements-annotation", prefer_implements_annotation_1.default, {
22
+ valid: [
23
+ {
24
+ name: "[REQ-BACKWARD-COMP-VALIDATION] comment with only @story is ignored",
25
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n */\nfunction onlyStory() {}`,
26
+ },
27
+ {
28
+ name: "[REQ-BACKWARD-COMP-VALIDATION] comment with only @req is ignored",
29
+ code: `/**\n * @req REQ-ONLY\n */\nfunction onlyReq() {}`,
30
+ },
31
+ {
32
+ name: "[REQ-BACKWARD-COMP-VALIDATION] comment with @implements only is ignored",
33
+ code: `/**\n * @implements docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction alreadyImplements() {}`,
34
+ },
35
+ ],
36
+ invalid: [
37
+ {
38
+ name: "[REQ-OPTIONAL-WARNING] single-story @story + @req block triggers preferImplements message",
39
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction legacy() {}`,
40
+ output: `/**\n * @implements docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction legacy() {}`,
41
+ errors: [{ messageId: "preferImplements" }],
42
+ },
43
+ {
44
+ name: "[REQ-MULTI-STORY-DETECT] mixed @story/@req and @implements triggers cannotAutoFix",
45
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n * @implements docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction mixed() {}`,
46
+ errors: [
47
+ {
48
+ messageId: "cannotAutoFix",
49
+ data: {
50
+ reason: "comment mixes @story/@req with existing @implements annotations",
51
+ },
52
+ },
53
+ ],
54
+ },
55
+ {
56
+ name: "[REQ-MULTI-STORY-DETECT] multiple @story paths in same block trigger multiStoryDetected",
57
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md\n * @req REQ-BRANCH-DETECTION\n */\nfunction multiStory() {}`,
58
+ errors: [{ messageId: "multiStoryDetected" }],
59
+ },
60
+ {
61
+ name: "[REQ-AUTO-FIX] single @story + single @req auto-fixes to single @implements line",
62
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction autoFixSingleReq() {}`,
63
+ output: `/**\n * @implements docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction autoFixSingleReq() {}`,
64
+ errors: [{ messageId: "preferImplements" }],
65
+ },
66
+ {
67
+ name: "[REQ-SINGLE-STORY-FIX] single @story with multiple @req lines auto-fixes to single @implements line containing all REQ IDs",
68
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ONE\n * @req REQ-TWO\n * @req REQ-THREE\n */\nfunction autoFixMultiReq() {}`,
69
+ output: `/**\n * @implements docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ONE REQ-TWO REQ-THREE\n */\nfunction autoFixMultiReq() {}`,
70
+ errors: [{ messageId: "preferImplements" }],
71
+ },
72
+ {
73
+ name: "[REQ-AUTO-FIX] complex @req content (extra description) does not auto-fix but still warns",
74
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED must handle extra description\n */\nfunction complexReqNoAutoFix() {}`,
75
+ errors: [{ messageId: "preferImplements" }],
76
+ },
77
+ {
78
+ name: "[REQ-AUTO-FIX] complex @story content (extra description) does not auto-fix but still warns",
79
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md additional descriptive text\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction complexStoryNoAutoFix() {}`,
80
+ errors: [{ messageId: "preferImplements" }],
81
+ },
82
+ ],
83
+ });
84
+ });
@@ -12,6 +12,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  *
13
13
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
14
14
  * @req REQ-TYPESCRIPT-SUPPORT - Verify TypeScript declarations are checked via shared annotation checker helper
15
+ *
16
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
17
+ * @req REQ-REQUIRE-ACCEPTS-IMPLEMENTS - Verify @implements is accepted as satisfying requirement annotations
15
18
  */
16
19
  const eslint_1 = require("eslint");
17
20
  const require_req_annotation_1 = __importDefault(require("../../src/rules/require-req-annotation"));
@@ -73,6 +76,10 @@ describe("Require Req Annotation Rule (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", (
73
76
  name: "[REQ-ANNOTATION-REQUIRED] valid with only @req annotation",
74
77
  code: `/**\n * @req REQ-EXAMPLE\n */\nfunction foo() {}`,
75
78
  },
79
+ {
80
+ name: "[REQ-REQUIRE-ACCEPTS-IMPLEMENTS] valid with only @implements annotation",
81
+ code: `/**\n * @implements docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction implOnly() {}`,
82
+ },
76
83
  {
77
84
  name: "[REQ-ANNOTATION-REQUIRED] valid with @story and @req annotations",
78
85
  code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-EXAMPLE\n */\nfunction bar() {}`,
@@ -131,7 +138,7 @@ describe("Require Req Annotation Rule (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", (
131
138
  ],
132
139
  invalid: [
133
140
  {
134
- name: "[REQ-ANNOTATION-REQUIRED] missing @req on function without JSDoc",
141
+ name: "[REQ-ANNOTATION-REQUIRED][REQ-REQUIRE-ACCEPTS-IMPLEMENTS] missing @req on function without JSDoc remains invalid under multi-story support",
135
142
  code: `function baz() {}`,
136
143
  errors: [
137
144
  {
@@ -6,15 +6,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  /**
7
7
  * Tests for: docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
8
8
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
9
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
9
10
  * @req REQ-ANNOTATION-REQUIRED - Verify require-story-annotation rule enforces @story annotation on functions
11
+ * @req REQ-REQUIRE-ACCEPTS-IMPLEMENTS - Verify @implements annotation is accepted as satisfying story requirements
10
12
  */
11
13
  const eslint_1 = require("eslint");
12
14
  const require_story_annotation_1 = __importDefault(require("../../src/rules/require-story-annotation"));
13
15
  const ts_language_options_1 = require("../utils/ts-language-options");
14
16
  const ruleTester = new eslint_1.RuleTester({
15
- languageOptions: {
16
- parserOptions: { ecmaVersion: 2020, sourceType: "module" },
17
- },
17
+ languageOptions: ts_language_options_1.tsRuleTesterLanguageOptions,
18
18
  });
19
19
  describe("Require Story Annotation Rule (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", () => {
20
20
  ruleTester.run("require-story-annotation", require_story_annotation_1.default, {
@@ -23,6 +23,10 @@ describe("Require Story Annotation Rule (Story 003.0-DEV-FUNCTION-ANNOTATIONS)",
23
23
  name: "[REQ-ANNOTATION-REQUIRED] valid with JSDoc @story annotation",
24
24
  code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n */\nfunction foo() {}`,
25
25
  },
26
+ {
27
+ name: "[REQ-REQUIRE-ACCEPTS-IMPLEMENTS] valid with only @implements annotation",
28
+ code: `/**\n * @implements docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction implOnly() {}`,
29
+ },
26
30
  {
27
31
  name: "[REQ-ANNOTATION-REQUIRED] valid with line comment @story annotation",
28
32
  code: `// @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
@@ -60,7 +64,8 @@ declare function tsDecl(): void;`,
60
64
  ],
61
65
  invalid: [
62
66
  {
63
- name: "[REQ-ANNOTATION-REQUIRED] missing @story annotation on function",
67
+ // Backward compatibility: plain unannotated functions remain invalid under multi-story support
68
+ name: "[REQ-ANNOTATION-REQUIRED][BACKCOMPAT] missing @story annotation on function with no @implements",
64
69
  code: `function bar() {}`,
65
70
  output: `/** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */\nfunction bar() {}`,
66
71
  errors: [
@@ -18,6 +18,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
18
18
  * @req REQ-CONFIGURABLE-PATTERNS-REQ - Rule supports configurable requirement ID regex patterns
19
19
  * @req REQ-CONFIGURABLE-PATTERNS-EXAMPLES - Rule supports configurable example strings in error messages
20
20
  * @req REQ-CONFIGURABLE-PATTERNS-FALLBACK - Invalid regex patterns fall back to default behavior without crashing
21
+ * Tests for: docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
22
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
23
+ * @req REQ-IMPLEMENTS-PARSE - Rule parses @implements annotations with story and requirement references
24
+ * @req REQ-FORMAT-VALIDATION - Rule validates story and requirement formats inside @implements annotations
25
+ * @req REQ-MIXED-SUPPORT - Rule supports mixed @story/@req/@implements usage in the same comment
21
26
  */
22
27
  const eslint_1 = require("eslint");
23
28
  const valid_annotation_format_1 = __importDefault(require("../../src/rules/valid-annotation-format"));
@@ -168,6 +173,27 @@ describe("Valid Annotation Format Rule (Story 005.0-DEV-ANNOTATION-VALIDATION)",
168
173
  },
169
174
  ],
170
175
  },
176
+ {
177
+ name: "[REQ-IMPLEMENTS-PARSE] valid single @implements with one story and one requirement (default patterns)",
178
+ code: `/**
179
+ * @implements docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md REQ-IMPLEMENTS-PARSE
180
+ */`,
181
+ },
182
+ {
183
+ name: "[REQ-IMPLEMENTS-PARSE] valid multiple @implements lines with different stories and requirements",
184
+ code: `/**
185
+ * @implements docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md REQ-IMPLEMENTS-PARSE REQ-FORMAT-VALIDATION
186
+ * @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-FORMAT-SPECIFICATION
187
+ */`,
188
+ },
189
+ {
190
+ name: "[REQ-MIXED-SUPPORT] valid mixed @story/@req/@implements usage in same block comment",
191
+ code: `/**
192
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
193
+ * @req REQ-MIXED-SUPPORT
194
+ * @implements docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md REQ-IMPLEMENTS-PARSE REQ-FORMAT-VALIDATION REQ-MIXED-SUPPORT
195
+ */`,
196
+ },
171
197
  ],
172
198
  invalid: [
173
199
  makeInvalidStory({
@@ -480,6 +506,58 @@ describe("Valid Annotation Format Rule (Story 005.0-DEV-ANNOTATION-VALIDATION)",
480
506
  },
481
507
  ],
482
508
  },
509
+ makeInvalid({
510
+ name: "[REQ-IMPLEMENTS-PARSE] @implements with no value is invalid",
511
+ code: `/**
512
+ * @implements
513
+ */`,
514
+ messageId: "invalidImplementsFormat",
515
+ details: 'Missing story path and requirement IDs for @implements annotation. Expected a value like "docs/stories/005.0-DEV-EXAMPLE.story.md REQ-EXAMPLE".',
516
+ }),
517
+ makeInvalid({
518
+ name: "[REQ-IMPLEMENTS-PARSE] @implements with only story path and no requirement IDs is invalid",
519
+ code: `/**
520
+ * @implements docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
521
+ */`,
522
+ messageId: "invalidImplementsFormat",
523
+ details: 'Missing requirement IDs for @implements annotation. Expected a value like "docs/stories/005.0-DEV-EXAMPLE.story.md REQ-EXAMPLE".',
524
+ }),
525
+ makeInvalid({
526
+ name: "[REQ-FORMAT-VALIDATION] @implements with invalid story path format",
527
+ code: `/**
528
+ * @implements invalid/path.txt REQ-IMPLEMENTS-PARSE
529
+ */`,
530
+ messageId: "invalidImplementsFormat",
531
+ details: 'Invalid story path "invalid/path.txt" for @implements annotation. Expected a path like "docs/stories/005.0-DEV-EXAMPLE.story.md".',
532
+ }),
533
+ {
534
+ name: "[REQ-FORMAT-VALIDATION] @implements with invalid requirement ID format",
535
+ code: `/**
536
+ * @implements docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md REQ-VALID invalid-format
537
+ */`,
538
+ errors: [
539
+ {
540
+ messageId: "invalidReqFormat",
541
+ data: {
542
+ details: 'Invalid requirement ID "invalid-format" for @req annotation. Expected an identifier like "REQ-EXAMPLE" (uppercase letters, numbers, and dashes only).',
543
+ },
544
+ },
545
+ ],
546
+ },
547
+ {
548
+ name: "[REQ-FORMAT-VALIDATION] @implements with multiple requirement IDs where one is invalid",
549
+ code: `/**
550
+ * @implements docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md REQ-VALID-1 REQ-VALID-2 bad-id
551
+ */`,
552
+ errors: [
553
+ {
554
+ messageId: "invalidReqFormat",
555
+ data: {
556
+ details: 'Invalid requirement ID "bad-id" for @req annotation. Expected an identifier like "REQ-EXAMPLE" (uppercase letters, numbers, and dashes only).',
557
+ },
558
+ },
559
+ ],
560
+ },
483
561
  ],
484
562
  });
485
563
  });