eslint-plugin-traceability 1.26.0 → 1.28.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
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
# [1.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
### Bug Fixes
|
|
5
|
-
|
|
6
|
-
* **tests:** update performance test for new return types ([6db924f](https://github.com/voder-ai/eslint-plugin-traceability/commit/6db924f7af3c4feadf9528b557a3948a2e89bb19))
|
|
1
|
+
# [1.28.0](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.27.0...v1.28.0) (2026-01-12)
|
|
7
2
|
|
|
8
3
|
|
|
9
4
|
### Features
|
|
10
5
|
|
|
11
|
-
*
|
|
6
|
+
* implement [@req](https://github.com/req) mismatch detection in migration rule ([0fa4f70](https://github.com/voder-ai/eslint-plugin-traceability/commit/0fa4f70ef41525b7414083ff6094e214639ea5c6)), closes [REQ-MULTI-STORY-DETECT#1](https://github.com/REQ-MULTI-STORY-DETECT/issues/1)
|
|
12
7
|
|
|
13
8
|
# Changelog
|
|
14
9
|
|
|
@@ -43,6 +43,13 @@ export interface AnnotationRuleOptions {
|
|
|
43
43
|
* Human-readable example requirement ID used in error messages.
|
|
44
44
|
*/
|
|
45
45
|
requirementIdExample?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Story directories to validate against. When provided, derives storyPathPattern
|
|
48
|
+
* to match files within these directories if no explicit pattern is configured.
|
|
49
|
+
* Aligns with valid-story-reference configuration.
|
|
50
|
+
* Default: ["docs/stories", "stories"]
|
|
51
|
+
*/
|
|
52
|
+
storyDirectories?: string[];
|
|
46
53
|
/**
|
|
47
54
|
* Global toggle for auto-fix behavior in valid-annotation-format.
|
|
48
55
|
* When false, no automatic suffix-normalization fixes are applied.
|
|
@@ -140,6 +147,12 @@ export declare function getRuleSchema(): {
|
|
|
140
147
|
requirementIdExample: {
|
|
141
148
|
type: string;
|
|
142
149
|
};
|
|
150
|
+
storyDirectories: {
|
|
151
|
+
type: string;
|
|
152
|
+
items: {
|
|
153
|
+
type: string;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
143
156
|
autoFix: {
|
|
144
157
|
type: string;
|
|
145
158
|
};
|
|
@@ -16,6 +16,29 @@ exports.getRuleSchema = getRuleSchema;
|
|
|
16
16
|
* @req REQ-SCHEMA-VALIDATION - Use JSON Schema to validate configuration options
|
|
17
17
|
*/
|
|
18
18
|
const pattern_validators_1 = require("./pattern-validators");
|
|
19
|
+
/**
|
|
20
|
+
* Derive a story path pattern from configured story directories.
|
|
21
|
+
* Creates a pattern that matches files within any of the provided directories.
|
|
22
|
+
*
|
|
23
|
+
* @story docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md
|
|
24
|
+
*/
|
|
25
|
+
function deriveStoryPatternFromDirectories(dirs) {
|
|
26
|
+
// Escape special regex characters in directory paths
|
|
27
|
+
const escapedDirs = dirs.map((dir) => dir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
28
|
+
// Create alternation pattern: (dir1|dir2|...)/filename.story.md
|
|
29
|
+
const dirsPattern = escapedDirs.length === 1 ? escapedDirs[0] : `(${escapedDirs.join("|")})`;
|
|
30
|
+
return new RegExp(String.raw `^${dirsPattern}/[0-9]+\.[0-9]+-DEV-[\w-]+\.story\.md$`);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Derive a story path example from configured story directories.
|
|
34
|
+
* Uses the first directory in the list to create an example path.
|
|
35
|
+
*
|
|
36
|
+
* @story docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md
|
|
37
|
+
*/
|
|
38
|
+
function deriveStoryExampleFromDirectories(dirs) {
|
|
39
|
+
const firstDir = dirs[0] || "docs/stories";
|
|
40
|
+
return `${firstDir}/005.0-DEV-EXAMPLE.story.md`;
|
|
41
|
+
}
|
|
19
42
|
/**
|
|
20
43
|
* Get the default regular expression used to validate story paths.
|
|
21
44
|
*
|
|
@@ -98,21 +121,31 @@ function getOptionErrors() {
|
|
|
98
121
|
/**
|
|
99
122
|
* Resolve the story path pattern from nested or flat configuration
|
|
100
123
|
* fields, validating and falling back to the default as needed.
|
|
124
|
+
* If storyDirectories is provided but no explicit pattern, derives pattern from directories.
|
|
101
125
|
*
|
|
102
126
|
* @story docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md
|
|
103
127
|
* @req REQ-PATTERN-CONFIG - Allow configurable story path patterns
|
|
104
128
|
* @req REQ-REGEX-VALIDATION - Validate story path regex options
|
|
105
129
|
* @req REQ-BACKWARD-COMPAT - Use a default when no pattern is provided
|
|
106
130
|
*/
|
|
107
|
-
function resolveStoryPattern(nestedStoryPattern, flatStoryPattern) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
131
|
+
function resolveStoryPattern(nestedStoryPattern, flatStoryPattern, storyDirectories) {
|
|
132
|
+
// If an explicit pattern is provided (nested or flat), use it
|
|
133
|
+
if (nestedStoryPattern || flatStoryPattern) {
|
|
134
|
+
return (0, pattern_validators_1.resolvePattern)({
|
|
135
|
+
nestedPattern: nestedStoryPattern,
|
|
136
|
+
nestedFieldName: "story.pattern",
|
|
137
|
+
flatPattern: flatStoryPattern,
|
|
138
|
+
flatFieldName: "storyPathPattern",
|
|
139
|
+
defaultPattern: getDefaultStoryPattern(),
|
|
140
|
+
errors: optionErrors,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// If storyDirectories is provided, derive pattern from it
|
|
144
|
+
if (storyDirectories && storyDirectories.length > 0) {
|
|
145
|
+
return deriveStoryPatternFromDirectories(storyDirectories);
|
|
146
|
+
}
|
|
147
|
+
// Otherwise, use the default pattern
|
|
148
|
+
return getDefaultStoryPattern();
|
|
116
149
|
}
|
|
117
150
|
/**
|
|
118
151
|
* Resolve the requirement ID pattern from nested or flat configuration
|
|
@@ -141,8 +174,17 @@ function resolveReqPattern(nestedReqPattern, flatReqPattern) {
|
|
|
141
174
|
* @req REQ-EXAMPLE-MESSAGES - Allow custom story examples in messages
|
|
142
175
|
* @req REQ-BACKWARD-COMPAT - Use a default story example when omitted
|
|
143
176
|
*/
|
|
144
|
-
function resolveStoryExample(nestedStoryExample, flatStoryExample) {
|
|
145
|
-
|
|
177
|
+
function resolveStoryExample(nestedStoryExample, flatStoryExample, storyDirectories) {
|
|
178
|
+
// If an explicit example is provided, use it
|
|
179
|
+
if (nestedStoryExample || flatStoryExample) {
|
|
180
|
+
return (0, pattern_validators_1.resolveExample)(nestedStoryExample, flatStoryExample, getDefaultStoryExample());
|
|
181
|
+
}
|
|
182
|
+
// If storyDirectories is provided, derive example from it
|
|
183
|
+
if (storyDirectories && storyDirectories.length > 0) {
|
|
184
|
+
return deriveStoryExampleFromDirectories(storyDirectories);
|
|
185
|
+
}
|
|
186
|
+
// Otherwise, use the default example
|
|
187
|
+
return getDefaultStoryExample();
|
|
146
188
|
}
|
|
147
189
|
/**
|
|
148
190
|
* Resolve the requirement ID example string from nested or flat configuration
|
|
@@ -227,9 +269,9 @@ function resolveOptionsInternal(user) {
|
|
|
227
269
|
const { nestedReqExample, flatReqExample } = getReqExampleInputs(user);
|
|
228
270
|
const autoFixFlag = user?.autoFix;
|
|
229
271
|
const autoFix = typeof autoFixFlag === "boolean" ? autoFixFlag : true;
|
|
230
|
-
const storyPattern = resolveStoryPattern(nestedStoryPattern, flatStoryPattern);
|
|
272
|
+
const storyPattern = resolveStoryPattern(nestedStoryPattern, flatStoryPattern, user?.storyDirectories);
|
|
231
273
|
const reqPattern = resolveReqPattern(nestedReqPattern, flatReqPattern);
|
|
232
|
-
const storyExample = resolveStoryExample(nestedStoryExample, flatStoryExample);
|
|
274
|
+
const storyExample = resolveStoryExample(nestedStoryExample, flatStoryExample, user?.storyDirectories);
|
|
233
275
|
const reqExample = resolveReqExample(nestedReqExample, flatReqExample);
|
|
234
276
|
return {
|
|
235
277
|
storyPattern,
|
|
@@ -290,6 +332,7 @@ function getRuleSchema() {
|
|
|
290
332
|
storyPathExample: { type: "string" },
|
|
291
333
|
requirementIdPattern: { type: "string" },
|
|
292
334
|
requirementIdExample: { type: "string" },
|
|
335
|
+
storyDirectories: { type: "array", items: { type: "string" } },
|
|
293
336
|
autoFix: { type: "boolean" },
|
|
294
337
|
},
|
|
295
338
|
additionalProperties: false,
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
3
8
|
const valid_annotation_format_internal_1 = require("./helpers/valid-annotation-format-internal");
|
|
4
9
|
const prefer_implements_inline_1 = require("./helpers/prefer-implements-inline");
|
|
10
|
+
// Module-level cache for story file requirement IDs
|
|
11
|
+
// Cleared between ESLint runs, reused within a single lint execution
|
|
12
|
+
// @supports prompts/010.3-prefer-supports-req-mismatch-detection.md REQ-MISMATCH-DETECTION
|
|
13
|
+
const storyFileCache = new Map();
|
|
5
14
|
// Maximum number of distinct @story paths allowed before treating as "multi-story".
|
|
6
15
|
// @req REQ-MULTI-STORY-DETECT - Centralized threshold constant for detecting multi-story patterns
|
|
7
16
|
const MULTI_STORY_THRESHOLD = 1;
|
|
@@ -13,6 +22,62 @@ const MIN_STORY_TOKENS = 2;
|
|
|
13
22
|
const MIN_REQ_TOKENS = MIN_STORY_TOKENS;
|
|
14
23
|
// Length of the opening "/*" portion of a block comment prefix.
|
|
15
24
|
const COMMENT_PREFIX_LENGTH = 2;
|
|
25
|
+
/**
|
|
26
|
+
* Extract requirement IDs defined in a story file.
|
|
27
|
+
* Supports multiple markdown formats used in story files:
|
|
28
|
+
* - Heading format: - **REQ-ID**: Description
|
|
29
|
+
* - Acceptance format: - [x] REQ-ID: Description
|
|
30
|
+
* - Code annotation format: @req REQ-ID
|
|
31
|
+
*
|
|
32
|
+
* Returns null if the story file cannot be found or read.
|
|
33
|
+
* Returns Set<string> of requirement IDs if successful (may be empty if no requirements found).
|
|
34
|
+
*
|
|
35
|
+
* Results are cached for the duration of the ESLint run to avoid repeated file I/O.
|
|
36
|
+
*
|
|
37
|
+
* @supports prompts/010.3-prefer-supports-req-mismatch-detection.md REQ-MISMATCH-DETECTION
|
|
38
|
+
* @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-MULTI-STORY-DETECT
|
|
39
|
+
*/
|
|
40
|
+
function extractRequirementsFromStory(storyPath, context) {
|
|
41
|
+
// Check cache first
|
|
42
|
+
if (storyFileCache.has(storyPath)) {
|
|
43
|
+
return storyFileCache.get(storyPath);
|
|
44
|
+
}
|
|
45
|
+
// Resolve story path relative to CWD
|
|
46
|
+
const cwd = context.getCwd ? context.getCwd() : process.cwd();
|
|
47
|
+
// Validate story path: no traversal or absolute paths
|
|
48
|
+
if (storyPath.includes("..") || path_1.default.isAbsolute(storyPath)) {
|
|
49
|
+
storyFileCache.set(storyPath, null);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const resolvedPath = path_1.default.resolve(cwd, storyPath);
|
|
53
|
+
// Ensure resolved path is within cwd (security check)
|
|
54
|
+
if (!resolvedPath.startsWith(cwd + path_1.default.sep) && resolvedPath !== cwd) {
|
|
55
|
+
storyFileCache.set(storyPath, null);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
// Read and parse story file
|
|
59
|
+
try {
|
|
60
|
+
const content = fs_1.default.readFileSync(resolvedPath, "utf8");
|
|
61
|
+
const found = new Set();
|
|
62
|
+
// Extract requirement IDs using regex pattern that matches:
|
|
63
|
+
// - **REQ-ID**: in markdown headings
|
|
64
|
+
// - [x] REQ-ID: in acceptance criteria
|
|
65
|
+
// - @req REQ-ID in code annotations
|
|
66
|
+
// - REQ-ID anywhere else in the file
|
|
67
|
+
const regex = /REQ-[A-Z0-9-]+/g;
|
|
68
|
+
let match;
|
|
69
|
+
while ((match = regex.exec(content)) !== null) {
|
|
70
|
+
found.add(match[0]);
|
|
71
|
+
}
|
|
72
|
+
storyFileCache.set(storyPath, found);
|
|
73
|
+
return found;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// File not found or read error
|
|
77
|
+
storyFileCache.set(storyPath, null);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
16
81
|
/**
|
|
17
82
|
* Collect line indices and metadata for @story and @req annotations within a
|
|
18
83
|
* single block comment. This helper isolates the parsing logic used by the
|
|
@@ -130,6 +195,23 @@ function buildImplementsAutoFix(context, comment, storyPaths) {
|
|
|
130
195
|
storyPath === null) {
|
|
131
196
|
return null;
|
|
132
197
|
}
|
|
198
|
+
// NEW: Validate @req IDs against story file content
|
|
199
|
+
// This implements REQ-MULTI-STORY-DETECT requirement to detect when
|
|
200
|
+
// @req IDs don't match the referenced @story
|
|
201
|
+
// @supports prompts/010.3-prefer-supports-req-mismatch-detection.md REQ-MISMATCH-DETECTION
|
|
202
|
+
// @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-MULTI-STORY-DETECT
|
|
203
|
+
const storyReqs = extractRequirementsFromStory(storyPath, context);
|
|
204
|
+
// If story file not found or unreadable, cannot safely auto-fix
|
|
205
|
+
if (storyReqs === null) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
// Check for mismatched @req IDs (IDs not defined in the story)
|
|
209
|
+
const mismatchedReqs = reqIds.filter((reqId) => !storyReqs.has(reqId));
|
|
210
|
+
// If any @req IDs don't match the story, cannot safely auto-fix
|
|
211
|
+
// This likely indicates a multi-story implementation that needs manual migration
|
|
212
|
+
if (mismatchedReqs.length > 0) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
133
215
|
const storyIdx = storyLineIndices[0];
|
|
134
216
|
const allIndicesToRemove = new Set([
|
|
135
217
|
...storyLineIndices,
|
|
@@ -187,6 +269,47 @@ function hasMultipleStories(storyPaths) {
|
|
|
187
269
|
// @req REQ-MULTI-STORY-DETECT - Use named threshold constant instead of a magic number
|
|
188
270
|
return storyPaths.size > MULTI_STORY_THRESHOLD;
|
|
189
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Check for and report @req ID mismatches when auto-fix is not available.
|
|
274
|
+
* Provides detailed error messages when @req IDs don't match the story file content.
|
|
275
|
+
*
|
|
276
|
+
* @supports prompts/010.3-prefer-supports-req-mismatch-detection.md REQ-MISMATCH-DETECTION
|
|
277
|
+
* @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-MULTI-STORY-DETECT
|
|
278
|
+
*/
|
|
279
|
+
function reportMismatchIfNeeded(comment, context) {
|
|
280
|
+
const { storyLineIndices, reqLineIndices, reqIds, storyPath } = collectStoryAndReqMetadata(comment);
|
|
281
|
+
// Only check for mismatch if we have valid structure
|
|
282
|
+
if (storyPath === null ||
|
|
283
|
+
storyLineIndices.length !== 1 ||
|
|
284
|
+
reqLineIndices.length < 1) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
const storyReqs = extractRequirementsFromStory(storyPath, context);
|
|
288
|
+
if (storyReqs === null) {
|
|
289
|
+
// Story file not found or unreadable
|
|
290
|
+
context.report({
|
|
291
|
+
node: comment,
|
|
292
|
+
messageId: "cannotAutoFix",
|
|
293
|
+
data: {
|
|
294
|
+
reason: `story file '${storyPath}' not found or cannot be read`,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
const mismatchedReqs = reqIds.filter((reqId) => !storyReqs.has(reqId));
|
|
300
|
+
if (mismatchedReqs.length > 0) {
|
|
301
|
+
// Found mismatched @req IDs
|
|
302
|
+
context.report({
|
|
303
|
+
node: comment,
|
|
304
|
+
messageId: "cannotAutoFix",
|
|
305
|
+
data: {
|
|
306
|
+
reason: `@req '${mismatchedReqs.join("', '")}' not found in story '${storyPath}'. This may indicate a multi-story implementation`,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
190
313
|
/**
|
|
191
314
|
* End-to-end processing for a single block comment: classify its
|
|
192
315
|
* traceability annotations, decide whether to report recommendations only
|
|
@@ -216,7 +339,16 @@ function processBlockComment(comment, context) {
|
|
|
216
339
|
});
|
|
217
340
|
return;
|
|
218
341
|
}
|
|
342
|
+
// Attempt to build auto-fix
|
|
343
|
+
// Will return null if story file not found or @req IDs don't match story
|
|
219
344
|
const fix = buildImplementsAutoFix(context, comment, storyPaths);
|
|
345
|
+
// If no fix available, check if it's due to mismatch and provide helpful message
|
|
346
|
+
if (fix === null) {
|
|
347
|
+
const reported = reportMismatchIfNeeded(comment, context);
|
|
348
|
+
if (reported) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
220
352
|
context.report({
|
|
221
353
|
node: comment,
|
|
222
354
|
messageId: "preferImplements",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-traceability",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.0",
|
|
4
4
|
"description": "A customizable ESLint plugin that enforces traceability annotations in your code, ensuring each implementation is linked to its requirement or test case.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
"lint:require-built-plugin": "npm run lint-plugin-guard",
|
|
29
29
|
"lint": "eslint --config eslint.config.js \"src/**/*.{js,ts}\" \"tests/**/*.{js,ts}\" --max-warnings=0",
|
|
30
30
|
"test": "jest --ci --bail",
|
|
31
|
-
"test:unit": "jest --ci --bail --testPathPatterns='tests/(rules|maintenance|utils|unit)' --testPathIgnorePatterns='integration'",
|
|
31
|
+
"test:unit": "jest --ci --bail --testPathPatterns='tests/(rules|maintenance|utils|unit)' --testPathIgnorePatterns='integration|e2e'",
|
|
32
32
|
"test:integration": "jest --ci --bail --testPathPatterns='tests/integration'",
|
|
33
|
+
"test:e2e": "jest --ci --bail --testPathPatterns='tests/e2e'",
|
|
33
34
|
"ci-verify": "npm run type-check && npm run lint && npm run format:check && npm run duplication && npm run check:traceability && npm test && npm run audit:ci && npm run safety:deps",
|
|
34
35
|
"ci-verify:full": "npm run check:traceability && npm run safety:deps && npm run audit:ci && npm run build && npm run smoke:runtime && npm run type-check && npm run lint-plugin-check && npm run lint -- --max-warnings=0 && npm run duplication && npm run test -- --coverage && npm run format:check && npm audit --omit=dev --audit-level=high && npm run audit:dev-high && npm run check:ci-artifacts",
|
|
35
36
|
"ci-verify:fast": "npm run type-check && npm run check:traceability && npm run duplication && jest --ci --bail --passWithNoTests --testPathPatterns 'tests/(rules|maintenance)'",
|
|
@@ -766,7 +766,7 @@ Generates a plain-text or JSON report of stale story references.
|
|
|
766
766
|
# Human-readable text report (default)
|
|
767
767
|
traceability-maint report --root .
|
|
768
768
|
|
|
769
|
-
# JSON
|
|
769
|
+
# Machine-readable JSON output for further analysis
|
|
770
770
|
traceability-maint report --root . --format json
|
|
771
771
|
```
|
|
772
772
|
|
|
@@ -847,7 +847,7 @@ If `--from` or `--to` is missing, the CLI prints an error, shows the help text,
|
|
|
847
847
|
}
|
|
848
848
|
```
|
|
849
849
|
|
|
850
|
-
|
|
850
|
+
Manual verification:
|
|
851
851
|
|
|
852
852
|
```bash
|
|
853
853
|
npm run traceability:verify
|