eslint-plugin-traceability 1.10.1 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +4 -3
  2. package/README.md +1 -0
  3. package/lib/src/maintenance/cli.js +12 -12
  4. package/lib/src/maintenance/detect.js +19 -19
  5. package/lib/src/rules/helpers/require-story-core.d.ts +2 -15
  6. package/lib/src/rules/helpers/require-story-core.js +4 -71
  7. package/lib/src/rules/helpers/require-story-helpers.d.ts +32 -8
  8. package/lib/src/rules/helpers/require-story-helpers.js +44 -15
  9. package/lib/src/rules/helpers/require-story-visitors.js +47 -6
  10. package/lib/src/rules/helpers/valid-annotation-format-validators.js +5 -1
  11. package/lib/src/rules/helpers/valid-annotation-options.d.ts +6 -0
  12. package/lib/src/rules/helpers/valid-annotation-options.js +4 -0
  13. package/lib/src/rules/helpers/valid-annotation-utils.js +31 -31
  14. package/lib/src/rules/helpers/valid-story-reference-helpers.js +19 -19
  15. package/lib/src/rules/prefer-implements-annotation.js +29 -1
  16. package/lib/src/rules/require-story-annotation.js +15 -0
  17. package/lib/src/rules/require-test-traceability.js +1 -6
  18. package/lib/src/utils/annotation-checker.js +1 -1
  19. package/lib/tests/perf/maintenance-cli-large-workspace.test.d.ts +1 -0
  20. package/lib/tests/perf/maintenance-cli-large-workspace.test.js +130 -0
  21. package/lib/tests/perf/maintenance-large-workspace.test.d.ts +1 -0
  22. package/lib/tests/perf/maintenance-large-workspace.test.js +149 -0
  23. package/lib/tests/rules/auto-fix-behavior-008.test.js +23 -0
  24. package/lib/tests/rules/require-story-core.autofix.test.js +9 -3
  25. package/lib/tests/rules/require-story-core.test.js +13 -7
  26. package/lib/tests/rules/require-story-helpers-edgecases.test.js +1 -1
  27. package/lib/tests/rules/require-story-helpers.test.js +14 -8
  28. package/lib/tests/utils/require-story-core-test-helpers.d.ts +1 -1
  29. package/lib/tests/utils/require-story-core-test-helpers.js +16 -16
  30. package/lib/tests/utils/temp-dir-helpers.js +1 -1
  31. package/package.json +9 -2
  32. package/user-docs/api-reference.md +8 -4
  33. package/user-docs/examples.md +42 -0
@@ -167,7 +167,11 @@ function validateStoryAnnotation(context, comment, rawValue, options) {
167
167
  // @story docs/stories/008.0-DEV-AUTO-FIX.story.md
168
168
  // @req REQ-AUTOFIX-FORMAT - Apply suffix-only auto-fix when it yields a pattern-compliant path
169
169
  if (fixed && pathPattern.test(fixed)) {
170
- reportInvalidStoryFormatWithFix(context, comment, collapsed, fixed);
170
+ if (options.autoFix !== false) {
171
+ reportInvalidStoryFormatWithFix(context, comment, collapsed, fixed);
172
+ return;
173
+ }
174
+ reportInvalidStoryFormat(context, comment, collapsed, options);
171
175
  return;
172
176
  }
173
177
  reportInvalidStoryFormat(context, comment, collapsed, options);
@@ -53,6 +53,11 @@ export interface AnnotationRuleOptions {
53
53
  * Human-readable example requirement ID used in error messages.
54
54
  */
55
55
  requirementIdExample?: string;
56
+ /**
57
+ * Global toggle for auto-fix behavior in valid-annotation-format.
58
+ * When false, no automatic suffix-normalization fixes are applied.
59
+ */
60
+ autoFix?: boolean;
56
61
  }
57
62
  /**
58
63
  * Resolved, runtime-ready options for the rule.
@@ -62,6 +67,7 @@ export interface ResolvedAnnotationOptions {
62
67
  storyExample: string;
63
68
  reqPattern: RegExp;
64
69
  reqExample: string;
70
+ autoFix: boolean;
65
71
  }
66
72
  export declare function getDefaultReqExample(): string;
67
73
  export declare function getResolvedDefaults(): ResolvedAnnotationOptions;
@@ -26,6 +26,7 @@ let resolvedDefaults = {
26
26
  storyExample: getDefaultStoryExample(),
27
27
  reqPattern: getDefaultReqPattern(),
28
28
  reqExample: getDefaultReqExample(),
29
+ autoFix: true,
29
30
  };
30
31
  /**
31
32
  * Collected configuration errors encountered while resolving options.
@@ -119,6 +120,8 @@ function resolveOptions(rawOptions) {
119
120
  const flatReqPattern = user?.requirementIdPattern;
120
121
  const nestedReqExample = user?.req?.example;
121
122
  const flatReqExample = user?.requirementIdExample;
123
+ const autoFixFlag = user?.autoFix;
124
+ const autoFix = typeof autoFixFlag === "boolean" ? autoFixFlag : true;
122
125
  const storyPattern = resolvePattern({
123
126
  nestedPattern: nestedStoryPattern,
124
127
  nestedFieldName: "story.pattern",
@@ -140,6 +143,7 @@ function resolveOptions(rawOptions) {
140
143
  storyExample,
141
144
  reqPattern,
142
145
  reqExample,
146
+ autoFix,
143
147
  };
144
148
  return resolvedDefaults;
145
149
  }
@@ -39,7 +39,7 @@ exports.STORY_EXAMPLE_PATH = "docs/stories/005.0-DEV-EXAMPLE.story.md";
39
39
  * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
40
40
  */
41
41
  function collapseAnnotationValue(value) {
42
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-MULTILINE-SUPPORT
42
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-MULTILINE-SUPPORT
43
43
  return value.replace(/\s+/g, "");
44
44
  }
45
45
  /**
@@ -62,42 +62,42 @@ function collapseAnnotationValue(value) {
62
62
  */
63
63
  function getFixedStoryPath(original) {
64
64
  // @story docs/stories/010.1-REQ-STORY-PATH-STRICTNESS.story.md | REQ-AUTOFIX-SAFE - Reject auto-fix when the path contains ".." traversal segments to avoid broadening the reference.
65
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT REQ-ERROR-SPECIFICITY - Enforces correctness of the story identifier by rejecting paths that use unsafe traversal segments.
65
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT REQ-ERROR-SPECIFICITY - Enforces correctness of the story identifier by rejecting paths that use unsafe traversal segments.
66
66
  if (original.includes("..")) {
67
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT
68
- // @implements docs/stories/008.0-DEV-AUTO-FIX.story.md REQ-AUTOFIX-SAFE
69
- // @implements docs/stories/010.1-REQ-STORY-PATH-STRICTNESS.story.md REQ-AUTOFIX-SAFE
67
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT
68
+ // @supports docs/stories/008.0-DEV-AUTO-FIX.story.md REQ-AUTOFIX-SAFE
69
+ // @supports docs/stories/010.1-REQ-STORY-PATH-STRICTNESS.story.md REQ-AUTOFIX-SAFE
70
70
  return null;
71
71
  }
72
72
  // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md | REQ-AUTOFIX-FORMAT - Leave correctly formatted ".story.md" paths unchanged so diagnostics are not hidden by redundant fixes.
73
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT REQ-ERROR-SPECIFICITY - Enforces correctness of the story identifier by recognizing already valid ".story.md" paths without altering them.
73
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT REQ-ERROR-SPECIFICITY - Enforces correctness of the story identifier by recognizing already valid ".story.md" paths without altering them.
74
74
  if (/\.story\.md$/.test(original)) {
75
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT
76
- // @implements docs/stories/008.0-DEV-AUTO-FIX.story.md REQ-AUTOFIX-FORMAT
77
- // @implements docs/stories/010.1-REQ-STORY-PATH-STRICTNESS.story.md REQ-AUTOFIX-FORMAT
75
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT
76
+ // @supports docs/stories/008.0-DEV-AUTO-FIX.story.md REQ-AUTOFIX-FORMAT
77
+ // @supports docs/stories/010.1-REQ-STORY-PATH-STRICTNESS.story.md REQ-AUTOFIX-FORMAT
78
78
  return null;
79
79
  }
80
80
  // @story docs/stories/008.0-DEV-AUTO-FIX.story.md | REQ-AUTOFIX-FORMAT REQ-AUTOFIX-PRESERVE - When ".story" is present but ".md" is missing, append only the extension without altering the base path.
81
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT REQ-ERROR-SPECIFICITY - Enforces correctness of the story identifier by completing a partially correct ".story" suffix to the canonical ".story.md".
81
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT REQ-ERROR-SPECIFICITY - Enforces correctness of the story identifier by completing a partially correct ".story" suffix to the canonical ".story.md".
82
82
  if (/\.story$/.test(original)) {
83
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT
84
- // @implements docs/stories/008.0-DEV-AUTO-FIX.story.md REQ-AUTOFIX-FORMAT REQ-AUTOFIX-PRESERVE
85
- // @implements docs/stories/010.2-REQ-STORY-PATH-AUTOFIX.story.md REQ-AUTOFIX-FORMAT
83
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT
84
+ // @supports docs/stories/008.0-DEV-AUTO-FIX.story.md REQ-AUTOFIX-FORMAT REQ-AUTOFIX-PRESERVE
85
+ // @supports docs/stories/010.2-REQ-STORY-PATH-AUTOFIX.story.md REQ-AUTOFIX-FORMAT
86
86
  return `${original}.md`;
87
87
  }
88
88
  // @story docs/stories/010.2-REQ-STORY-PATH-AUTOFIX.story.md | REQ-AUTOFIX-FORMAT REQ-AUTOFIX-PRESERVE - Normalize plain ".md" doc paths to ".story.md" while keeping the rest of the path intact.
89
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT REQ-ERROR-SPECIFICITY - Enforces correctness of the story identifier by transforming generic ".md" references into canonical ".story.md" story paths.
89
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT REQ-ERROR-SPECIFICITY - Enforces correctness of the story identifier by transforming generic ".md" references into canonical ".story.md" story paths.
90
90
  if (/\.md$/.test(original)) {
91
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT
92
- // @implements docs/stories/008.0-DEV-AUTO-FIX.story.md REQ-AUTOFIX-FORMAT REQ-AUTOFIX-PRESERVE
93
- // @implements docs/stories/010.2-REQ-STORY-PATH-AUTOFIX.story.md REQ-AUTOFIX-FORMAT
91
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT
92
+ // @supports docs/stories/008.0-DEV-AUTO-FIX.story.md REQ-AUTOFIX-FORMAT REQ-AUTOFIX-PRESERVE
93
+ // @supports docs/stories/010.2-REQ-STORY-PATH-AUTOFIX.story.md REQ-AUTOFIX-FORMAT
94
94
  return original.replace(/\.md$/, ".story.md");
95
95
  }
96
96
  // @story docs/stories/010.2-REQ-STORY-PATH-AUTOFIX.story.md | REQ-AUTOFIX-FORMAT REQ-AUTOFIX-PRESERVE REQ-AUTOFIX-SAFE - For bare paths with no extension, append ".story.md" as a canonical story reference without touching the directory.
97
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT REQ-ERROR-SPECIFICITY - Enforces presence and correctness of the story identifier by supplying the standard ".story.md" suffix when no extension is provided.
98
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT
99
- // @implements docs/stories/008.0-DEV-AUTO-FIX.story.md REQ-AUTOFIX-FORMAT REQ-AUTOFIX-PRESERVE REQ-AUTOFIX-SAFE
100
- // @implements docs/stories/010.2-REQ-STORY-PATH-AUTOFIX.story.md REQ-AUTOFIX-FORMAT
97
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT REQ-ERROR-SPECIFICITY - Enforces presence and correctness of the story identifier by supplying the standard ".story.md" suffix when no extension is provided.
98
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT
99
+ // @supports docs/stories/008.0-DEV-AUTO-FIX.story.md REQ-AUTOFIX-FORMAT REQ-AUTOFIX-PRESERVE REQ-AUTOFIX-SAFE
100
+ // @supports docs/stories/010.2-REQ-STORY-PATH-AUTOFIX.story.md REQ-AUTOFIX-FORMAT
101
101
  return `${original}.story.md`;
102
102
  }
103
103
  /**
@@ -112,14 +112,14 @@ function getFixedStoryPath(original) {
112
112
  function buildStoryErrorMessage(kind, value, options) {
113
113
  const example = options.storyExample || exports.STORY_EXAMPLE_PATH;
114
114
  // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md | REQ-ERROR-SPECIFICITY - Use a dedicated message variant when the @story value is completely missing.
115
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT REQ-ERROR-SPECIFICITY - Enforces presence of the story identifier by emitting a targeted message when the @story value is absent.
115
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT REQ-ERROR-SPECIFICITY - Enforces presence of the story identifier by emitting a targeted message when the @story value is absent.
116
116
  if (kind === "missing") {
117
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-ERROR-SPECIFICITY
118
- // @implements docs/stories/010.1-REQ-STORY-PATH-STRICTNESS.story.md REQ-ERROR-SPECIFICITY
117
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-ERROR-SPECIFICITY
118
+ // @supports docs/stories/010.1-REQ-STORY-PATH-STRICTNESS.story.md REQ-ERROR-SPECIFICITY
119
119
  return `Missing story path for @story annotation. Expected a path like "${example}".`;
120
120
  }
121
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-ERROR-SPECIFICITY
122
- // @implements docs/stories/010.1-REQ-STORY-PATH-STRICTNESS.story.md REQ-ERROR-SPECIFICITY
121
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-ERROR-SPECIFICITY
122
+ // @supports docs/stories/010.1-REQ-STORY-PATH-STRICTNESS.story.md REQ-ERROR-SPECIFICITY
123
123
  return `Invalid story path "${value ?? ""}" for @story annotation. Expected a path like "${example}".`;
124
124
  }
125
125
  /**
@@ -134,13 +134,13 @@ function buildStoryErrorMessage(kind, value, options) {
134
134
  function buildReqErrorMessage(kind, value, options) {
135
135
  const example = options.reqExample || (0, valid_annotation_options_1.getDefaultReqExample)();
136
136
  // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md | REQ-ERROR-SPECIFICITY - Distinguish a completely missing @req from one that is present but malformed.
137
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT REQ-ERROR-SPECIFICITY - Enforces presence of the requirement identifier by emitting a specific message when the @req value is missing.
137
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-REQ-FORMAT REQ-ERROR-SPECIFICITY - Enforces presence of the requirement identifier by emitting a specific message when the @req value is missing.
138
138
  if (kind === "missing") {
139
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-ERROR-SPECIFICITY
140
- // @implements docs/stories/010.1-REQ-STORY-PATH-STRICTNESS.story.md REQ-ERROR-SPECIFICITY
139
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-ERROR-SPECIFICITY
140
+ // @supports docs/stories/010.1-REQ-STORY-PATH-STRICTNESS.story.md REQ-ERROR-SPECIFICITY
141
141
  return `Missing requirement ID for @req annotation. Expected an identifier like "${example}".`;
142
142
  }
143
- // @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-ERROR-SPECIFICITY
144
- // @implements docs/stories/010.1-REQ-STORY-PATH-STRICTNESS.story.md REQ-ERROR-SPECIFICITY
143
+ // @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-ERROR-SPECIFICITY
144
+ // @supports docs/stories/010.1-REQ-STORY-PATH-STRICTNESS.story.md REQ-ERROR-SPECIFICITY
145
145
  return `Invalid requirement ID "${value ?? ""}" for @req annotation. Expected an identifier like "${example}" (uppercase letters, numbers, and dashes only).`;
146
146
  }
@@ -20,14 +20,14 @@ function analyzeCandidateBoundaries(candidates, cwd) {
20
20
  let hasInProjectCandidate = false;
21
21
  let hasOutOfProjectCandidate = false;
22
22
  for (const candidate of candidates) {
23
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
23
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
24
24
  const boundary = (0, storyReferenceUtils_1.enforceProjectBoundary)(candidate, cwd);
25
25
  if (boundary.isWithinProject) {
26
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY
26
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY
27
27
  hasInProjectCandidate = true;
28
28
  }
29
29
  else {
30
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY
30
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY
31
31
  hasOutOfProjectCandidate = true;
32
32
  }
33
33
  }
@@ -47,12 +47,12 @@ function analyzeCandidateBoundaries(candidates, cwd) {
47
47
  */
48
48
  function handleProjectBoundaryForExistence({ storyPath, commentNode, context, cwd, candidates, existenceResult, reportInvalidPath, }) {
49
49
  if (candidates.length > 0) {
50
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
51
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
50
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
51
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
52
52
  const { hasInProjectCandidate, hasOutOfProjectCandidate } = analyzeCandidateBoundaries(candidates, cwd);
53
53
  if (hasOutOfProjectCandidate && !hasInProjectCandidate) {
54
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
55
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY
54
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
55
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY
56
56
  reportInvalidPath({ storyPath, commentNode, context });
57
57
  return true;
58
58
  }
@@ -60,12 +60,12 @@ function handleProjectBoundaryForExistence({ storyPath, commentNode, context, cw
60
60
  if (existenceResult &&
61
61
  existenceResult.status === "exists" &&
62
62
  existenceResult.matchedPath) {
63
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
64
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY
63
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
64
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY
65
65
  const boundary = (0, storyReferenceUtils_1.enforceProjectBoundary)(existenceResult.matchedPath, cwd);
66
66
  if (!boundary.isWithinProject) {
67
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
68
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY
67
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
68
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY
69
69
  reportInvalidPath({ storyPath, commentNode, context });
70
70
  return true;
71
71
  }
@@ -83,11 +83,11 @@ function handleProjectBoundaryForExistence({ storyPath, commentNode, context, cw
83
83
  function performSecurityValidations({ storyPath, commentNode, context, cwd, allowAbsolute, reportInvalidPath, }) {
84
84
  // Absolute path check
85
85
  if (path_1.default.isAbsolute(storyPath)) {
86
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
87
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-SECURITY-VALIDATION
86
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
87
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-SECURITY-VALIDATION
88
88
  if (!allowAbsolute) {
89
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
90
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-SECURITY-VALIDATION
89
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
90
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-SECURITY-VALIDATION
91
91
  reportInvalidPath({ storyPath, commentNode, context });
92
92
  return false;
93
93
  }
@@ -97,12 +97,12 @@ function performSecurityValidations({ storyPath, commentNode, context, cwd, allo
97
97
  // Path traversal check
98
98
  const containsTraversal = storyPath.includes("..") || /\\|\//.test(storyPath);
99
99
  if (containsTraversal) {
100
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
101
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-SECURITY-VALIDATION
100
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
101
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-SECURITY-VALIDATION
102
102
  const full = path_1.default.resolve(cwd, path_1.default.normalize(storyPath));
103
103
  if (!full.startsWith(cwd + path_1.default.sep)) {
104
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
105
- // @implements docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-SECURITY-VALIDATION
104
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY REQ-SECURITY-VALIDATION
105
+ // @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-SECURITY-VALIDATION
106
106
  reportInvalidPath({ storyPath, commentNode, context });
107
107
  return false;
108
108
  }
@@ -12,6 +12,14 @@ const MIN_STORY_TOKENS = 2;
12
12
  const MIN_REQ_TOKENS = MIN_STORY_TOKENS;
13
13
  // Length of the opening "/*" portion of a block comment prefix.
14
14
  const COMMENT_PREFIX_LENGTH = 2;
15
+ /**
16
+ * Collect line indices and metadata for @story and @req annotations within a
17
+ * single block comment. This helper isolates the parsing logic used by the
18
+ * auto-fix path so that complex or ambiguous patterns can be detected and
19
+ * safely rejected.
20
+ *
21
+ * @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-AUTO-FIX REQ-SINGLE-STORY-FIX REQ-VALID-OUTPUT
22
+ */
15
23
  function collectStoryAndReqMetadata(comment) {
16
24
  const rawValue = comment.value || "";
17
25
  const rawLines = rawValue.split(/\r?\n/);
@@ -52,6 +60,13 @@ function collectStoryAndReqMetadata(comment) {
52
60
  });
53
61
  return { storyLineIndices, reqLineIndices, reqIds, storyPath };
54
62
  }
63
+ /**
64
+ * Apply the @supports replacement for simple, single-story legacy blocks,
65
+ * constructing a fixed comment body that preserves existing indentation and
66
+ * prefix formatting while removing the original @story/@req lines.
67
+ *
68
+ * @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-AUTO-FIX REQ-SINGLE-STORY-FIX REQ-PRESERVE-FORMAT REQ-VALID-OUTPUT
69
+ */
55
70
  function applyImplementsReplacement(context, comment, details) {
56
71
  const { storyIdx, allIndicesToRemove, storyPath, reqIds } = details;
57
72
  const rawValue = comment.value || "";
@@ -98,7 +113,7 @@ function applyImplementsReplacement(context, comment, details) {
98
113
  * More complex patterns remain diagnostics-only with no fix to avoid
99
114
  * producing invalid or ambiguous output.
100
115
  *
101
- * @implements docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md
116
+ * @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md
102
117
  * @req REQ-AUTO-FIX - Provide safe, opt-in auto-fix for simple legacy patterns
103
118
  * @req REQ-SINGLE-STORY-FIX - Restrict auto-fix to single-story, single-path cases
104
119
  * @req REQ-PRESERVE-FORMAT - Preserve original JSDoc indentation and prefix formatting
@@ -126,6 +141,12 @@ function buildImplementsAutoFix(context, comment, storyPaths) {
126
141
  reqIds,
127
142
  });
128
143
  }
144
+ /**
145
+ * Analyze a block comment to detect legacy @story/@req usage, existing
146
+ * @supports lines, and the presence of multiple distinct @story paths.
147
+ *
148
+ * @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-OPTIONAL-WARNING REQ-MULTI-STORY-DETECT
149
+ */
129
150
  function analyzeComment(comment) {
130
151
  const rawLines = (comment.value || "").split(/\r?\n/);
131
152
  let hasStory = false;
@@ -158,6 +179,13 @@ function hasMultipleStories(storyPaths) {
158
179
  // @req REQ-MULTI-STORY-DETECT - Use named threshold constant instead of a magic number
159
180
  return storyPaths.size > MULTI_STORY_THRESHOLD;
160
181
  }
182
+ /**
183
+ * End-to-end processing for a single block comment: classify its
184
+ * traceability annotations, decide whether to report recommendations only
185
+ * or emit an auto-fix, and surface the appropriate message ID.
186
+ *
187
+ * @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-OPTIONAL-WARNING REQ-MULTI-STORY-DETECT REQ-AUTO-FIX REQ-VALID-OUTPUT
188
+ */
161
189
  function processComment(comment, context) {
162
190
  const { hasStory, hasReq, hasImplements, storyPaths } = analyzeComment(comment);
163
191
  if (!hasStory || !hasReq) {
@@ -45,6 +45,9 @@ const rule = {
45
45
  uniqueItems: true,
46
46
  },
47
47
  exportPriority: { type: "string", enum: require_story_helpers_1.EXPORT_PRIORITY_VALUES },
48
+ annotationTemplate: { type: "string" },
49
+ methodAnnotationTemplate: { type: "string" },
50
+ autoFix: { type: "boolean" },
48
51
  },
49
52
  additionalProperties: false,
50
53
  },
@@ -63,6 +66,15 @@ const rule = {
63
66
  const opts = (context.options && context.options[0]) || {};
64
67
  const scope = opts.scope || require_story_helpers_1.DEFAULT_SCOPE;
65
68
  const exportPriority = opts.exportPriority || "all";
69
+ const annotationTemplate = typeof opts.annotationTemplate === "string" &&
70
+ opts.annotationTemplate.trim().length > 0
71
+ ? opts.annotationTemplate.trim()
72
+ : undefined;
73
+ const methodAnnotationTemplate = typeof opts.methodAnnotationTemplate === "string" &&
74
+ opts.methodAnnotationTemplate.trim().length > 0
75
+ ? opts.methodAnnotationTemplate.trim()
76
+ : undefined;
77
+ const autoFix = typeof opts.autoFix === "boolean" ? opts.autoFix : true;
66
78
  /**
67
79
  * Optional debug logging for troubleshooting this rule.
68
80
  * Developers can temporarily uncomment the block below to log when the rule
@@ -84,6 +96,9 @@ const rule = {
84
96
  shouldProcessNode: should,
85
97
  scope,
86
98
  exportPriority,
99
+ annotationTemplate,
100
+ methodAnnotationTemplate,
101
+ autoFix,
87
102
  });
88
103
  },
89
104
  };
@@ -37,12 +37,7 @@ const rule = {
37
37
  testFilePatterns: {
38
38
  type: "array",
39
39
  items: { type: "string" },
40
- default: [
41
- "**/tests/**/*.test.{js,ts}",
42
- "**/tests/**/*.spec.{js,ts}",
43
- "**/__tests__/**/*.{js,ts}",
44
- "**/*.{test,spec}.{js,ts}",
45
- ],
40
+ default: ["/tests/", "/test/", "/__tests__", ".test.", ".spec."],
46
41
  },
47
42
  requireDescribeStory: {
48
43
  type: "boolean",
@@ -76,7 +76,7 @@ function getFixTargetNode(node) {
76
76
  * Returned function is a proper named function so no inline arrow is used.
77
77
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
78
78
  * @req REQ-ANNOTATION-AUTOFIX - Provide autofix for missing @req annotation
79
- * @implements docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-AUTOFIX REQ-ANNOTATION-REPORTING
79
+ * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-AUTOFIX REQ-ANNOTATION-REPORTING
80
80
  */
81
81
  function createMissingReqFix(node) {
82
82
  const target = getFixTargetNode(node);
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ /**
37
+ * CLI-level performance tests for maintenance tools on large workspaces.
38
+ * @supports docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md REQ-MAINT-DETECT REQ-MAINT-REPORT REQ-MAINT-SAFE
39
+ */
40
+ const fs = __importStar(require("fs"));
41
+ const os = __importStar(require("os"));
42
+ const path = __importStar(require("path"));
43
+ const perf_hooks_1 = require("perf_hooks");
44
+ const cli_1 = require("../../src/maintenance/cli");
45
+ function createCliLargeWorkspace() {
46
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "traceability-cli-large-"));
47
+ // Create a modestly sized workspace reusing the same shape as the core perf tests,
48
+ // but with fewer files to keep end-to-end CLI timing predictable.
49
+ for (let moduleIndex = 0; moduleIndex < 5; moduleIndex += 1) {
50
+ const moduleDir = path.join(root, `module-${moduleIndex.toString().padStart(3, "0")}`);
51
+ fs.mkdirSync(moduleDir);
52
+ for (let fileIndex = 0; fileIndex < 20; fileIndex += 1) {
53
+ const filePath = path.join(moduleDir, `file-${fileIndex.toString().padStart(3, "0")}.ts`);
54
+ const validStory = "cli-valid.story.md";
55
+ const staleStory = "cli-stale.story.md";
56
+ const content = `/**
57
+ * @story ${validStory}
58
+ * @story ${staleStory}
59
+ */
60
+ export function cli_example_${moduleIndex}_${fileIndex}() {}
61
+ `;
62
+ fs.writeFileSync(filePath, content, "utf8");
63
+ }
64
+ }
65
+ // Create the valid story file so that only the stale entries are reported.
66
+ fs.writeFileSync(path.join(root, "cli-valid.story.md"), "# cli valid", "utf8");
67
+ return {
68
+ root,
69
+ cleanup: () => {
70
+ fs.rmSync(root, { recursive: true, force: true });
71
+ },
72
+ };
73
+ }
74
+ 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
+ it("[REQ-MAINT-DETECT] detect --json completes within a generous time budget and returns JSON payload", () => {
87
+ 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();
107
+ });
108
+ it("[REQ-MAINT-REPORT] report --format=json completes within a generous time budget", () => {
109
+ 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();
129
+ });
130
+ });