eslint-plugin-traceability 1.1.0 → 1.1.2
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/lib/src/index.d.ts +80 -0
- package/lib/src/index.js +58 -0
- package/lib/src/maintenance/batch.d.ts +16 -0
- package/lib/src/maintenance/batch.js +28 -0
- package/lib/src/maintenance/detect.d.ts +6 -0
- package/lib/src/maintenance/detect.js +69 -0
- package/lib/src/maintenance/index.js +22 -0
- package/lib/src/maintenance/report.d.ts +7 -0
- package/lib/src/maintenance/report.js +17 -0
- package/lib/src/maintenance/update.d.ts +6 -0
- package/lib/src/maintenance/update.js +67 -0
- package/lib/src/maintenance/utils.d.ts +6 -0
- package/lib/src/maintenance/utils.js +64 -0
- package/lib/src/rules/require-branch-annotation.d.ts +7 -0
- package/lib/src/rules/require-branch-annotation.js +111 -0
- package/lib/src/rules/require-req-annotation.d.ts +7 -0
- package/lib/src/rules/require-req-annotation.js +38 -0
- package/lib/src/rules/require-story-annotation.d.ts +7 -0
- package/lib/src/rules/require-story-annotation.js +50 -0
- package/lib/src/rules/valid-annotation-format.d.ts +10 -0
- package/lib/src/rules/valid-annotation-format.js +60 -0
- package/lib/src/rules/valid-req-reference.d.ts +3 -0
- package/lib/src/rules/valid-req-reference.js +104 -0
- package/lib/src/rules/valid-story-reference.d.ts +3 -0
- package/lib/src/rules/valid-story-reference.js +168 -0
- package/lib/tests/fixtures/stale/example.d.ts +0 -0
- package/{tests/fixtures/stale/example.ts → lib/tests/fixtures/stale/example.js} +1 -0
- package/lib/tests/fixtures/update/example.d.ts +0 -0
- package/{tests/fixtures/update/example.ts → lib/tests/fixtures/update/example.js} +1 -0
- package/lib/tests/fixtures/valid-annotations/example.d.ts +0 -0
- package/{tests/fixtures/valid-annotations/example.ts → lib/tests/fixtures/valid-annotations/example.js} +1 -0
- package/lib/tests/maintenance/batch.test.d.ts +1 -0
- package/lib/tests/maintenance/batch.test.js +79 -0
- package/lib/tests/maintenance/detect-isolated.test.d.ts +1 -0
- package/lib/tests/maintenance/detect-isolated.test.js +95 -0
- package/lib/tests/maintenance/detect.test.d.ts +1 -0
- package/lib/tests/maintenance/detect.test.js +23 -0
- package/lib/tests/maintenance/report.test.d.ts +1 -0
- package/lib/tests/maintenance/report.test.js +67 -0
- package/lib/tests/maintenance/update-isolated.test.d.ts +1 -0
- package/lib/tests/maintenance/update-isolated.test.js +66 -0
- package/lib/tests/maintenance/update.test.d.ts +1 -0
- package/lib/tests/maintenance/update.test.js +26 -0
- package/lib/tests/plugin-default-export-and-configs.test.d.ts +1 -0
- package/lib/tests/plugin-default-export-and-configs.test.js +72 -0
- package/lib/tests/plugin-setup.test.d.ts +1 -0
- package/lib/tests/plugin-setup.test.js +51 -0
- package/lib/tests/rules/require-branch-annotation.test.d.ts +1 -0
- package/lib/tests/rules/require-branch-annotation.test.js +253 -0
- package/lib/tests/rules/require-req-annotation.test.d.ts +1 -0
- package/lib/tests/rules/require-req-annotation.test.js +41 -0
- package/lib/tests/rules/require-story-annotation.test.d.ts +1 -0
- package/lib/tests/rules/require-story-annotation.test.js +36 -0
- package/lib/tests/rules/valid-annotation-format.test.d.ts +1 -0
- package/lib/tests/rules/valid-annotation-format.test.js +58 -0
- package/lib/tests/rules/valid-req-reference.test.d.ts +1 -0
- package/lib/tests/rules/valid-req-reference.test.js +87 -0
- package/lib/tests/rules/valid-story-reference.test.d.ts +1 -0
- package/lib/tests/rules/valid-story-reference.test.js +69 -0
- package/package.json +6 -1
- package/.env.example +0 -6
- package/.github/workflows/ci-cd.yml +0 -107
- package/.husky/pre-commit +0 -1
- package/.husky/pre-push +0 -1
- package/.prettierignore +0 -27
- package/.prettierrc +0 -4
- package/.releaserc.json +0 -15
- package/.voder/history.md +0 -162
- package/.voder/implementation-progress.md +0 -144
- package/.voder/last-action.md +0 -83
- package/.voder/plan.md +0 -12
- package/.voder/progress-chart.png +0 -0
- package/.voder/progress-log-areas.csv +0 -39
- package/.voder/progress-log.csv +0 -38
- package/.voder/traceability/docs-stories-001.0-DEV-PLUGIN-SETUP.story.xml +0 -17
- package/.voder/traceability/docs-stories-002.0-DEV-ESLINT-CONFIG.story.xml +0 -13
- package/.voder/traceability/docs-stories-003.0-DEV-FUNCTION-ANNOTATIONS.story.xml +0 -9
- package/.voder/traceability/docs-stories-004.0-DEV-BRANCH-ANNOTATIONS.story.xml +0 -9
- package/.voder/traceability/docs-stories-005.0-DEV-ANNOTATION-VALIDATION.story.xml +0 -9
- package/.voder/traceability/docs-stories-006.0-DEV-FILE-VALIDATION.story.xml +0 -9
- package/.voder/traceability/docs-stories-007.0-DEV-ERROR-REPORTING.story.xml +0 -9
- package/.voder/traceability/docs-stories-008.0-DEV-AUTO-FIX.story.xml +0 -9
- package/.voder/traceability/docs-stories-009.0-DEV-MAINTENANCE-TOOLS.story.xml +0 -16
- package/.voder/traceability/docs-stories-010.0-DEV-DEEP-VALIDATION.story.xml +0 -11
- package/CHANGELOG.md +0 -57
- package/CONTRIBUTING.md +0 -99
- package/cli-integration.js +0 -103
- package/docs/cli-integration.md +0 -105
- package/docs/config-presets.md +0 -38
- package/docs/conventional-commits-guide.md +0 -185
- package/docs/decisions/001-typescript-for-eslint-plugin.accepted.md +0 -111
- package/docs/decisions/002-jest-for-eslint-testing.accepted.md +0 -137
- package/docs/decisions/003-code-quality-ratcheting-plan.md +0 -48
- package/docs/decisions/004-automated-version-bumping-for-ci-cd.md +0 -196
- package/docs/decisions/005-github-actions-validation-tooling.accepted.md +0 -144
- package/docs/decisions/006-semantic-release-for-automated-publishing.accepted.md +0 -227
- package/docs/eslint-9-setup-guide.md +0 -517
- package/docs/eslint-plugin-development-guide.md +0 -483
- package/docs/jest-testing-guide.md +0 -100
- package/docs/rules/require-branch-annotation.md +0 -34
- package/docs/rules/require-req-annotation.md +0 -39
- package/docs/rules/require-story-annotation.md +0 -36
- package/docs/rules/valid-annotation-format.md +0 -52
- package/docs/rules/valid-req-reference.md +0 -58
- package/docs/rules/valid-story-reference.md +0 -47
- package/docs/security-incidents/unresolved-vulnerabilities.md +0 -11
- package/docs/stories/001.0-DEV-PLUGIN-SETUP.story.md +0 -82
- package/docs/stories/002.0-DEV-ESLINT-CONFIG.story.md +0 -82
- package/docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md +0 -85
- package/docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md +0 -107
- package/docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md +0 -119
- package/docs/stories/006.0-DEV-FILE-VALIDATION.story.md +0 -127
- package/docs/stories/007.0-DEV-ERROR-REPORTING.story.md +0 -89
- package/docs/stories/008.0-DEV-AUTO-FIX.story.md +0 -104
- package/docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md +0 -104
- package/docs/stories/010.0-DEV-DEEP-VALIDATION.story.md +0 -110
- package/docs/stories/developer-story.map.md +0 -118
- package/eslint.config.js +0 -146
- package/jest.config.js +0 -21
- package/scripts/smoke-test.sh +0 -45
- package/src/index.ts +0 -56
- package/src/maintenance/batch.ts +0 -29
- package/src/maintenance/detect.ts +0 -42
- package/src/maintenance/report.ts +0 -15
- package/src/maintenance/update.ts +0 -40
- package/src/maintenance/utils.ts +0 -28
- package/src/rules/require-branch-annotation.ts +0 -114
- package/src/rules/require-req-annotation.ts +0 -36
- package/src/rules/require-story-annotation.ts +0 -52
- package/src/rules/valid-annotation-format.ts +0 -62
- package/src/rules/valid-req-reference.ts +0 -114
- package/src/rules/valid-story-reference.ts +0 -213
- package/tests/fixtures/story_bullet.md +0 -6
- package/tests/maintenance/batch.test.ts +0 -55
- package/tests/maintenance/detect-isolated.test.ts +0 -65
- package/tests/maintenance/detect.test.ts +0 -19
- package/tests/maintenance/report.test.ts +0 -37
- package/tests/maintenance/update-isolated.test.ts +0 -39
- package/tests/maintenance/update.test.ts +0 -21
- package/tests/plugin-default-export-and-configs.test.ts +0 -50
- package/tests/plugin-setup.test.ts +0 -17
- package/tests/rules/require-branch-annotation.test.ts +0 -250
- package/tests/rules/require-req-annotation.test.ts +0 -38
- package/tests/rules/require-story-annotation.test.ts +0 -33
- package/tests/rules/valid-annotation-format.test.ts +0 -55
- package/tests/rules/valid-req-reference.test.ts +0 -85
- package/tests/rules/valid-story-reference.test.ts +0 -66
- package/tsconfig.json +0 -15
- package/user-docs/api-reference.md +0 -135
- package/user-docs/examples.md +0 -73
- package/user-docs/migration-guide.md +0 -71
- /package/{src/maintenance/index.ts → lib/src/maintenance/index.d.ts} +0 -0
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rule to validate @story and @req annotation format and syntax
|
|
3
|
-
* @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
|
|
4
|
-
* @req REQ-FORMAT-SPECIFICATION - Define clear format rules for @story and @req annotations
|
|
5
|
-
* @req REQ-SYNTAX-VALIDATION - Validate annotation syntax matches specification
|
|
6
|
-
* @req REQ-PATH-FORMAT - Validate @story paths follow expected patterns
|
|
7
|
-
* @req REQ-REQ-FORMAT - Validate @req identifiers follow expected patterns
|
|
8
|
-
*/
|
|
9
|
-
export default {
|
|
10
|
-
meta: {
|
|
11
|
-
type: "problem",
|
|
12
|
-
docs: {
|
|
13
|
-
description: "Validate format and syntax of @story and @req annotations",
|
|
14
|
-
recommended: "error",
|
|
15
|
-
},
|
|
16
|
-
messages: {
|
|
17
|
-
invalidStoryFormat: "Invalid @story annotation format",
|
|
18
|
-
invalidReqFormat: "Invalid @req annotation format",
|
|
19
|
-
},
|
|
20
|
-
schema: [],
|
|
21
|
-
},
|
|
22
|
-
create(context: any) {
|
|
23
|
-
const sourceCode = context.getSourceCode();
|
|
24
|
-
return {
|
|
25
|
-
Program() {
|
|
26
|
-
const comments = sourceCode.getAllComments() || [];
|
|
27
|
-
comments.forEach((comment: any) => {
|
|
28
|
-
const lines = comment.value
|
|
29
|
-
.split(/\r?\n/)
|
|
30
|
-
.map((l: string) => l.replace(/^[^@]*/, "").trim());
|
|
31
|
-
lines.forEach((line: string) => {
|
|
32
|
-
if (line.startsWith("@story")) {
|
|
33
|
-
const parts = line.split(/\s+/);
|
|
34
|
-
const storyPath = parts[1];
|
|
35
|
-
if (
|
|
36
|
-
!storyPath ||
|
|
37
|
-
!/^docs\/stories\/[0-9]+\.[0-9]+-DEV-[\w-]+\.story\.md$/.test(
|
|
38
|
-
storyPath,
|
|
39
|
-
)
|
|
40
|
-
) {
|
|
41
|
-
context.report({
|
|
42
|
-
node: comment as any,
|
|
43
|
-
messageId: "invalidStoryFormat",
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
if (line.startsWith("@req")) {
|
|
48
|
-
const parts = line.split(/\s+/);
|
|
49
|
-
const reqId = parts[1];
|
|
50
|
-
if (!reqId || !/^REQ-[A-Z0-9-]+$/.test(reqId)) {
|
|
51
|
-
context.report({
|
|
52
|
-
node: comment as any,
|
|
53
|
-
messageId: "invalidReqFormat",
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
},
|
|
62
|
-
} as any;
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/* eslint-env node */
|
|
2
|
-
/**
|
|
3
|
-
* Rule to validate @req annotation references refer to existing requirements in story files
|
|
4
|
-
* @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
|
|
5
|
-
* @req REQ-DEEP-PARSE - Parse story files to extract requirement identifiers
|
|
6
|
-
* @req REQ-DEEP-MATCH - Validate @req references against story file content
|
|
7
|
-
* @req REQ-DEEP-CACHE - Cache parsed story content for performance
|
|
8
|
-
* @req REQ-DEEP-PATH - Protect against path traversal in story paths
|
|
9
|
-
*/
|
|
10
|
-
import fs from "fs";
|
|
11
|
-
import path from "path";
|
|
12
|
-
import type { Rule } from "eslint";
|
|
13
|
-
|
|
14
|
-
export default {
|
|
15
|
-
meta: {
|
|
16
|
-
type: "problem",
|
|
17
|
-
docs: {
|
|
18
|
-
description:
|
|
19
|
-
"Validate that @req annotations reference existing requirements in referenced story files",
|
|
20
|
-
recommended: "error",
|
|
21
|
-
},
|
|
22
|
-
messages: {
|
|
23
|
-
reqMissing: "Requirement '{{reqId}}' not found in '{{storyPath}}'",
|
|
24
|
-
invalidPath: "Invalid story path '{{storyPath}}'",
|
|
25
|
-
},
|
|
26
|
-
schema: [],
|
|
27
|
-
},
|
|
28
|
-
create(context) {
|
|
29
|
-
const sourceCode = context.getSourceCode();
|
|
30
|
-
const cwd = process.cwd();
|
|
31
|
-
|
|
32
|
-
// Cache for resolved story file paths to parsed set of requirement IDs
|
|
33
|
-
const reqCache = new Map<string, Set<string>>();
|
|
34
|
-
|
|
35
|
-
let rawStoryPath: string | null = null;
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
Program() {
|
|
39
|
-
const comments = sourceCode.getAllComments() || [];
|
|
40
|
-
comments.forEach((comment) => {
|
|
41
|
-
const rawLines = comment.value.split(/\r?\n/);
|
|
42
|
-
const lines = rawLines.map((rawLine) =>
|
|
43
|
-
rawLine.trim().replace(/^\*+\s*/, ""),
|
|
44
|
-
);
|
|
45
|
-
lines.forEach((line) => {
|
|
46
|
-
if (line.startsWith("@story")) {
|
|
47
|
-
const parts = line.split(/\s+/);
|
|
48
|
-
rawStoryPath = parts[1] || null;
|
|
49
|
-
}
|
|
50
|
-
if (line.startsWith("@req")) {
|
|
51
|
-
const parts = line.split(/\s+/);
|
|
52
|
-
const reqId = parts[1];
|
|
53
|
-
if (!reqId || !rawStoryPath) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Protect against path traversal and absolute paths
|
|
58
|
-
if (
|
|
59
|
-
rawStoryPath.includes("..") ||
|
|
60
|
-
path.isAbsolute(rawStoryPath)
|
|
61
|
-
) {
|
|
62
|
-
context.report({
|
|
63
|
-
node: comment as any,
|
|
64
|
-
messageId: "invalidPath",
|
|
65
|
-
data: { storyPath: rawStoryPath },
|
|
66
|
-
});
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const resolvedStoryPath = path.resolve(cwd, rawStoryPath);
|
|
71
|
-
if (
|
|
72
|
-
!resolvedStoryPath.startsWith(cwd + path.sep) &&
|
|
73
|
-
resolvedStoryPath !== cwd
|
|
74
|
-
) {
|
|
75
|
-
context.report({
|
|
76
|
-
node: comment as any,
|
|
77
|
-
messageId: "invalidPath",
|
|
78
|
-
data: { storyPath: rawStoryPath },
|
|
79
|
-
});
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Load and parse story file if not cached
|
|
84
|
-
if (!reqCache.has(resolvedStoryPath)) {
|
|
85
|
-
try {
|
|
86
|
-
const content = fs.readFileSync(resolvedStoryPath, "utf8");
|
|
87
|
-
const found = new Set<string>();
|
|
88
|
-
const regex = /REQ-[A-Z0-9-]+/g;
|
|
89
|
-
let match;
|
|
90
|
-
while ((match = regex.exec(content)) !== null) {
|
|
91
|
-
found.add(match[0]);
|
|
92
|
-
}
|
|
93
|
-
reqCache.set(resolvedStoryPath, found);
|
|
94
|
-
} catch {
|
|
95
|
-
// Unable to read file, treat as no requirements
|
|
96
|
-
reqCache.set(resolvedStoryPath, new Set());
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const reqSet = reqCache.get(resolvedStoryPath)!;
|
|
101
|
-
if (!reqSet.has(reqId)) {
|
|
102
|
-
context.report({
|
|
103
|
-
node: comment as any,
|
|
104
|
-
messageId: "reqMissing",
|
|
105
|
-
data: { reqId, storyPath: rawStoryPath },
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
},
|
|
114
|
-
} as Rule.RuleModule;
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
/* eslint-env node */
|
|
2
|
-
/**
|
|
3
|
-
* Rule to validate @story annotation references refer to existing story files
|
|
4
|
-
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
5
|
-
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
6
|
-
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
7
|
-
* @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
|
|
8
|
-
*/
|
|
9
|
-
import fs from "fs";
|
|
10
|
-
import path from "path";
|
|
11
|
-
import type { Rule } from "eslint";
|
|
12
|
-
|
|
13
|
-
const defaultStoryDirs = ["docs/stories", "stories"];
|
|
14
|
-
const fileExistCache = new Map<string, boolean>();
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Build possible file paths for a given storyPath.
|
|
18
|
-
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
19
|
-
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
20
|
-
*/
|
|
21
|
-
function buildCandidates(
|
|
22
|
-
storyPath: string,
|
|
23
|
-
cwd: string,
|
|
24
|
-
storyDirs: string[],
|
|
25
|
-
): string[] {
|
|
26
|
-
const candidates: string[] = [];
|
|
27
|
-
if (storyPath.startsWith("./") || storyPath.startsWith("../")) {
|
|
28
|
-
candidates.push(path.resolve(cwd, storyPath));
|
|
29
|
-
} else {
|
|
30
|
-
candidates.push(path.resolve(cwd, storyPath));
|
|
31
|
-
for (const dir of storyDirs) {
|
|
32
|
-
candidates.push(path.resolve(cwd, dir, path.basename(storyPath)));
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return candidates;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Check if any of the candidate files exist.
|
|
40
|
-
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
41
|
-
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
42
|
-
*/
|
|
43
|
-
function existsAny(paths: string[]): boolean {
|
|
44
|
-
for (const candidate of paths) {
|
|
45
|
-
let ok = fileExistCache.get(candidate);
|
|
46
|
-
if (ok === undefined) {
|
|
47
|
-
ok = fs.existsSync(candidate) && fs.statSync(candidate).isFile();
|
|
48
|
-
fileExistCache.set(candidate, ok);
|
|
49
|
-
}
|
|
50
|
-
if (ok) {
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Validate a single @story annotation line.
|
|
59
|
-
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
60
|
-
* @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
|
|
61
|
-
* @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
|
|
62
|
-
* @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
|
|
63
|
-
*/
|
|
64
|
-
function validateStoryPath(
|
|
65
|
-
line: string,
|
|
66
|
-
commentNode: any,
|
|
67
|
-
context: any,
|
|
68
|
-
cwd: string,
|
|
69
|
-
storyDirs: string[],
|
|
70
|
-
allowAbsolute: boolean,
|
|
71
|
-
requireExt: boolean,
|
|
72
|
-
): void {
|
|
73
|
-
const parts = line.split(/\s+/);
|
|
74
|
-
const storyPath = parts[1];
|
|
75
|
-
if (!storyPath) {
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
// Absolute path check
|
|
79
|
-
if (path.isAbsolute(storyPath)) {
|
|
80
|
-
if (!allowAbsolute) {
|
|
81
|
-
context.report({
|
|
82
|
-
node: commentNode,
|
|
83
|
-
messageId: "invalidPath",
|
|
84
|
-
data: { path: storyPath },
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
// Path traversal prevention
|
|
90
|
-
if (storyPath.includes("..")) {
|
|
91
|
-
const normalized = path.normalize(storyPath);
|
|
92
|
-
const full = path.resolve(cwd, normalized);
|
|
93
|
-
if (!full.startsWith(cwd + path.sep)) {
|
|
94
|
-
context.report({
|
|
95
|
-
node: commentNode,
|
|
96
|
-
messageId: "invalidPath",
|
|
97
|
-
data: { path: storyPath },
|
|
98
|
-
});
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
// Extension check
|
|
103
|
-
if (requireExt && !storyPath.endsWith(".story.md")) {
|
|
104
|
-
context.report({
|
|
105
|
-
node: commentNode,
|
|
106
|
-
messageId: "invalidExtension",
|
|
107
|
-
data: { path: storyPath },
|
|
108
|
-
});
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
// Build candidate paths and check existence
|
|
112
|
-
const candidates = buildCandidates(storyPath, cwd, storyDirs);
|
|
113
|
-
if (!existsAny(candidates)) {
|
|
114
|
-
context.report({
|
|
115
|
-
node: commentNode,
|
|
116
|
-
messageId: "fileMissing",
|
|
117
|
-
data: { path: storyPath },
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Handle a single comment node by processing its lines.
|
|
124
|
-
* @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
|
|
125
|
-
* @req REQ-ANNOTATION-VALIDATION - Ensure each annotation line is parsed
|
|
126
|
-
*/
|
|
127
|
-
function handleComment(
|
|
128
|
-
commentNode: any,
|
|
129
|
-
context: any,
|
|
130
|
-
sourceCode: any,
|
|
131
|
-
cwd: string,
|
|
132
|
-
storyDirs: string[],
|
|
133
|
-
allowAbsolute: boolean,
|
|
134
|
-
requireExt: boolean,
|
|
135
|
-
): void {
|
|
136
|
-
const lines = commentNode.value
|
|
137
|
-
.split(/\r?\n/)
|
|
138
|
-
.map((l: string) => l.replace(/^[^@]*/, "").trim());
|
|
139
|
-
for (const line of lines) {
|
|
140
|
-
if (line.startsWith("@story")) {
|
|
141
|
-
validateStoryPath(
|
|
142
|
-
line,
|
|
143
|
-
commentNode,
|
|
144
|
-
context,
|
|
145
|
-
cwd,
|
|
146
|
-
storyDirs,
|
|
147
|
-
allowAbsolute,
|
|
148
|
-
requireExt,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export default {
|
|
155
|
-
meta: {
|
|
156
|
-
type: "problem",
|
|
157
|
-
docs: {
|
|
158
|
-
description:
|
|
159
|
-
"Validate that @story annotations reference existing .story.md files",
|
|
160
|
-
recommended: "error",
|
|
161
|
-
},
|
|
162
|
-
messages: {
|
|
163
|
-
fileMissing: "Story file '{{path}}' not found",
|
|
164
|
-
invalidExtension:
|
|
165
|
-
"Invalid story file extension for '{{path}}', expected '.story.md'",
|
|
166
|
-
invalidPath: "Invalid story path '{{path}}'",
|
|
167
|
-
},
|
|
168
|
-
schema: [
|
|
169
|
-
{
|
|
170
|
-
type: "object",
|
|
171
|
-
properties: {
|
|
172
|
-
storyDirectories: {
|
|
173
|
-
type: "array",
|
|
174
|
-
items: { type: "string" },
|
|
175
|
-
},
|
|
176
|
-
allowAbsolutePaths: { type: "boolean" },
|
|
177
|
-
requireStoryExtension: { type: "boolean" },
|
|
178
|
-
},
|
|
179
|
-
additionalProperties: false,
|
|
180
|
-
},
|
|
181
|
-
],
|
|
182
|
-
},
|
|
183
|
-
create(context) {
|
|
184
|
-
const sourceCode = context.getSourceCode();
|
|
185
|
-
const cwd = process.cwd();
|
|
186
|
-
const opts = context.options[0] as
|
|
187
|
-
| {
|
|
188
|
-
storyDirectories?: string[];
|
|
189
|
-
allowAbsolutePaths?: boolean;
|
|
190
|
-
requireStoryExtension?: boolean;
|
|
191
|
-
}
|
|
192
|
-
| undefined;
|
|
193
|
-
const storyDirs = opts?.storyDirectories || defaultStoryDirs;
|
|
194
|
-
const allowAbsolute = opts?.allowAbsolutePaths || false;
|
|
195
|
-
const requireExt = opts?.requireStoryExtension !== false;
|
|
196
|
-
return {
|
|
197
|
-
Program() {
|
|
198
|
-
const comments = sourceCode.getAllComments() || [];
|
|
199
|
-
for (const comment of comments) {
|
|
200
|
-
handleComment(
|
|
201
|
-
comment,
|
|
202
|
-
context,
|
|
203
|
-
sourceCode,
|
|
204
|
-
cwd,
|
|
205
|
-
storyDirs,
|
|
206
|
-
allowAbsolute,
|
|
207
|
-
requireExt,
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
},
|
|
213
|
-
} as Rule.RuleModule;
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for: docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
3
|
-
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
4
|
-
* @req REQ-MAINT-BATCH - Perform batch updates
|
|
5
|
-
* @req REQ-MAINT-VERIFY - Verify annotation references
|
|
6
|
-
*/
|
|
7
|
-
import * as fs from "fs";
|
|
8
|
-
import * as path from "path";
|
|
9
|
-
import * as os from "os";
|
|
10
|
-
import {
|
|
11
|
-
batchUpdateAnnotations,
|
|
12
|
-
verifyAnnotations,
|
|
13
|
-
} from "../../src/maintenance/batch";
|
|
14
|
-
|
|
15
|
-
describe("batchUpdateAnnotations (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
|
|
16
|
-
let tmpDir: string;
|
|
17
|
-
|
|
18
|
-
beforeAll(() => {
|
|
19
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "batch-test-"));
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
afterAll(() => {
|
|
23
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("[REQ-MAINT-BATCH] should return 0 when no mappings applied", () => {
|
|
27
|
-
const count = batchUpdateAnnotations(tmpDir, []);
|
|
28
|
-
expect(count).toBe(0);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe("verifyAnnotations (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
|
|
33
|
-
let tmpDir: string;
|
|
34
|
-
|
|
35
|
-
beforeAll(() => {
|
|
36
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "verify-test-"));
|
|
37
|
-
const tsContent = `
|
|
38
|
-
/**
|
|
39
|
-
* Tests for: my-story.story.md
|
|
40
|
-
* @story my-story.story.md
|
|
41
|
-
*/
|
|
42
|
-
`;
|
|
43
|
-
fs.writeFileSync(path.join(tmpDir, "test.ts"), tsContent);
|
|
44
|
-
fs.writeFileSync(path.join(tmpDir, "my-story.story.md"), "# Dummy Story");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
afterAll(() => {
|
|
48
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("[REQ-MAINT-VERIFY] should return true when annotations are valid", () => {
|
|
52
|
-
const valid = verifyAnnotations(tmpDir);
|
|
53
|
-
expect(valid).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for: docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
3
|
-
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
4
|
-
* @req REQ-MAINT-DETECT - Detect stale annotation references
|
|
5
|
-
*/
|
|
6
|
-
import * as fs from "fs";
|
|
7
|
-
import * as path from "path";
|
|
8
|
-
import * as os from "os";
|
|
9
|
-
import { detectStaleAnnotations } from "../../src/maintenance/detect";
|
|
10
|
-
|
|
11
|
-
describe("detectStaleAnnotations isolated (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
|
|
12
|
-
it("[REQ-MAINT-DETECT] returns empty array when directory does not exist", () => {
|
|
13
|
-
const result = detectStaleAnnotations("non-existent-dir");
|
|
14
|
-
expect(result).toEqual([]);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("[REQ-MAINT-DETECT] detects stale annotations in nested directories", () => {
|
|
18
|
-
// Arrange
|
|
19
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tmp-nested-"));
|
|
20
|
-
const nestedDir = path.join(tmpDir, "nested");
|
|
21
|
-
fs.mkdirSync(nestedDir);
|
|
22
|
-
const filePath1 = path.join(tmpDir, "file1.ts");
|
|
23
|
-
const filePath2 = path.join(nestedDir, "file2.ts");
|
|
24
|
-
const content1 = `
|
|
25
|
-
/**
|
|
26
|
-
* @story stale1.story.md
|
|
27
|
-
*/
|
|
28
|
-
`;
|
|
29
|
-
fs.writeFileSync(filePath1, content1, "utf8");
|
|
30
|
-
const content2 = `
|
|
31
|
-
/**
|
|
32
|
-
* @story stale2.story.md
|
|
33
|
-
*/
|
|
34
|
-
`;
|
|
35
|
-
fs.writeFileSync(filePath2, content2, "utf8");
|
|
36
|
-
|
|
37
|
-
// Act
|
|
38
|
-
const result = detectStaleAnnotations(tmpDir);
|
|
39
|
-
|
|
40
|
-
// Assert
|
|
41
|
-
expect(result).toHaveLength(2);
|
|
42
|
-
expect(result).toContain("stale1.story.md");
|
|
43
|
-
expect(result).toContain("stale2.story.md");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("[REQ-MAINT-DETECT] throws error on permission denied", () => {
|
|
47
|
-
const tmpDir2 = fs.mkdtempSync(path.join(os.tmpdir(), "tmp-perm-"));
|
|
48
|
-
const dir = path.join(tmpDir2, "subdir");
|
|
49
|
-
fs.mkdirSync(dir);
|
|
50
|
-
const filePath = path.join(dir, "file.ts");
|
|
51
|
-
const content = `
|
|
52
|
-
/**
|
|
53
|
-
* @story none.story.md
|
|
54
|
-
*/
|
|
55
|
-
`;
|
|
56
|
-
fs.writeFileSync(filePath, content, "utf8");
|
|
57
|
-
// Remove read permission
|
|
58
|
-
fs.chmodSync(dir, 0o000);
|
|
59
|
-
expect(() => detectStaleAnnotations(tmpDir2)).toThrow();
|
|
60
|
-
// Restore permissions
|
|
61
|
-
fs.chmodSync(dir, 0o700);
|
|
62
|
-
// Cleanup temporary directory
|
|
63
|
-
fs.rmSync(tmpDir2, { recursive: true, force: true });
|
|
64
|
-
});
|
|
65
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for: docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
3
|
-
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
4
|
-
* @req REQ-MAINT-DETECT - Detect stale annotation references
|
|
5
|
-
*/
|
|
6
|
-
import fs from "fs";
|
|
7
|
-
import path from "path";
|
|
8
|
-
import os from "os";
|
|
9
|
-
import { detectStaleAnnotations } from "../../src/maintenance/detect";
|
|
10
|
-
|
|
11
|
-
describe("detectStaleAnnotations (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
|
|
12
|
-
it("[REQ-MAINT-DETECT] should return empty array when no stale annotations", () => {
|
|
13
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "detect-test-"));
|
|
14
|
-
// No annotation files are created in tmpDir to simulate no stale annotations
|
|
15
|
-
const result = detectStaleAnnotations(tmpDir);
|
|
16
|
-
expect(result).toEqual([]);
|
|
17
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
18
|
-
});
|
|
19
|
-
});
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for: docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
3
|
-
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
4
|
-
* @req REQ-MAINT-REPORT - Generate maintenance report
|
|
5
|
-
* @req REQ-MAINT-SAFE - Ensure operations are safe and reversible
|
|
6
|
-
*/
|
|
7
|
-
import * as fs from "fs";
|
|
8
|
-
import * as path from "path";
|
|
9
|
-
import * as os from "os";
|
|
10
|
-
import { generateMaintenanceReport } from "../../src/maintenance/report";
|
|
11
|
-
|
|
12
|
-
describe("generateMaintenanceReport (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
|
|
13
|
-
let tmpDir: string;
|
|
14
|
-
|
|
15
|
-
beforeAll(() => {
|
|
16
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "report-test-"));
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
afterAll(() => {
|
|
20
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("[REQ-MAINT-REPORT] should return empty string when no operations", () => {
|
|
24
|
-
const report = generateMaintenanceReport(tmpDir);
|
|
25
|
-
expect(report).toBe("");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("[REQ-MAINT-REPORT] should report stale story annotation", () => {
|
|
29
|
-
const filePath = path.join(tmpDir, "stub.md");
|
|
30
|
-
const content = `/**
|
|
31
|
-
* @story non-existent.md
|
|
32
|
-
*/`;
|
|
33
|
-
fs.writeFileSync(filePath, content);
|
|
34
|
-
const report = generateMaintenanceReport(tmpDir);
|
|
35
|
-
expect(report).toContain("non-existent.md");
|
|
36
|
-
});
|
|
37
|
-
});
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for: docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
3
|
-
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
4
|
-
* @req REQ-MAINT-UPDATE - Update annotation references
|
|
5
|
-
*/
|
|
6
|
-
import * as fs from "fs";
|
|
7
|
-
import * as path from "path";
|
|
8
|
-
import * as os from "os";
|
|
9
|
-
import { updateAnnotationReferences } from "../../src/maintenance/update";
|
|
10
|
-
|
|
11
|
-
describe("updateAnnotationReferences isolated (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
|
|
12
|
-
it("[REQ-MAINT-UPDATE] updates @story annotations in files", () => {
|
|
13
|
-
// Create a temporary directory for testing
|
|
14
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "tmp-"));
|
|
15
|
-
const filePath = path.join(tmpDir, "file.ts");
|
|
16
|
-
const originalContent = `
|
|
17
|
-
/**
|
|
18
|
-
* @story old.path.md
|
|
19
|
-
*/
|
|
20
|
-
function foo() {}
|
|
21
|
-
`;
|
|
22
|
-
fs.writeFileSync(filePath, originalContent, "utf8");
|
|
23
|
-
|
|
24
|
-
// Run the function under test
|
|
25
|
-
const count = updateAnnotationReferences(
|
|
26
|
-
tmpDir,
|
|
27
|
-
"old.path.md",
|
|
28
|
-
"new.path.md",
|
|
29
|
-
);
|
|
30
|
-
expect(count).toBe(1);
|
|
31
|
-
|
|
32
|
-
// Verify the file content was updated
|
|
33
|
-
const updatedContent = fs.readFileSync(filePath, "utf8");
|
|
34
|
-
expect(updatedContent).toContain("@story new.path.md");
|
|
35
|
-
|
|
36
|
-
// Cleanup temporary directory
|
|
37
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
38
|
-
});
|
|
39
|
-
});
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for: docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
3
|
-
* @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
|
|
4
|
-
* @req REQ-MAINT-UPDATE - Update annotation references
|
|
5
|
-
*/
|
|
6
|
-
import fs from "fs";
|
|
7
|
-
import os from "os";
|
|
8
|
-
import path from "path";
|
|
9
|
-
import { updateAnnotationReferences } from "../../src/maintenance/update";
|
|
10
|
-
|
|
11
|
-
describe("updateAnnotationReferences (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
|
|
12
|
-
it("[REQ-MAINT-UPDATE] should return 0 when no updates made", () => {
|
|
13
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "update-test-"));
|
|
14
|
-
try {
|
|
15
|
-
const count = updateAnnotationReferences(tmpDir, "old.md", "new.md");
|
|
16
|
-
expect(count).toBe(0);
|
|
17
|
-
} finally {
|
|
18
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
});
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for: docs/stories/001.0-DEV-PLUGIN-SETUP.story.md
|
|
3
|
-
* @story docs/stories/001.0-DEV-PLUGIN-SETUP.story.md
|
|
4
|
-
* @req REQ-PLUGIN-STRUCTURE - Validate plugin default export and configs in src/index.ts
|
|
5
|
-
*/
|
|
6
|
-
import plugin, { rules, configs } from "../src/index";
|
|
7
|
-
|
|
8
|
-
describe("Plugin Default Export and Configs (Story 001.0-DEV-PLUGIN-SETUP)", () => {
|
|
9
|
-
it("[REQ-PLUGIN-STRUCTURE] default export includes rules and configs", () => {
|
|
10
|
-
expect(plugin.rules).toBe(rules);
|
|
11
|
-
expect(plugin.configs).toBe(configs);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("[REQ-PLUGIN-STRUCTURE] rules object has correct rule names", () => {
|
|
15
|
-
// Arrange: expected rule names in insertion order
|
|
16
|
-
const expected = [
|
|
17
|
-
"require-story-annotation",
|
|
18
|
-
"require-req-annotation",
|
|
19
|
-
"require-branch-annotation",
|
|
20
|
-
"valid-annotation-format",
|
|
21
|
-
"valid-story-reference",
|
|
22
|
-
"valid-req-reference",
|
|
23
|
-
];
|
|
24
|
-
// Act: get actual rule names from plugin
|
|
25
|
-
const actual = Object.keys(rules);
|
|
26
|
-
// Assert: actual matches expected
|
|
27
|
-
expect(actual).toEqual(expected);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("[REQ-RULE-REGISTRY] configs.recommended contains correct rule configuration", () => {
|
|
31
|
-
const recommendedRules = configs.recommended[0].rules;
|
|
32
|
-
expect(recommendedRules).toHaveProperty(
|
|
33
|
-
"traceability/require-story-annotation",
|
|
34
|
-
"error",
|
|
35
|
-
);
|
|
36
|
-
expect(recommendedRules).toHaveProperty(
|
|
37
|
-
"traceability/require-req-annotation",
|
|
38
|
-
"error",
|
|
39
|
-
);
|
|
40
|
-
expect(recommendedRules).toHaveProperty(
|
|
41
|
-
"traceability/require-branch-annotation",
|
|
42
|
-
"error",
|
|
43
|
-
);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("[REQ-CONFIG-SYSTEM] configs.strict contains same rules as recommended", () => {
|
|
47
|
-
const strictRules = configs.strict[0].rules;
|
|
48
|
-
expect(strictRules).toEqual(configs.recommended[0].rules);
|
|
49
|
-
});
|
|
50
|
-
});
|