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.
- package/CHANGELOG.md +3 -2
- package/README.md +1 -0
- package/lib/src/maintenance/cli.js +12 -12
- package/lib/src/maintenance/detect.js +19 -19
- package/lib/src/rules/helpers/require-story-core.d.ts +2 -15
- package/lib/src/rules/helpers/require-story-core.js +4 -71
- package/lib/src/rules/helpers/require-story-helpers.d.ts +32 -8
- package/lib/src/rules/helpers/require-story-helpers.js +44 -15
- package/lib/src/rules/helpers/require-story-visitors.js +47 -6
- package/lib/src/rules/helpers/valid-annotation-format-internal.d.ts +11 -0
- package/lib/src/rules/helpers/valid-annotation-format-internal.js +21 -0
- package/lib/src/rules/helpers/valid-annotation-format-validators.d.ts +125 -0
- package/lib/src/rules/helpers/valid-annotation-format-validators.js +274 -0
- package/lib/src/rules/helpers/valid-annotation-options.d.ts +6 -0
- package/lib/src/rules/helpers/valid-annotation-options.js +4 -0
- package/lib/src/rules/helpers/valid-annotation-utils.js +31 -31
- package/lib/src/rules/helpers/valid-story-reference-helpers.js +19 -19
- package/lib/src/rules/prefer-implements-annotation.js +29 -1
- package/lib/src/rules/require-story-annotation.js +15 -0
- package/lib/src/rules/require-test-traceability.js +1 -6
- package/lib/src/rules/valid-annotation-format.js +10 -243
- package/lib/src/utils/annotation-checker.js +1 -1
- package/lib/tests/perf/maintenance-cli-large-workspace.test.d.ts +1 -0
- package/lib/tests/perf/maintenance-cli-large-workspace.test.js +130 -0
- package/lib/tests/perf/maintenance-large-workspace.test.d.ts +1 -0
- package/lib/tests/perf/maintenance-large-workspace.test.js +149 -0
- package/lib/tests/rules/auto-fix-behavior-008.test.js +23 -0
- package/lib/tests/rules/require-story-core.autofix.test.js +9 -3
- package/lib/tests/rules/require-story-core.test.js +13 -7
- package/lib/tests/rules/require-story-helpers-edgecases.test.js +1 -1
- package/lib/tests/rules/require-story-helpers.test.js +14 -8
- package/lib/tests/rules/valid-annotation-format.test.js +71 -0
- package/lib/tests/utils/require-story-core-test-helpers.d.ts +1 -1
- package/lib/tests/utils/require-story-core-test-helpers.js +16 -16
- package/lib/tests/utils/temp-dir-helpers.js +1 -1
- package/package.json +9 -2
- package/user-docs/api-reference.md +8 -4
- 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
|
-
// @
|
|
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
|
-
// @
|
|
26
|
+
// @supports docs/stories/006.0-DEV-FILE-VALIDATION.story.md REQ-PROJECT-BOUNDARY
|
|
27
27
|
hasInProjectCandidate = true;
|
|
28
28
|
}
|
|
29
29
|
else {
|
|
30
|
-
// @
|
|
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
|
-
// @
|
|
51
|
-
// @
|
|
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
|
-
// @
|
|
55
|
-
// @
|
|
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
|
-
// @
|
|
64
|
-
// @
|
|
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
|
-
// @
|
|
68
|
-
// @
|
|
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
|
-
// @
|
|
87
|
-
// @
|
|
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
|
-
// @
|
|
90
|
-
// @
|
|
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
|
-
// @
|
|
101
|
-
// @
|
|
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
|
-
// @
|
|
105
|
-
// @
|
|
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
|
-
* @
|
|
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
|
-
* @
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|