eslint-plugin-traceability 1.10.0 → 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 (38) hide show
  1. package/CHANGELOG.md +3 -2
  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-internal.d.ts +11 -0
  11. package/lib/src/rules/helpers/valid-annotation-format-internal.js +21 -0
  12. package/lib/src/rules/helpers/valid-annotation-format-validators.d.ts +125 -0
  13. package/lib/src/rules/helpers/valid-annotation-format-validators.js +274 -0
  14. package/lib/src/rules/helpers/valid-annotation-options.d.ts +6 -0
  15. package/lib/src/rules/helpers/valid-annotation-options.js +4 -0
  16. package/lib/src/rules/helpers/valid-annotation-utils.js +31 -31
  17. package/lib/src/rules/helpers/valid-story-reference-helpers.js +19 -19
  18. package/lib/src/rules/prefer-implements-annotation.js +29 -1
  19. package/lib/src/rules/require-story-annotation.js +15 -0
  20. package/lib/src/rules/require-test-traceability.js +1 -6
  21. package/lib/src/rules/valid-annotation-format.js +10 -243
  22. package/lib/src/utils/annotation-checker.js +1 -1
  23. package/lib/tests/perf/maintenance-cli-large-workspace.test.d.ts +1 -0
  24. package/lib/tests/perf/maintenance-cli-large-workspace.test.js +130 -0
  25. package/lib/tests/perf/maintenance-large-workspace.test.d.ts +1 -0
  26. package/lib/tests/perf/maintenance-large-workspace.test.js +149 -0
  27. package/lib/tests/rules/auto-fix-behavior-008.test.js +23 -0
  28. package/lib/tests/rules/require-story-core.autofix.test.js +9 -3
  29. package/lib/tests/rules/require-story-core.test.js +13 -7
  30. package/lib/tests/rules/require-story-helpers-edgecases.test.js +1 -1
  31. package/lib/tests/rules/require-story-helpers.test.js +14 -8
  32. package/lib/tests/rules/valid-annotation-format.test.js +71 -0
  33. package/lib/tests/utils/require-story-core-test-helpers.d.ts +1 -1
  34. package/lib/tests/utils/require-story-core-test-helpers.js +16 -16
  35. package/lib/tests/utils/temp-dir-helpers.js +1 -1
  36. package/package.json +9 -2
  37. package/user-docs/api-reference.md +8 -4
  38. package/user-docs/examples.md +42 -0
@@ -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",
@@ -1,247 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const valid_annotation_options_1 = require("./helpers/valid-annotation-options");
4
- const valid_annotation_utils_1 = require("./helpers/valid-annotation-utils");
5
- const valid_implements_utils_1 = require("./helpers/valid-implements-utils");
6
4
  const valid_annotation_format_internal_1 = require("./helpers/valid-annotation-format-internal");
7
- /**
8
- * Report an invalid @story annotation without applying a fix.
9
- *
10
- * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
11
- * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
12
- * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
13
- * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
14
- */
15
- function reportInvalidStoryFormat(context, comment, collapsed, options) {
16
- context.report({
17
- node: comment,
18
- messageId: "invalidStoryFormat",
19
- data: { details: (0, valid_annotation_utils_1.buildStoryErrorMessage)("invalid", collapsed, options) },
20
- });
21
- }
22
- /**
23
- * Compute the text replacement for an invalid @story annotation within a comment.
24
- *
25
- * This helper:
26
- * - finds the @story tag in the raw comment text,
27
- * - computes the character range of its value,
28
- * - and returns an ESLint fix that replaces only that range.
29
- *
30
- * Returns null when the tag or value cannot be safely located.
31
- *
32
- * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
33
- * @req REQ-AUTOFIX-SAFE
34
- * @req REQ-AUTOFIX-PRESERVE
35
- */
36
- function createStoryFix(context, comment, fixed) {
37
- const sourceCode = context.getSourceCode();
38
- const commentText = sourceCode.getText(comment);
39
- const search = "@story";
40
- const tagIndex = commentText.indexOf(search);
41
- // @story docs/stories/008.0-DEV-AUTO-FIX.story.md
42
- // @req REQ-AUTOFIX-SAFE - Skip auto-fix when @story tag cannot be reliably located
43
- if (tagIndex === valid_annotation_utils_1.TAG_NOT_FOUND_INDEX) {
44
- return null;
45
- }
46
- const afterTagIndex = tagIndex + search.length;
47
- const rest = commentText.slice(afterTagIndex);
48
- const valueMatch = rest.match(/[^\S\r\n]*([^\r\n*]+)/);
49
- // @story docs/stories/008.0-DEV-AUTO-FIX.story.md
50
- // @req REQ-AUTOFIX-SAFE - Abort auto-fix when story value range cannot be safely determined
51
- if (!valueMatch || valueMatch.index === undefined) {
52
- return null;
53
- }
54
- const valueStartInComment = afterTagIndex +
55
- valueMatch.index +
56
- (valueMatch[0].length - valueMatch[1].length);
57
- const valueEndInComment = valueStartInComment + valueMatch[1].length;
58
- const start = comment.range[0];
59
- const fixRange = [
60
- start + valueStartInComment,
61
- start + valueEndInComment,
62
- ];
63
- return () => (fixer) => fixer.replaceTextRange(fixRange, fixed);
64
- }
65
- /**
66
- * Report an invalid @story annotation and attempt a minimal, safe auto-fix
67
- * for common path suffix issues by locating and replacing the path text
68
- * within the original comment.
69
- *
70
- * This helper:
71
- * - only adjusts the story path suffix when a safe, well-understood
72
- * transformation is available, satisfying REQ-AUTOFIX-SAFE.
73
- * - preserves all surrounding comment formatting, spacing, and text
74
- * outside the path substring, satisfying REQ-AUTOFIX-PRESERVE.
75
- *
76
- * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
77
- * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
78
- * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
79
- * @req REQ-PATH-FORMAT - Validate @story paths follow expected patterns
80
- * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
81
- * @req REQ-AUTOFIX-SAFE - Auto-fix must be conservative and avoid changing semantics
82
- * @req REQ-AUTOFIX-PRESERVE - Auto-fix must preserve surrounding formatting and comments
83
- */
84
- function reportInvalidStoryFormatWithFix(context, comment, collapsed, fixed) {
85
- const fixFactory = createStoryFix(context, comment, fixed);
86
- // @story docs/stories/008.0-DEV-AUTO-FIX.story.md
87
- // @req REQ-AUTOFIX-SAFE - Fall back to reporting without fix when safe fix cannot be created
88
- if (!fixFactory) {
89
- reportInvalidStoryFormat(context, comment, collapsed, (0, valid_annotation_options_1.getResolvedDefaults)());
90
- return;
91
- }
92
- context.report({
93
- node: comment,
94
- messageId: "invalidStoryFormat",
95
- data: {
96
- details: (0, valid_annotation_utils_1.buildStoryErrorMessage)("invalid", collapsed, (0, valid_annotation_options_1.getResolvedDefaults)()),
97
- },
98
- fix: fixFactory(),
99
- });
100
- }
101
- /**
102
- * Validate a @story annotation value and report detailed errors when needed.
103
- * Where safe and unambiguous, apply an automatic fix for missing suffixes.
104
- *
105
- * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
106
- * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
107
- * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
108
- * @req REQ-PATH-FORMAT - Validate @story paths follow expected patterns
109
- * @req REQ-ERROR-SPECIFICITY - Provide specific error messages for different format violations
110
- * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
111
- * @req REQ-REGEX-VALIDATION - Validate configurable story regex patterns and fall back safely
112
- * @req REQ-BACKWARD-COMP - Preserve behavior when invalid regex config is supplied
113
- * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
114
- */
115
- function validateStoryAnnotation(context, comment, rawValue, options) {
116
- const trimmed = rawValue.trim();
117
- // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
118
- // @req REQ-PATH-FORMAT - Treat missing @story value as a specific validation error
119
- if (!trimmed) {
120
- context.report({
121
- node: comment,
122
- messageId: "invalidStoryFormat",
123
- data: { details: (0, valid_annotation_utils_1.buildStoryErrorMessage)("missing", null, options) },
124
- });
125
- return;
126
- }
127
- const collapsed = (0, valid_annotation_utils_1.collapseAnnotationValue)(trimmed);
128
- const pathPattern = options.storyPattern;
129
- // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
130
- // @req REQ-PATH-FORMAT - Accept @story value when it matches configured storyPattern
131
- if (pathPattern.test(collapsed)) {
132
- return;
133
- }
134
- // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
135
- // @req REQ-PATH-FORMAT - Reject @story values containing internal whitespace as invalid
136
- if (/\s/.test(trimmed)) {
137
- reportInvalidStoryFormat(context, comment, collapsed, options);
138
- return;
139
- }
140
- const fixed = (0, valid_annotation_utils_1.getFixedStoryPath)(collapsed);
141
- // @story docs/stories/008.0-DEV-AUTO-FIX.story.md
142
- // @req REQ-AUTOFIX-FORMAT - Apply suffix-only auto-fix when it yields a pattern-compliant path
143
- if (fixed && pathPattern.test(fixed)) {
144
- reportInvalidStoryFormatWithFix(context, comment, collapsed, fixed);
145
- return;
146
- }
147
- reportInvalidStoryFormat(context, comment, collapsed, options);
148
- }
149
- /**
150
- * Validate a @req annotation value and report detailed errors when needed.
151
- *
152
- * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
153
- * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
154
- * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
155
- * @req REQ-REQ-FORMAT - Validate @req identifiers follow expected patterns
156
- * @req REQ-ERROR-SPECIFICITY - Provide specific error messages for different format violations
157
- * @req REQ-REGEX-VALIDATION - Validate configurable requirement regex patterns and fall back safely
158
- * @req REQ-BACKWARD-COMP - Preserve behavior when invalid regex config is supplied
159
- * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
160
- */
161
- function validateReqAnnotation(context, comment, rawValue, options) {
162
- const trimmed = rawValue.trim();
163
- // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
164
- // @req REQ-REQ-FORMAT - Treat missing @req value as a specific validation error
165
- if (!trimmed) {
166
- context.report({
167
- node: comment,
168
- messageId: "invalidReqFormat",
169
- data: { details: (0, valid_annotation_utils_1.buildReqErrorMessage)("missing", null, options) },
170
- });
171
- return;
172
- }
173
- const collapsed = (0, valid_annotation_utils_1.collapseAnnotationValue)(trimmed);
174
- const reqPattern = options.reqPattern;
175
- // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
176
- // @req REQ-REQ-FORMAT - Flag @req identifiers that do not match the configured pattern
177
- if (!reqPattern.test(collapsed)) {
178
- context.report({
179
- node: comment,
180
- messageId: "invalidReqFormat",
181
- data: { details: (0, valid_annotation_utils_1.buildReqErrorMessage)("invalid", collapsed, options) },
182
- });
183
- }
184
- }
185
- /**
186
- * Validate an @supports annotation value and report detailed errors when needed.
187
- *
188
- * Expected format:
189
- * @supports <storyPath> <REQ-ID> [<REQ-ID> ...]
190
- *
191
- * Validation rules:
192
- * - Value must include at least a story path and one requirement ID.
193
- * - Story path must match the same storyPattern used for @story (no auto-fix).
194
- * - Each subsequent token must match reqPattern and is validated individually.
195
- *
196
- * Story path issues are reported with "invalidImplementsFormat" and
197
- * requirement ID issues reuse the existing "invalidReqFormat" message.
198
- *
199
- * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
200
- * @req REQ-SUPPORTS-PARSE - Parse @supports annotations without affecting @story/@req
201
- * @req REQ-FORMAT-VALIDATION - Validate @implements story path and requirement IDs
202
- * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
203
- */
204
- function validateImplementsAnnotation(context, comment, rawValue, options) {
205
- const deps = {
206
- MIN_IMPLEMENTS_TOKENS: valid_implements_utils_1.MIN_IMPLEMENTS_TOKENS,
207
- reportMissingImplementsReqIds: valid_implements_utils_1.reportMissingImplementsReqIds,
208
- reportMissingImplementsValue: valid_implements_utils_1.reportMissingImplementsValue,
209
- reportInvalidImplementsReqId: valid_implements_utils_1.reportInvalidImplementsReqId,
210
- reportInvalidImplementsStoryPath: valid_implements_utils_1.reportInvalidImplementsStoryPath,
211
- };
212
- (0, valid_implements_utils_1.validateImplementsAnnotationHelper)(deps, context, comment, {
213
- rawValue,
214
- options,
215
- });
216
- }
217
- /**
218
- * Finalize and validate the currently pending annotation, if any.
219
- *
220
- * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
221
- * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
222
- * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
223
- * @req REQ-SYNTAX-VALIDATION - Validate annotation syntax matches specification
224
- * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
225
- * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
226
- */
227
- function finalizePendingAnnotation(context, comment, options, pending) {
228
- // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
229
- // @req REQ-MULTILINE-SUPPORT - Do nothing when there is no pending multi-line annotation to finalize
230
- if (!pending) {
231
- return null;
232
- }
233
- // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
234
- // @req REQ-SYNTAX-VALIDATION - Dispatch to @story or @req validator based on pending annotation type
235
- // @req REQ-AUTOFIX-FORMAT - Route to story validator which may apply safe auto-fixes
236
- // @req REQ-MIXED-SUPPORT - Ensure @story and @req annotations are handled independently
237
- if (pending.type === "story") {
238
- validateStoryAnnotation(context, comment, pending.value, options);
239
- }
240
- else {
241
- validateReqAnnotation(context, comment, pending.value, options);
242
- }
243
- return null;
244
- }
5
+ const valid_annotation_format_validators_1 = require("./helpers/valid-annotation-format-validators");
245
6
  /**
246
7
  * Process a single normalized comment line and update the pending annotation state.
247
8
  *
@@ -269,7 +30,7 @@ function processCommentLine({ normalized, pending, context, comment, options, })
269
30
  // @req REQ-IMPLEMENTS-PARSE - Immediately validate @supports without starting multi-line state
270
31
  if (isImplements) {
271
32
  const implementsValue = normalized.replace(/^@supports\b/, "").trim();
272
- validateImplementsAnnotation(context, comment, implementsValue, options);
33
+ (0, valid_annotation_format_validators_1.validateImplementsAnnotation)(context, comment, implementsValue, options);
273
34
  return pending;
274
35
  }
275
36
  // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
@@ -279,7 +40,7 @@ function processCommentLine({ normalized, pending, context, comment, options, })
279
40
  // @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
280
41
  // @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
281
42
  if (isStory || isReq) {
282
- finalizePendingAnnotation(context, comment, options, pending);
43
+ (0, valid_annotation_format_validators_1.finalizePendingAnnotation)(context, comment, options, pending);
283
44
  const value = normalized.replace(/^@story\b|^@req\b/, "").trim();
284
45
  return {
285
46
  type: isStory ? "story" : "req",
@@ -287,6 +48,12 @@ function processCommentLine({ normalized, pending, context, comment, options, })
287
48
  hasValue: value.trim().length > 0,
288
49
  };
289
50
  }
51
+ // Implement JSDoc tag coexistence behavior: terminate @story/@req values when a new non-traceability JSDoc tag line (e.g., @param, @returns) is encountered.
52
+ // @supports docs/stories/022.0-DEV-JSDOC-COEXISTENCE.story.md REQ-ANNOTATION-TERMINATION REQ-CONTINUATION-LOGIC
53
+ if ((0, valid_annotation_format_internal_1.isNonTraceabilityJSDocTagLine)(normalized)) {
54
+ (0, valid_annotation_format_validators_1.finalizePendingAnnotation)(context, comment, options, pending);
55
+ return null;
56
+ }
290
57
  // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
291
58
  // @story docs/stories/008.0-DEV-AUTO-FIX.story.md
292
59
  // @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
@@ -344,7 +111,7 @@ function processComment(context, comment, options) {
344
111
  options,
345
112
  });
346
113
  });
347
- finalizePendingAnnotation(context, comment, options, pending);
114
+ (0, valid_annotation_format_validators_1.finalizePendingAnnotation)(context, comment, options, pending);
348
115
  }
349
116
  exports.default = {
350
117
  meta: {
@@ -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
+ });