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.
- package/CHANGELOG.md +4 -3
- 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-validators.js +5 -1
- 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/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/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
|
@@ -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
|
-
|
|
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
|
-
// @
|
|
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
|
-
// @
|
|
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
|
-
// @
|
|
68
|
-
// @
|
|
69
|
-
// @
|
|
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
|
-
// @
|
|
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
|
-
// @
|
|
76
|
-
// @
|
|
77
|
-
// @
|
|
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
|
-
// @
|
|
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
|
-
// @
|
|
84
|
-
// @
|
|
85
|
-
// @
|
|
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
|
-
// @
|
|
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
|
-
// @
|
|
92
|
-
// @
|
|
93
|
-
// @
|
|
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
|
-
// @
|
|
98
|
-
// @
|
|
99
|
-
// @
|
|
100
|
-
// @
|
|
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
|
-
// @
|
|
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
|
-
// @
|
|
118
|
-
// @
|
|
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
|
-
// @
|
|
122
|
-
// @
|
|
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
|
-
// @
|
|
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
|
-
// @
|
|
140
|
-
// @
|
|
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
|
-
// @
|
|
144
|
-
// @
|
|
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
|
-
// @
|
|
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",
|
|
@@ -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 {};
|