eslint-plugin-traceability 1.17.0 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/lib/src/index.js +53 -33
  3. package/lib/src/maintenance/commands.d.ts +4 -0
  4. package/lib/src/maintenance/commands.js +4 -0
  5. package/lib/src/maintenance/index.d.ts +1 -0
  6. package/lib/src/maintenance/index.js +1 -0
  7. package/lib/src/maintenance/report.js +2 -2
  8. package/lib/src/maintenance/update.js +4 -2
  9. package/lib/src/rules/helpers/test-callback-exclusion.d.ts +5 -1
  10. package/lib/src/rules/helpers/test-callback-exclusion.js +2 -11
  11. package/lib/src/rules/helpers/valid-annotation-format-validators.js +8 -2
  12. package/lib/src/rules/no-redundant-annotation.js +4 -0
  13. package/lib/src/rules/prefer-implements-annotation.js +25 -20
  14. package/lib/src/rules/valid-annotation-format.js +62 -42
  15. package/lib/tests/integration/no-redundant-annotation.integration.test.js +31 -0
  16. package/lib/tests/integration/require-traceability-test-callbacks.integration.test.d.ts +1 -0
  17. package/lib/tests/integration/require-traceability-test-callbacks.integration.test.js +148 -0
  18. package/lib/tests/maintenance/detect-isolated.test.js +22 -14
  19. package/lib/tests/perf/maintenance-cli-large-workspace.test.js +145 -64
  20. package/lib/tests/perf/maintenance-large-workspace.test.js +65 -46
  21. package/lib/tests/rules/no-redundant-annotation.test.js +5 -0
  22. package/lib/tests/utils/{annotation-checker-branches.test.d.ts → annotation-checker-autofix-behavior.test.d.ts} +1 -1
  23. package/lib/tests/utils/{annotation-checker-branches.test.js → annotation-checker-autofix-behavior.test.js} +2 -2
  24. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,9 +1,9 @@
1
- # [1.17.0](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.16.1...v1.17.0) (2025-12-09)
1
+ ## [1.17.1](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.17.0...v1.17.1) (2025-12-10)
2
2
 
3
3
 
4
- ### Features
4
+ ### Bug Fixes
5
5
 
6
- * allow configuring additional excluded test helper callbacks ([d266197](https://github.com/voder-ai/eslint-plugin-traceability/commit/d26619721b1826fe97d01a63647f07505e35c846))
6
+ * avoid redundant-annotation false positives for catch blocks ([2ac69e2](https://github.com/voder-ai/eslint-plugin-traceability/commit/2ac69e2a03b54cf29bf2bc175771bf3b23aba6e9))
7
7
 
8
8
  # Changelog
9
9
 
package/lib/src/index.js CHANGED
@@ -86,61 +86,77 @@ RULE_NAMES.forEach(
86
86
  * and diagnostics).
87
87
  *
88
88
  * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED REQ-CONFIGURABLE-SCOPE REQ-EXPORT-PRIORITY
89
+ * @supports docs/stories/010.4-DEV-UNIFIED-FUNCTION-RULE-AND-ALIASES.story.md REQ-UNIFIED-ALIAS-ENGINE
89
90
  */
90
- {
91
+ function createAliasRuleMeta(unifiedRule, legacyRule) {
92
+ if (!legacyRule) {
93
+ return null;
94
+ }
95
+ const baseMeta = (unifiedRule.meta ?? {});
96
+ const legacyMeta = (legacyRule.meta ?? {});
97
+ return {
98
+ ...baseMeta,
99
+ ...legacyMeta,
100
+ docs: {
101
+ ...(baseMeta.docs ?? {}),
102
+ ...(legacyMeta.docs ?? {}),
103
+ },
104
+ messages: {
105
+ ...(baseMeta.messages ?? {}),
106
+ ...(legacyMeta.messages ?? {}),
107
+ },
108
+ schema: legacyMeta.schema ??
109
+ baseMeta.schema ??
110
+ [],
111
+ hasSuggestions: legacyMeta.hasSuggestions ??
112
+ baseMeta.hasSuggestions,
113
+ fixable: legacyMeta.fixable ??
114
+ baseMeta.fixable,
115
+ deprecated: legacyMeta.deprecated ??
116
+ baseMeta.deprecated,
117
+ replacedBy: legacyMeta.replacedBy ??
118
+ baseMeta.replacedBy,
119
+ type: legacyMeta.type ??
120
+ baseMeta.type ??
121
+ "problem",
122
+ };
123
+ }
124
+ /**
125
+ * Wire up the unified `require-traceability` rule and its legacy alias rules
126
+ * so that they share the same implementation while preserving legacy metadata.
127
+ *
128
+ * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED REQ-EXPORT-PRIORITY
129
+ * @supports docs/stories/010.4-DEV-UNIFIED-FUNCTION-RULE-AND-ALIASES.story.md REQ-UNIFIED-ALIAS-ENGINE
130
+ */
131
+ function wireUnifiedFunctionAnnotationAliases() {
91
132
  const unifiedRule = rules["require-traceability"];
92
133
  const legacyStoryRule = rules["require-story-annotation"];
93
134
  const legacyReqRule = rules["require-req-annotation"];
94
135
  if (unifiedRule) {
95
136
  const createAliasRule = (legacyRule) => {
96
- if (!legacyRule) {
137
+ const mergedMeta = createAliasRuleMeta(unifiedRule, legacyRule);
138
+ if (!mergedMeta) {
97
139
  return unifiedRule;
98
140
  }
99
- const baseMeta = (unifiedRule.meta ?? {});
100
- const legacyMeta = (legacyRule.meta ?? {});
101
- const mergedMeta = {
102
- ...baseMeta,
103
- ...legacyMeta,
104
- docs: {
105
- ...(baseMeta.docs ?? {}),
106
- ...(legacyMeta.docs ?? {}),
107
- },
108
- messages: {
109
- ...(baseMeta.messages ?? {}),
110
- ...(legacyMeta.messages ?? {}),
111
- },
112
- schema: legacyMeta.schema ??
113
- baseMeta.schema ??
114
- [],
115
- hasSuggestions: legacyMeta.hasSuggestions ??
116
- baseMeta.hasSuggestions,
117
- fixable: legacyMeta.fixable ??
118
- baseMeta.fixable,
119
- deprecated: legacyMeta.deprecated ??
120
- baseMeta.deprecated,
121
- replacedBy: legacyMeta.replacedBy ??
122
- baseMeta.replacedBy,
123
- type: legacyMeta.type ??
124
- baseMeta.type ??
125
- "problem",
126
- };
127
- const aliasRule = {
141
+ return {
128
142
  ...unifiedRule,
129
143
  meta: mergedMeta,
130
144
  create: unifiedRule.create,
131
145
  };
132
- return aliasRule;
133
146
  };
134
147
  rules["require-story-annotation"] = createAliasRule(legacyStoryRule);
135
148
  rules["require-req-annotation"] = createAliasRule(legacyReqRule);
136
149
  }
137
150
  }
151
+ wireUnifiedFunctionAnnotationAliases();
138
152
  /**
139
153
  * @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-RULE-NAME
140
154
  * Wire up traceability/prefer-supports-annotation as the primary rule name and
141
155
  * traceability/prefer-implements-annotation as its deprecated alias.
156
+ *
157
+ * @supports docs/stories/010.4-DEV-UNIFIED-FUNCTION-RULE-AND-ALIASES.story.md REQ-MIGRATION-RULE-NAMING
142
158
  */
143
- {
159
+ function wirePreferSupportsAlias() {
144
160
  const implementsRule = rules["prefer-implements-annotation"];
145
161
  if (implementsRule) {
146
162
  const originalMeta = implementsRule.meta ?? {};
@@ -163,6 +179,7 @@ RULE_NAMES.forEach(
163
179
  }
164
180
  }
165
181
  }
182
+ wirePreferSupportsAlias();
166
183
  /**
167
184
  * Plugin metadata used by ESLint for debugging and caching.
168
185
  *
@@ -226,6 +243,9 @@ const TRACEABILITY_RULE_SEVERITIES = {
226
243
  * @req REQ-ERROR-SEVERITY - Map rule types to appropriate ESLint severity levels (errors vs warnings)
227
244
  * @story docs/stories/002.0-DEV-ESLINT-CONFIG.story.md
228
245
  * @req REQ-CONFIG-PRESETS - Provide flat-config presets that self-register the plugin and core rules
246
+ *
247
+ * @supports docs/stories/007.0-DEV-ERROR-REPORTING.story.md REQ-ERROR-SEVERITY
248
+ * @supports docs/stories/002.0-DEV-ESLINT-CONFIG.story.md REQ-CONFIG-PRESETS
229
249
  */
230
250
  function createTraceabilityFlatConfig() {
231
251
  return {
@@ -7,6 +7,7 @@ export declare const EXIT_USAGE = 2;
7
7
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
8
8
  * @req REQ-MAINT-DETECT - CLI surface for detection of stale annotations
9
9
  * @req REQ-MAINT-SAFE - Return specific exit codes for stale vs clean states
10
+ * @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-DETECT REQ-MAINT-SAFE
10
11
  */
11
12
  export declare function handleDetect(normalized: NormalizedCliArgs): number;
12
13
  /**
@@ -14,6 +15,7 @@ export declare function handleDetect(normalized: NormalizedCliArgs): number;
14
15
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
15
16
  * @req REQ-MAINT-VERIFY - CLI surface for verification of annotations
16
17
  * @req REQ-MAINT-SAFE - Return distinct exit codes for verification failures
18
+ * @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-VERIFY REQ-MAINT-SAFE
17
19
  */
18
20
  export declare function handleVerify(normalized: NormalizedCliArgs): number;
19
21
  /**
@@ -21,6 +23,7 @@ export declare function handleVerify(normalized: NormalizedCliArgs): number;
21
23
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
22
24
  * @req REQ-MAINT-REPORT - CLI surface for human-readable maintenance reports
23
25
  * @req REQ-MAINT-SAFE - Support machine-readable formats for safe automation
26
+ * @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-REPORT REQ-MAINT-SAFE
24
27
  */
25
28
  export declare function handleReport(normalized: NormalizedCliArgs): number;
26
29
  /**
@@ -28,5 +31,6 @@ export declare function handleReport(normalized: NormalizedCliArgs): number;
28
31
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
29
32
  * @req REQ-MAINT-UPDATE - CLI surface for updating annotation references
30
33
  * @req REQ-MAINT-SAFE - Provide dry-run mode and explicit parameter checks
34
+ * @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-UPDATE REQ-MAINT-SAFE
31
35
  */
32
36
  export declare function handleUpdate(normalized: NormalizedCliArgs): number;
@@ -28,6 +28,7 @@ exports.EXIT_USAGE = 2;
28
28
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
29
29
  * @req REQ-MAINT-DETECT - CLI surface for detection of stale annotations
30
30
  * @req REQ-MAINT-SAFE - Return specific exit codes for stale vs clean states
31
+ * @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-DETECT REQ-MAINT-SAFE
31
32
  */
32
33
  function handleDetect(normalized) {
33
34
  const flags = (0, flags_1.parseFlags)(normalized);
@@ -54,6 +55,7 @@ Run 'traceability-maint report' for a structured summary.`);
54
55
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
55
56
  * @req REQ-MAINT-VERIFY - CLI surface for verification of annotations
56
57
  * @req REQ-MAINT-SAFE - Return distinct exit codes for verification failures
58
+ * @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-VERIFY REQ-MAINT-SAFE
57
59
  */
58
60
  function handleVerify(normalized) {
59
61
  const flags = (0, flags_1.parseFlags)(normalized);
@@ -71,6 +73,7 @@ function handleVerify(normalized) {
71
73
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
72
74
  * @req REQ-MAINT-REPORT - CLI surface for human-readable maintenance reports
73
75
  * @req REQ-MAINT-SAFE - Support machine-readable formats for safe automation
76
+ * @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-REPORT REQ-MAINT-SAFE
74
77
  */
75
78
  function handleReport(normalized) {
76
79
  const flags = (0, flags_1.parseFlags)(normalized);
@@ -96,6 +99,7 @@ function handleReport(normalized) {
96
99
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
97
100
  * @req REQ-MAINT-UPDATE - CLI surface for updating annotation references
98
101
  * @req REQ-MAINT-SAFE - Provide dry-run mode and explicit parameter checks
102
+ * @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-UPDATE REQ-MAINT-SAFE
99
103
  */
100
104
  function handleUpdate(normalized) {
101
105
  const flags = (0, flags_1.parseFlags)(normalized);
@@ -7,6 +7,7 @@
7
7
  * @req REQ-MAINT-VERIFY
8
8
  * @req REQ-MAINT-REPORT
9
9
  * @req REQ-MAINT-SAFE
10
+ * @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-DETECT REQ-MAINT-UPDATE REQ-MAINT-BATCH REQ-MAINT-VERIFY REQ-MAINT-REPORT REQ-MAINT-SAFE
10
11
  */
11
12
  export { detectStaleAnnotations } from "./detect";
12
13
  export { updateAnnotationReferences } from "./update";
@@ -10,6 +10,7 @@ exports.generateMaintenanceReport = exports.verifyAnnotations = exports.batchUpd
10
10
  * @req REQ-MAINT-VERIFY
11
11
  * @req REQ-MAINT-REPORT
12
12
  * @req REQ-MAINT-SAFE
13
+ * @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-DETECT REQ-MAINT-UPDATE REQ-MAINT-BATCH REQ-MAINT-VERIFY REQ-MAINT-REPORT REQ-MAINT-SAFE
13
14
  */
14
15
  var detect_1 = require("./detect");
15
16
  Object.defineProperty(exports, "detectStaleAnnotations", { enumerable: true, get: function () { return detect_1.detectStaleAnnotations; } });
@@ -12,8 +12,8 @@ const detect_1 = require("./detect");
12
12
  */
13
13
  function generateMaintenanceReport(codebasePath) {
14
14
  const staleAnnotations = (0, detect_1.detectStaleAnnotations)(codebasePath);
15
- // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md // @req REQ-MAINT-SAFE - When no stale annotations are found, return empty string to indicate no actions required
16
- // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md // @req REQ-MAINT-REPORT - When stale annotations exist, produce a newline-separated report
15
+ // @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-SAFE - When no stale annotations are found, return empty string to indicate no actions required
16
+ // @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-REPORT - When stale annotations exist, produce a newline-separated report
17
17
  if (staleAnnotations.length === 0) {
18
18
  return "";
19
19
  }
@@ -40,6 +40,7 @@ const utils_1 = require("./utils");
40
40
  * Helper to process a single file for annotation reference updates
41
41
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
42
42
  * @req REQ-MAINT-UPDATE
43
+ * @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-UPDATE
43
44
  */
44
45
  function processFileForAnnotationUpdates(fullPath, regex, newPath, replacementCountRef) {
45
46
  const content = fs.readFileSync(fullPath, "utf8"); // getAllFiles already returns regular files
@@ -78,8 +79,7 @@ function updateAnnotationReferences(codebasePath, oldPath, newPath) {
78
79
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
79
80
  * @req REQ-MAINT-UPDATE
80
81
  */
81
- // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
82
- // @req REQ-MAINT-UPDATE
82
+ // @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-UPDATE
83
83
  if (!fs.existsSync(codebasePath) ||
84
84
  !fs.statSync(codebasePath).isDirectory()) {
85
85
  return 0;
@@ -92,11 +92,13 @@ function updateAnnotationReferences(codebasePath, oldPath, newPath) {
92
92
  * Iterate over all files and replace annotation references
93
93
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
94
94
  * @req REQ-MAINT-UPDATE
95
+ * @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-UPDATE
95
96
  */
96
97
  /**
97
98
  * Loop over each discovered file path
98
99
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
99
100
  * @req REQ-MAINT-UPDATE
101
+ * @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-UPDATE
100
102
  */
101
103
  for (const fullPath of files) {
102
104
  processFileForAnnotationUpdates(fullPath, regex, newPath, replacementCountRef);
@@ -8,6 +8,7 @@
8
8
  * @story docs/stories/013-exclude-test-framework-callbacks.proposed.md
9
9
  * @req REQ-TEST-CALLBACK-EXCLUSION - Provide reusable test callback exclusion logic
10
10
  */
11
+ import type { TSESTree } from "@typescript-eslint/utils";
11
12
  /**
12
13
  * Options controlling how test callbacks are treated by the helpers.
13
14
  *
@@ -21,6 +22,9 @@ interface CallbackExclusionOptions {
21
22
  excludeTestCallbacks?: boolean;
22
23
  additionalTestHelperNames?: string[];
23
24
  }
25
+ type TraceabilityNodeWithParent = TSESTree.Node & {
26
+ parent?: TraceabilityNodeWithParent | null;
27
+ };
24
28
  /**
25
29
  * Determine whether a node represents a callback passed to a known test
26
30
  * framework function (Jest, Mocha, Vitest, etc).
@@ -34,6 +38,6 @@ interface CallbackExclusionOptions {
34
38
  *
35
39
  * @req REQ-TEST-CALLBACK-EXCLUSION
36
40
  */
37
- declare function isTestFrameworkCallback(node: any, options?: CallbackExclusionOptions): boolean;
41
+ declare function isTestFrameworkCallback(node: TraceabilityNodeWithParent | null | undefined, options?: CallbackExclusionOptions): boolean;
38
42
  export type { CallbackExclusionOptions };
39
43
  export { isTestFrameworkCallback };
@@ -1,14 +1,4 @@
1
1
  "use strict";
2
- /**
3
- * Shared helpers for determining whether a function-like node should be
4
- * treated as a test framework callback that may be excluded from
5
- * function-level annotation requirements.
6
- *
7
- * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
8
- * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
9
- * @story docs/stories/013-exclude-test-framework-callbacks.proposed.md
10
- * @req REQ-TEST-CALLBACK-EXCLUSION - Provide reusable test callback exclusion logic
11
- */
12
2
  Object.defineProperty(exports, "__esModule", { value: true });
13
3
  exports.isTestFrameworkCallback = isTestFrameworkCallback;
14
4
  /**
@@ -91,7 +81,8 @@ function isTestFrameworkCallback(node, options) {
91
81
  if (!parent || parent.type !== "CallExpression") {
92
82
  return false;
93
83
  }
94
- const callee = parent.callee;
84
+ const callExpressionParent = parent;
85
+ const callee = callExpressionParent.callee;
95
86
  if (callee.type === "Identifier") {
96
87
  return isRecognizedTestHelperName(callee.name, options);
97
88
  }
@@ -175,8 +175,8 @@ function validateStoryAnnotation(context, comment, rawValue, options) {
175
175
  return;
176
176
  }
177
177
  // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
178
- // @req REQ-PATH-FORMAT - Reject @story values containing internal whitespace as invalid
179
- if (/\s/.test(trimmed)) {
178
+ // @req REQ-PATH-FORMAT - Reject @story values containing internal whitespace that do not collapse into a valid story path
179
+ if (/\s/.test(trimmed) && !pathPattern.test(collapsed)) {
180
180
  reportInvalidStoryFormat(context, comment, collapsed, options);
181
181
  return;
182
182
  }
@@ -223,6 +223,12 @@ function validateReqAnnotation(context, comment, rawValue, options) {
223
223
  return;
224
224
  }
225
225
  const collapsed = (0, valid_annotation_utils_1.collapseAnnotationValue)(trimmed);
226
+ // Allow mixed @req/@supports lines to pass without additional @req validation,
227
+ // while still validating simple multi-line @req identifiers that collapse
228
+ // to a single token.
229
+ if (collapsed.includes("@supports")) {
230
+ return;
231
+ }
226
232
  const reqPattern = options.reqPattern;
227
233
  // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
228
234
  // @req REQ-REQ-FORMAT - Flag @req identifiers that do not match the configured pattern
@@ -326,6 +326,10 @@ const rule = {
326
326
  if (process.env.TRACEABILITY_DEBUG === "1") {
327
327
  console.log("[no-redundant-annotation] BlockStatement parent=%s statements=%d", parent && parent.type, Array.isArray(node.body) ? node.body.length : 0);
328
328
  }
329
+ // @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-CATCH-BLOCK-HANDLING
330
+ if (parent && parent.type === "CatchClause") {
331
+ return;
332
+ }
329
333
  const scopePairs = collectScopePairs(context, parent, options.maxScopeDepth);
330
334
  debugScopePairs(parent, scopePairs);
331
335
  if (scopePairs.size === 0)
@@ -286,17 +286,8 @@ function tryBuildInlineAutoFix(context, comments, storyIndex, reqIndices) {
286
286
  * @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md
287
287
  * @req REQ-MIGRATE-INLINE
288
288
  */
289
- function handleInlineStorySequence(context, group, startIndex) {
289
+ function collectReqIndicesAfterStory(group, startIndex) {
290
290
  const n = group.length;
291
- const current = group[startIndex];
292
- const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(current.value || "");
293
- if (!normalized || !/^@story\b/.test(normalized)) {
294
- return startIndex + 1;
295
- }
296
- if (/^@supports\b/.test(normalized)) {
297
- return startIndex + 1;
298
- }
299
- const storyIndex = startIndex;
300
291
  const reqIndices = [];
301
292
  let j = startIndex + 1;
302
293
  while (j < n) {
@@ -312,6 +303,19 @@ function handleInlineStorySequence(context, group, startIndex) {
312
303
  }
313
304
  break;
314
305
  }
306
+ return { reqIndices, nextIndex: j };
307
+ }
308
+ function handleInlineStorySequence(context, group, startIndex) {
309
+ const current = group[startIndex];
310
+ const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(current.value || "");
311
+ if (!normalized || !/^@story\b/.test(normalized)) {
312
+ return startIndex + 1;
313
+ }
314
+ if (/^@supports\b/.test(normalized)) {
315
+ return startIndex + 1;
316
+ }
317
+ const storyIndex = startIndex;
318
+ const { reqIndices, nextIndex } = collectReqIndicesAfterStory(group, startIndex);
315
319
  if (reqIndices.length === 0) {
316
320
  context.report({
317
321
  node: current,
@@ -333,7 +337,7 @@ function handleInlineStorySequence(context, group, startIndex) {
333
337
  messageId: "preferImplements",
334
338
  });
335
339
  }
336
- return reqIndices[reqIndices.length - 1] + 1;
340
+ return nextIndex;
337
341
  }
338
342
  /**
339
343
  * Process a contiguous group of inline line comments, identifying legacy
@@ -343,19 +347,20 @@ function handleInlineStorySequence(context, group, startIndex) {
343
347
  * @story docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md
344
348
  * @req REQ-MIGRATE-INLINE
345
349
  */
350
+ function advanceInlineGroupIndex(context, group, currentIndex) {
351
+ const current = group[currentIndex];
352
+ const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(current.value || "");
353
+ if (!normalized || !/^@story\b/.test(normalized)) {
354
+ return currentIndex + 1;
355
+ }
356
+ return handleInlineStorySequence(context, group, currentIndex);
357
+ }
346
358
  function processInlineGroup(context, group) {
347
359
  if (group.length === 0)
348
360
  return;
349
- const n = group.length;
350
361
  let i = 0;
351
- while (i < n) {
352
- const current = group[i];
353
- const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(current.value || "");
354
- if (!normalized || !/^@story\b/.test(normalized)) {
355
- i += 1;
356
- continue;
357
- }
358
- i = handleInlineStorySequence(context, group, i);
362
+ while (i < group.length) {
363
+ i = advanceInlineGroupIndex(context, group, i);
359
364
  }
360
365
  }
361
366
  /**
@@ -3,6 +3,49 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const valid_annotation_options_1 = require("./helpers/valid-annotation-options");
4
4
  const valid_annotation_format_internal_1 = require("./helpers/valid-annotation-format-internal");
5
5
  const valid_annotation_format_validators_1 = require("./helpers/valid-annotation-format-validators");
6
+ function handleImplementsLine(normalized, pending, deps) {
7
+ const { context, comment, options } = deps;
8
+ const isImplements = /@supports\b/.test(normalized);
9
+ if (!isImplements) {
10
+ return pending;
11
+ }
12
+ const implementsValue = normalized.replace(/^@supports\b/, "").trim();
13
+ (0, valid_annotation_format_validators_1.validateImplementsAnnotation)(context, comment, implementsValue, options);
14
+ return pending;
15
+ }
16
+ function handleStoryOrReqLine(normalized, pending, deps) {
17
+ const { context, comment, options } = deps;
18
+ const isStory = /@story\b/.test(normalized);
19
+ const isReq = /@req\b/.test(normalized);
20
+ if (!isStory && !isReq) {
21
+ return pending;
22
+ }
23
+ (0, valid_annotation_format_validators_1.finalizePendingAnnotation)(context, comment, options, pending);
24
+ const rawValue = normalized.replace(/^@story\b|^@req\b/, "");
25
+ const trimmedValue = rawValue.trim();
26
+ return {
27
+ type: isStory ? "story" : "req",
28
+ value: trimmedValue,
29
+ hasValue: trimmedValue.length > 0,
30
+ };
31
+ }
32
+ function extendPendingAnnotation(normalized, pending) {
33
+ if (!pending) {
34
+ return pending;
35
+ }
36
+ const continuation = normalized.trim();
37
+ if (!continuation) {
38
+ return pending;
39
+ }
40
+ const updatedValue = pending.value
41
+ ? `${pending.value} ${continuation}`
42
+ : continuation;
43
+ return {
44
+ ...pending,
45
+ value: updatedValue,
46
+ hasValue: pending.hasValue || continuation.length > 0,
47
+ };
48
+ }
6
49
  /**
7
50
  * Process a single normalized comment line and update the pending annotation state.
8
51
  *
@@ -22,31 +65,21 @@ function processCommentLine({ normalized, pending, context, comment, options, })
22
65
  if (!normalized) {
23
66
  return pending;
24
67
  }
25
- const isStory = /@story\b/.test(normalized);
26
- const isReq = /@req\b/.test(normalized);
27
- const isImplements = /@supports\b/.test(normalized);
28
- // Handle @supports as an immediate, single-line annotation
29
- // @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
30
- // @req REQ-IMPLEMENTS-PARSE - Immediately validate @supports without starting multi-line state
31
- if (isImplements) {
32
- const implementsValue = normalized.replace(/^@supports\b/, "").trim();
33
- (0, valid_annotation_format_validators_1.validateImplementsAnnotation)(context, comment, implementsValue, options);
34
- return pending;
68
+ const afterImplements = handleImplementsLine(normalized, pending, {
69
+ context,
70
+ comment,
71
+ options,
72
+ });
73
+ if (afterImplements !== pending) {
74
+ return afterImplements;
35
75
  }
36
- // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
37
- // @story docs/stories/008.0-DEV-AUTO-FIX.story.md
38
- // @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
39
- // @req REQ-SYNTAX-VALIDATION - Start new pending annotation when a tag is found
40
- // @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
41
- // @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
42
- if (isStory || isReq) {
43
- (0, valid_annotation_format_validators_1.finalizePendingAnnotation)(context, comment, options, pending);
44
- const value = normalized.replace(/^@story\b|^@req\b/, "").trim();
45
- return {
46
- type: isStory ? "story" : "req",
47
- value,
48
- hasValue: value.trim().length > 0,
49
- };
76
+ const afterStoryOrReq = handleStoryOrReqLine(normalized, pending, {
77
+ context,
78
+ comment,
79
+ options,
80
+ });
81
+ if (afterStoryOrReq !== pending) {
82
+ return afterStoryOrReq;
50
83
  }
51
84
  // Implement JSDoc tag coexistence behavior: terminate @story/@req values when a new non-traceability JSDoc tag line (e.g., @param, @returns) is encountered.
52
85
  // @supports docs/stories/022.0-DEV-JSDOC-COEXISTENCE.story.md REQ-ANNOTATION-TERMINATION REQ-CONTINUATION-LOGIC
@@ -60,23 +93,7 @@ function processCommentLine({ normalized, pending, context, comment, options, })
60
93
  // @req REQ-MULTILINE-SUPPORT - Extend value of existing pending annotation across lines
61
94
  // @req REQ-AUTOFIX-FORMAT - Maintain complete logical value for downstream validation and fixes
62
95
  // @req REQ-MIXED-SUPPORT - Leave non-annotation lines untouched when no pending state exists
63
- if (pending) {
64
- const continuation = normalized.trim();
65
- // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
66
- // @req REQ-MULTILINE-SUPPORT - Skip blank continuation lines without altering pending annotation
67
- if (!continuation) {
68
- return pending;
69
- }
70
- const updatedValue = pending.value
71
- ? `${pending.value} ${continuation}`
72
- : continuation;
73
- return {
74
- ...pending,
75
- value: updatedValue,
76
- hasValue: pending.hasValue || continuation.length > 0,
77
- };
78
- }
79
- return pending;
96
+ return extendPendingAnnotation(normalized, pending);
80
97
  }
81
98
  /**
82
99
  * Process a single comment node and validate any @story/@req/@supports annotations it contains.
@@ -98,7 +115,7 @@ function processCommentLine({ normalized, pending, context, comment, options, })
98
115
  * @req REQ-FORMAT-VALIDATION - Validate @implements story path and requirement IDs
99
116
  * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
100
117
  */
101
- function processComment(context, comment, options) {
118
+ function processCommentLines({ context, comment, options, }) {
102
119
  const rawLines = (comment.value || "").split(/\r?\n/);
103
120
  let pending = null;
104
121
  rawLines.forEach((rawLine) => {
@@ -113,6 +130,9 @@ function processComment(context, comment, options) {
113
130
  });
114
131
  (0, valid_annotation_format_validators_1.finalizePendingAnnotation)(context, comment, options, pending);
115
132
  }
133
+ function processComment(context, comment, options) {
134
+ processCommentLines({ context, comment, options });
135
+ }
116
136
  exports.default = {
117
137
  meta: {
118
138
  type: "problem",
@@ -95,4 +95,35 @@ function process(value) {
95
95
  expect(fixedB.output).toContain("@req REQ-PROCESS");
96
96
  expect(fixedB.output).not.toContain("@req REQ-PROCESS\n */\n return");
97
97
  });
98
+ it("[REQ-CATCH-BLOCK-HANDLING] does not report redundant annotations for try/if/else-if/catch pattern from story 027.0 (regression from issue #6)", async () => {
99
+ const code = `// @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md
100
+ // @req REQ-SAFE-ONLY
101
+ async function filterVulnerableVersions(versionInfo, safeVersions) {
102
+ try {
103
+ // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md
104
+ // @req REQ-SAFE-ONLY
105
+ if (!versionInfo) {
106
+ return [];
107
+ } else if (!safeVersions || safeVersions.length === 0) {
108
+ return versionInfo;
109
+ }
110
+
111
+ // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md
112
+ // @req REQ-SAFE-ONLY
113
+ return versionInfo.filter(v => safeVersions.includes(v));
114
+ } catch (error) {
115
+ // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md
116
+ // @req REQ-SAFE-ONLY
117
+ return [];
118
+ }
119
+ }
120
+ `;
121
+ const config = {
122
+ rules: {
123
+ "traceability/no-redundant-annotation": ["warn"],
124
+ },
125
+ };
126
+ const result = await lintTextWithConfig(code, "filter-vulnerable-versions.js", config);
127
+ expect(result.messages.filter((m) => m.ruleId === "traceability/no-redundant-annotation").length).toBe(0);
128
+ });
98
129
  });
@@ -0,0 +1,148 @@
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
+ * Integration tests for require-traceability with configurable test callback exclusion.
8
+ *
9
+ * @supports docs/stories/010.4-DEV-UNIFIED-FUNCTION-RULE-AND-ALIASES.story.md REQ-UNIFIED-ALIAS-ENGINE
10
+ * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED REQ-FUNCTION-DETECTION
11
+ * @supports docs/stories/013-exclude-test-framework-callbacks.proposed.md REQ-TEST-CALLBACK-EXCLUSION
12
+ */
13
+ const use_at_your_own_risk_1 = require("eslint/use-at-your-own-risk");
14
+ const index_1 = __importDefault(require("../../src/index"));
15
+ async function lintTextWithConfig(text, filename, extraConfig) {
16
+ const baseConfig = {
17
+ plugins: {
18
+ traceability: index_1.default,
19
+ },
20
+ };
21
+ const eslint = new use_at_your_own_risk_1.FlatESLint({
22
+ overrideConfig: [baseConfig, ...extraConfig],
23
+ overrideConfigFile: true,
24
+ ignore: false,
25
+ });
26
+ const [result] = await eslint.lintText(text, { filePath: filename });
27
+ return result;
28
+ }
29
+ describe("Unified require-traceability with configurable test callback exclusion (Story 013-exclude-test-framework-callbacks)", () => {
30
+ const baseHeader = `/**\n * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */`;
31
+ const jsTestCallback = `${baseHeader}\n
32
+ describe('suite', () => {\n it('does something', () => {\n const value = 1;\n });\n});`;
33
+ const tsTestCallback = `${baseHeader}\n
34
+ import { describe, it } from 'vitest';
35
+
36
+ describe('suite', () => {\n it('does something', () => {\n const value = 1;\n });\n});`;
37
+ const jsBenchCallback = `${baseHeader}\n
38
+ import { bench } from 'vitest';
39
+
40
+ bench('bench case', () => {\n function helper() {}\n helper();\n});`;
41
+ const jsCustomHelperCallback = `${baseHeader}\n
42
+ function helperWrapper(fn) {\n return fn;\n}
43
+
44
+ helperWrapper(() => {\n function helper() {}\n helper();\n});`;
45
+ async function getRuleMessages(code, filename, extraConfig) {
46
+ const result = await lintTextWithConfig(code, filename, extraConfig);
47
+ return result.messages.filter((m) => m.ruleId === "traceability/require-traceability" ||
48
+ m.ruleId === "traceability/require-story-annotation");
49
+ }
50
+ it("[REQ-TEST-CALLBACK-EXCLUSION] excludes callbacks under known test helpers when configured", async () => {
51
+ const config = [
52
+ {
53
+ rules: {
54
+ "traceability/require-traceability": ["error"],
55
+ "traceability/require-story-annotation": [
56
+ "error",
57
+ {
58
+ excludeTestCallbacks: true,
59
+ },
60
+ ],
61
+ },
62
+ },
63
+ ];
64
+ const messagesJs = await getRuleMessages(jsTestCallback, "example.test.js", config);
65
+ const messagesTs = await getRuleMessages(tsTestCallback, "example.test.ts", config);
66
+ expect(messagesJs).toHaveLength(0);
67
+ expect(messagesTs).toHaveLength(0);
68
+ });
69
+ it("[REQ-TEST-CALLBACK-EXCLUSION] never excludes Vitest bench callbacks via test-callback exclusion, even when exclusion is enabled", async () => {
70
+ const baseConfig = [
71
+ {
72
+ rules: {
73
+ "traceability/require-traceability": ["error"],
74
+ "traceability/require-story-annotation": [
75
+ "error",
76
+ {
77
+ excludeTestCallbacks: true,
78
+ },
79
+ ],
80
+ },
81
+ },
82
+ ];
83
+ const withBenchAsHelperConfig = [
84
+ {
85
+ rules: {
86
+ "traceability/require-traceability": ["error"],
87
+ "traceability/require-story-annotation": [
88
+ "error",
89
+ {
90
+ excludeTestCallbacks: true,
91
+ additionalTestHelperNames: ["bench"],
92
+ },
93
+ ],
94
+ },
95
+ },
96
+ ];
97
+ const baseResult = await lintTextWithConfig(jsBenchCallback, "bench.test.ts", baseConfig);
98
+ const withBenchHelperResult = await lintTextWithConfig(jsBenchCallback, "bench.test.ts", withBenchAsHelperConfig);
99
+ const baseMessages = baseResult.messages.filter((m) => m.ruleId === "traceability/require-traceability" ||
100
+ m.ruleId === "traceability/require-story-annotation");
101
+ const withBenchHelperMessages = withBenchHelperResult.messages.filter((m) => m.ruleId === "traceability/require-traceability" ||
102
+ m.ruleId === "traceability/require-story-annotation");
103
+ expect(withBenchHelperMessages.length).toBeGreaterThanOrEqual(baseMessages.length);
104
+ });
105
+ it("[REQ-TEST-CALLBACK-EXCLUSION] respects additionalTestHelperNames for custom helpers but not for bench callbacks", async () => {
106
+ const baseConfig = [
107
+ {
108
+ rules: {
109
+ "traceability/require-traceability": ["error"],
110
+ "traceability/require-story-annotation": [
111
+ "error",
112
+ {
113
+ excludeTestCallbacks: true,
114
+ },
115
+ ],
116
+ },
117
+ },
118
+ ];
119
+ const withAdditionalHelpersConfig = [
120
+ {
121
+ rules: {
122
+ "traceability/require-traceability": ["error"],
123
+ "traceability/require-story-annotation": [
124
+ "error",
125
+ {
126
+ excludeTestCallbacks: true,
127
+ additionalTestHelperNames: ["helperWrapper", "bench"],
128
+ },
129
+ ],
130
+ },
131
+ },
132
+ ];
133
+ const wrapperBaseResult = await lintTextWithConfig(jsCustomHelperCallback, "helper-wrapper.test.ts", baseConfig);
134
+ const wrapperWithHelpersResult = await lintTextWithConfig(jsCustomHelperCallback, "helper-wrapper.test.ts", withAdditionalHelpersConfig);
135
+ const benchBaseResult = await lintTextWithConfig(jsBenchCallback, "bench.test.ts", baseConfig);
136
+ const benchWithHelpersResult = await lintTextWithConfig(jsBenchCallback, "bench.test.ts", withAdditionalHelpersConfig);
137
+ const wrapperBaseMessages = wrapperBaseResult.messages.filter((m) => m.ruleId === "traceability/require-traceability" ||
138
+ m.ruleId === "traceability/require-story-annotation");
139
+ const wrapperWithHelpersMessages = wrapperWithHelpersResult.messages.filter((m) => m.ruleId === "traceability/require-traceability" ||
140
+ m.ruleId === "traceability/require-story-annotation");
141
+ const benchBaseMessages = benchBaseResult.messages.filter((m) => m.ruleId === "traceability/require-traceability" ||
142
+ m.ruleId === "traceability/require-story-annotation");
143
+ const benchWithHelpersMessages = benchWithHelpersResult.messages.filter((m) => m.ruleId === "traceability/require-traceability" ||
144
+ m.ruleId === "traceability/require-story-annotation");
145
+ expect(wrapperWithHelpersMessages.length).toBeLessThanOrEqual(wrapperBaseMessages.length);
146
+ expect(benchWithHelpersMessages.length).toBeGreaterThanOrEqual(benchBaseMessages.length);
147
+ });
148
+ });
@@ -77,8 +77,8 @@ describe("detectStaleAnnotations isolated (Story 009.0-DEV-MAINTENANCE-TOOLS)",
77
77
  }
78
78
  });
79
79
  it("[REQ-MAINT-DETECT] handles permission denied errors by returning an empty result", () => {
80
- const tmpDir2 = fs.mkdtempSync(path.join(os.tmpdir(), "tmp-perm-"));
81
- const dir = path.join(tmpDir2, "subdir");
80
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tmp-perm-"));
81
+ const dir = path.join(tmpDir, "subdir");
82
82
  fs.mkdirSync(dir);
83
83
  const filePath = path.join(dir, "file.ts");
84
84
  const content = `
@@ -87,24 +87,32 @@ describe("detectStaleAnnotations isolated (Story 009.0-DEV-MAINTENANCE-TOOLS)",
87
87
  */
88
88
  `;
89
89
  fs.writeFileSync(filePath, content, "utf8");
90
- // Remove read permission
90
+ const originalReadFileSync = fs.readFileSync;
91
+ const readSpy = jest
92
+ .spyOn(fs, "readFileSync")
93
+ .mockImplementation((p, ...args) => {
94
+ const strPath = typeof p === "string" ? p : p.toString();
95
+ if (strPath === filePath) {
96
+ const err = new Error("EACCES: permission denied, open");
97
+ err.code = "EACCES";
98
+ throw err;
99
+ }
100
+ // Delegate to original implementation for all other paths
101
+ // to keep behavior realistic.
102
+ // @ts-ignore
103
+ return originalReadFileSync(p, ...args);
104
+ });
91
105
  try {
92
- fs.chmodSync(dir, 0o000);
93
- expect(() => (0, detect_1.detectStaleAnnotations)(tmpDir2)).toThrow();
106
+ const result = (0, detect_1.detectStaleAnnotations)(tmpDir);
107
+ expect(result).toEqual([]);
94
108
  }
95
109
  finally {
96
- // Restore permissions and cleanup temporary directory, ignoring errors during cleanup
97
- try {
98
- fs.chmodSync(dir, 0o700);
99
- }
100
- catch {
101
- // ignore
102
- }
110
+ readSpy.mockRestore();
103
111
  try {
104
- fs.rmSync(tmpDir2, { recursive: true, force: true });
112
+ fs.rmSync(tmpDir, { recursive: true, force: true });
105
113
  }
106
114
  catch {
107
- // ignore
115
+ // ignore cleanup errors
108
116
  }
109
117
  }
110
118
  });
@@ -42,6 +42,8 @@ const os = __importStar(require("os"));
42
42
  const path = __importStar(require("path"));
43
43
  const perf_hooks_1 = require("perf_hooks");
44
44
  const cli_1 = require("../../src/maintenance/cli");
45
+ // Performance budget documented in docs/maintenance-performance-tests.md
46
+ const CLI_LARGE_WORKSPACE_PERF_BUDGET_MS = 5000;
45
47
  function createCliLargeWorkspace() {
46
48
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "traceability-cli-large-"));
47
49
  // Create a modestly sized workspace reusing the same shape as the core perf tests,
@@ -71,78 +73,157 @@ export function cli_example_${moduleIndex}_${fileIndex}() {}
71
73
  },
72
74
  };
73
75
  }
76
+ function createDeepNestedCliWorkspace() {
77
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "traceability-cli-deep-nested-"));
78
+ // Create a deeply nested directory structure with a small number of files.
79
+ for (let branchIndex = 0; branchIndex < 3; branchIndex += 1) {
80
+ const level1 = path.join(root, `branch-${branchIndex.toString().padStart(3, "0")}`);
81
+ fs.mkdirSync(level1);
82
+ const level2 = path.join(level1, "deep", "nested", "structure");
83
+ fs.mkdirSync(path.join(level1, "deep"), { recursive: true });
84
+ fs.mkdirSync(path.join(level1, "deep", "nested"), { recursive: true });
85
+ fs.mkdirSync(level2, { recursive: true });
86
+ for (let fileIndex = 0; fileIndex < 3; fileIndex += 1) {
87
+ const filePath = path.join(level2, `deep-file-${fileIndex.toString().padStart(3, "0")}.ts`);
88
+ const validStory = "cli-valid.story.md";
89
+ const staleStory = "cli-deep-stale.story.md";
90
+ const content = `/**
91
+ * @story ${validStory}
92
+ * @story ${staleStory}
93
+ */
94
+ export function cli_deep_example_${branchIndex}_${fileIndex}() {}
95
+ `;
96
+ fs.writeFileSync(filePath, content, "utf8");
97
+ }
98
+ }
99
+ // Create the valid story file so that only the stale entries are reported.
100
+ fs.writeFileSync(path.join(root, "cli-valid.story.md"), "# cli valid", "utf8");
101
+ return {
102
+ root,
103
+ cleanup: () => {
104
+ fs.rmSync(root, { recursive: true, force: true });
105
+ },
106
+ };
107
+ }
74
108
  describe("Maintenance CLI on large workspaces (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
75
- let workspace;
76
- let originalCwd;
77
- beforeAll(() => {
78
- originalCwd = process.cwd();
79
- workspace = createCliLargeWorkspace();
80
- process.chdir(workspace.root);
81
- });
82
- afterAll(() => {
83
- process.chdir(originalCwd);
84
- workspace.cleanup();
85
- });
86
109
  it("[REQ-MAINT-DETECT] detect --json completes within a generous time budget and returns JSON payload", () => {
110
+ const { root, cleanup } = createCliLargeWorkspace();
111
+ const originalCwd = process.cwd();
112
+ process.chdir(root);
87
113
  const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
88
- const start = perf_hooks_1.performance.now();
89
- const exitCode = (0, cli_1.runMaintenanceCli)([
90
- "node",
91
- "traceability-maint",
92
- "detect",
93
- "--root",
94
- workspace.root,
95
- "--json",
96
- ]);
97
- const durationMs = perf_hooks_1.performance.now() - start;
98
- expect(exitCode === 0 || exitCode === 1).toBe(true);
99
- expect(durationMs).toBeLessThan(5000);
100
- expect(logSpy).toHaveBeenCalledTimes(1);
101
- const payloadRaw = String(logSpy.mock.calls[0][0]);
102
- const payload = JSON.parse(payloadRaw);
103
- expect(payload.root).toBe(workspace.root);
104
- expect(Array.isArray(payload.stale)).toBe(true);
105
- expect(payload.stale.length).toBeGreaterThan(0);
106
- logSpy.mockRestore();
114
+ try {
115
+ const start = perf_hooks_1.performance.now();
116
+ const exitCode = (0, cli_1.runMaintenanceCli)([
117
+ "node",
118
+ "traceability-maint",
119
+ "detect",
120
+ "--root",
121
+ root,
122
+ "--json",
123
+ ]);
124
+ const durationMs = perf_hooks_1.performance.now() - start;
125
+ expect(exitCode === 0 || exitCode === 1).toBe(true);
126
+ expect(durationMs).toBeLessThan(CLI_LARGE_WORKSPACE_PERF_BUDGET_MS);
127
+ expect(logSpy).toHaveBeenCalledTimes(1);
128
+ const payloadRaw = String(logSpy.mock.calls[0][0]);
129
+ const payload = JSON.parse(payloadRaw);
130
+ expect(payload.root).toBe(root);
131
+ expect(Array.isArray(payload.stale)).toBe(true);
132
+ expect(payload.stale.length).toBeGreaterThan(0);
133
+ }
134
+ finally {
135
+ logSpy.mockRestore();
136
+ process.chdir(originalCwd);
137
+ cleanup();
138
+ }
107
139
  });
108
140
  it("[REQ-MAINT-REPORT] report --format=json completes within a generous time budget", () => {
141
+ const { root, cleanup } = createCliLargeWorkspace();
142
+ const originalCwd = process.cwd();
143
+ process.chdir(root);
109
144
  const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
110
- const start = perf_hooks_1.performance.now();
111
- const exitCode = (0, cli_1.runMaintenanceCli)([
112
- "node",
113
- "traceability-maint",
114
- "report",
115
- "--root",
116
- workspace.root,
117
- "--format",
118
- "json",
119
- ]);
120
- const durationMs = perf_hooks_1.performance.now() - start;
121
- expect(exitCode).toBe(0);
122
- expect(durationMs).toBeLessThan(5000);
123
- expect(logSpy).toHaveBeenCalledTimes(1);
124
- const payloadRaw = String(logSpy.mock.calls[0][0]);
125
- const payload = JSON.parse(payloadRaw);
126
- expect(payload.root).toBe(workspace.root);
127
- expect(typeof payload.report).toBe("string");
128
- logSpy.mockRestore();
145
+ try {
146
+ const start = perf_hooks_1.performance.now();
147
+ const exitCode = (0, cli_1.runMaintenanceCli)([
148
+ "node",
149
+ "traceability-maint",
150
+ "report",
151
+ "--root",
152
+ root,
153
+ "--format",
154
+ "json",
155
+ ]);
156
+ const durationMs = perf_hooks_1.performance.now() - start;
157
+ expect(exitCode).toBe(0);
158
+ expect(durationMs).toBeLessThan(CLI_LARGE_WORKSPACE_PERF_BUDGET_MS);
159
+ expect(logSpy).toHaveBeenCalledTimes(1);
160
+ const payloadRaw = String(logSpy.mock.calls[0][0]);
161
+ const payload = JSON.parse(payloadRaw);
162
+ expect(payload.root).toBe(root);
163
+ expect(typeof payload.report).toBe("string");
164
+ }
165
+ finally {
166
+ logSpy.mockRestore();
167
+ process.chdir(originalCwd);
168
+ cleanup();
169
+ }
129
170
  });
130
171
  it("[REQ-MAINT-VERIFY] verify completes within a generous time budget and reports stale annotations", () => {
172
+ const { root, cleanup } = createCliLargeWorkspace();
173
+ const originalCwd = process.cwd();
174
+ process.chdir(root);
131
175
  const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
132
- const start = perf_hooks_1.performance.now();
133
- const exitCode = (0, cli_1.runMaintenanceCli)([
134
- "node",
135
- "traceability-maint",
136
- "verify",
137
- "--root",
138
- workspace.root,
139
- ]);
140
- const durationMs = perf_hooks_1.performance.now() - start;
141
- expect(exitCode).toBe(1);
142
- expect(durationMs).toBeLessThan(5000);
143
- expect(logSpy).toHaveBeenCalledTimes(1);
144
- const message = String(logSpy.mock.calls[0][0]);
145
- expect(message).toContain("Stale or invalid traceability annotations detected under");
146
- logSpy.mockRestore();
176
+ try {
177
+ const start = perf_hooks_1.performance.now();
178
+ const exitCode = (0, cli_1.runMaintenanceCli)([
179
+ "node",
180
+ "traceability-maint",
181
+ "verify",
182
+ "--root",
183
+ root,
184
+ ]);
185
+ const durationMs = perf_hooks_1.performance.now() - start;
186
+ expect(exitCode).toBe(1);
187
+ expect(durationMs).toBeLessThan(CLI_LARGE_WORKSPACE_PERF_BUDGET_MS);
188
+ expect(logSpy).toHaveBeenCalledTimes(1);
189
+ const message = String(logSpy.mock.calls[0][0]);
190
+ expect(message).toContain("Stale or invalid traceability annotations detected under");
191
+ }
192
+ finally {
193
+ logSpy.mockRestore();
194
+ process.chdir(originalCwd);
195
+ cleanup();
196
+ }
197
+ });
198
+ it("[REQ-MAINT-DETECT] detect traverses deeply nested directories within a generous time budget", () => {
199
+ const { root, cleanup } = createDeepNestedCliWorkspace();
200
+ const originalCwd = process.cwd();
201
+ process.chdir(root);
202
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
203
+ try {
204
+ const start = perf_hooks_1.performance.now();
205
+ const exitCode = (0, cli_1.runMaintenanceCli)([
206
+ "node",
207
+ "traceability-maint",
208
+ "detect",
209
+ "--root",
210
+ root,
211
+ "--json",
212
+ ]);
213
+ const durationMs = perf_hooks_1.performance.now() - start;
214
+ expect(exitCode === 0 || exitCode === 1).toBe(true);
215
+ expect(durationMs).toBeLessThan(CLI_LARGE_WORKSPACE_PERF_BUDGET_MS);
216
+ expect(logSpy).toHaveBeenCalledTimes(1);
217
+ const payloadRaw = String(logSpy.mock.calls[0][0]);
218
+ const payload = JSON.parse(payloadRaw);
219
+ expect(payload.root).toBe(root);
220
+ expect(Array.isArray(payload.stale)).toBe(true);
221
+ expect(payload.stale.length).toBeGreaterThan(0);
222
+ }
223
+ finally {
224
+ logSpy.mockRestore();
225
+ process.chdir(originalCwd);
226
+ cleanup();
227
+ }
147
228
  });
148
229
  });
@@ -45,6 +45,8 @@ const detect_1 = require("../../src/maintenance/detect");
45
45
  const batch_1 = require("../../src/maintenance/batch");
46
46
  const report_1 = require("../../src/maintenance/report");
47
47
  const update_1 = require("../../src/maintenance/update");
48
+ // Performance budget for large-workspace maintenance tests; documented in docs/maintenance-performance-tests.md.
49
+ const LARGE_WORKSPACE_PERF_BUDGET_MS = 5000;
48
50
  /**
49
51
  * Shape of the synthetic large workspace:
50
52
  * - 10 modules (module-000 .. module-009)
@@ -92,58 +94,75 @@ export function example_${moduleIndex}_${fileIndex}() {}
92
94
  };
93
95
  }
94
96
  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
97
  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);
98
+ const workspace = createLargeWorkspace();
99
+ try {
100
+ const start = perf_hooks_1.performance.now();
101
+ const stale = (0, detect_1.detectStaleAnnotations)(workspace.root);
102
+ const durationMs = perf_hooks_1.performance.now() - start;
103
+ // Sanity check: we expect at least some stale entries due to the generated stale-story-* references.
104
+ expect(stale.length).toBeGreaterThan(0);
105
+ // Guardrail: this operation should remain comfortably under ~5 seconds on CI hardware.
106
+ expect(durationMs).toBeLessThan(LARGE_WORKSPACE_PERF_BUDGET_MS);
107
+ }
108
+ finally {
109
+ workspace.cleanup();
110
+ }
110
111
  });
111
112
  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);
113
+ const workspace = createLargeWorkspace();
114
+ try {
115
+ const start = perf_hooks_1.performance.now();
116
+ const result = (0, batch_1.verifyAnnotations)(workspace.root);
117
+ const durationMs = perf_hooks_1.performance.now() - start;
118
+ // With both valid and stale references, verification should report false.
119
+ expect(result).toBe(false);
120
+ expect(durationMs).toBeLessThan(LARGE_WORKSPACE_PERF_BUDGET_MS);
121
+ }
122
+ finally {
123
+ workspace.cleanup();
124
+ }
118
125
  });
119
126
  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);
127
+ const workspace = createLargeWorkspace();
128
+ try {
129
+ const start = perf_hooks_1.performance.now();
130
+ const report = (0, report_1.generateMaintenanceReport)(workspace.root);
131
+ const durationMs = perf_hooks_1.performance.now() - start;
132
+ expect(report).not.toBe("");
133
+ expect(durationMs).toBeLessThan(LARGE_WORKSPACE_PERF_BUDGET_MS);
134
+ }
135
+ finally {
136
+ workspace.cleanup();
137
+ }
125
138
  });
126
139
  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);
140
+ const workspace = createLargeWorkspace();
141
+ try {
142
+ const exampleOldPath = "stale-story-0000.story.md";
143
+ const exampleNewPath = "updated-story-0000.story.md";
144
+ const singleStart = perf_hooks_1.performance.now();
145
+ const updatedCount = (0, update_1.updateAnnotationReferences)(workspace.root, exampleOldPath, exampleNewPath);
146
+ const singleDuration = perf_hooks_1.performance.now() - singleStart;
147
+ expect(updatedCount).toBeGreaterThan(0);
148
+ expect(singleDuration).toBeLessThan(LARGE_WORKSPACE_PERF_BUDGET_MS);
149
+ const batchStart = perf_hooks_1.performance.now();
150
+ const totalUpdated = (0, batch_1.batchUpdateAnnotations)(workspace.root, [
151
+ {
152
+ oldPath: "stale-story-0001.story.md",
153
+ newPath: "updated-story-0001.story.md",
154
+ },
155
+ {
156
+ oldPath: "stale-story-0002.story.md",
157
+ newPath: "updated-story-0002.story.md",
158
+ },
159
+ ]);
160
+ const batchDuration = perf_hooks_1.performance.now() - batchStart;
161
+ expect(totalUpdated).toBeGreaterThanOrEqual(2);
162
+ expect(batchDuration).toBeLessThan(LARGE_WORKSPACE_PERF_BUDGET_MS);
163
+ }
164
+ finally {
165
+ workspace.cleanup();
166
+ }
148
167
  });
149
168
  });
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
11
11
  * @req REQ-STATEMENT-SIGNIFICANCE - Verify that simple statements are treated as redundant when covered by scope
12
12
  * @req REQ-SAFE-REMOVAL - Verify that auto-fix removes only redundant annotations and preserves code
13
13
  * @req REQ-DIFFERENT-REQUIREMENTS - Verify that annotations with different requirement IDs are preserved
14
+ * @req REQ-CATCH-BLOCK-HANDLING - Verify that catch block annotations are not incorrectly treated as redundant
14
15
  */
15
16
  const eslint_1 = require("eslint");
16
17
  const no_redundant_annotation_1 = __importDefault(require("../../src/rules/no-redundant-annotation"));
@@ -37,6 +38,10 @@ describe("no-redundant-annotation rule (Story 027.0-DEV-REDUNDANT-ANNOTATION-DET
37
38
  name: "[REQ-SCOPE-ANALYSIS] preserves annotations on both branch and statement when they intentionally duplicate each other",
38
39
  code: `function example() {\n if (condition) { // @story docs/stories/007.0-EXAMPLE.story.md @req REQ-BRANCH\n // @story docs/stories/007.0-EXAMPLE.story.md\n // @req REQ-BRANCH\n doBranchWork();\n }\n}`,
39
40
  },
41
+ {
42
+ name: "[REQ-CATCH-BLOCK-HANDLING] preserves catch block annotation from issue #6 scenario",
43
+ code: `async function example() {\n try {\n // @story prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.story.md\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n if (isSafeVersion({ version, vulnerabilityData })) {\n return version;\n }\n\n // @story prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.story.md\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n if (!vulnerabilityData.isVulnerable) {\n return version;\n }\n } catch (error) {\n // @story prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.story.md\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n return null;\n }\n}`,
44
+ },
40
45
  ],
41
46
  invalid: [
42
47
  {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Focused branch coverage tests for annotation-checker helper.
2
+ * Focused autofix behavior tests for annotation-checker helper.
3
3
  * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-AUTOFIX REQ-ANNOTATION-REPORTING
4
4
  */
5
5
  export {};
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * Focused branch coverage tests for annotation-checker helper.
3
+ * Focused autofix behavior tests for annotation-checker helper.
4
4
  * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-AUTOFIX REQ-ANNOTATION-REPORTING
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -34,7 +34,7 @@ function createContextStub() {
34
34
  };
35
35
  return { context, report };
36
36
  }
37
- describe("annotation-checker helper branch coverage (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", () => {
37
+ describe("annotation-checker helper autofix behavior (Story 003.0-DEV-FUNCTION-ANNOTATIONS)", () => {
38
38
  it("[REQ-ANNOTATION-AUTOFIX] attaches fix directly to node when parent is missing", () => {
39
39
  const { context, report } = createContextStub();
40
40
  const node = { type: "FunctionDeclaration" }; // no parent property
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.17.0",
3
+ "version": "1.17.1",
4
4
  "description": "A customizable ESLint plugin that enforces traceability annotations in your code, ensuring each implementation is linked to its requirement or test case.",
5
5
  "main": "lib/src/index.js",
6
6
  "types": "lib/src/index.d.ts",
@@ -88,7 +88,7 @@
88
88
  "jest": "^30.2.0",
89
89
  "jscpd": "^4.0.5",
90
90
  "lint-staged": "^16.2.7",
91
- "prettier": "^3.6.2",
91
+ "prettier": "^3.7.4",
92
92
  "semantic-release": "25.0.2",
93
93
  "ts-jest": "^29.4.6",
94
94
  "typescript": "^5.9.3",