eslint-plugin-traceability 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +2 -1
  2. package/lib/src/index.d.ts +12 -17
  3. package/lib/src/index.js +69 -24
  4. package/lib/src/maintenance/utils.js +5 -0
  5. package/lib/src/rules/require-branch-annotation.js +27 -147
  6. package/lib/src/rules/require-req-annotation.d.ts +5 -0
  7. package/lib/src/rules/require-req-annotation.js +20 -0
  8. package/lib/src/rules/require-story-annotation.d.ts +3 -12
  9. package/lib/src/rules/require-story-annotation.js +192 -162
  10. package/lib/src/rules/valid-annotation-format.js +11 -0
  11. package/lib/src/rules/valid-req-reference.js +65 -25
  12. package/lib/src/rules/valid-story-reference.js +55 -58
  13. package/lib/src/utils/annotation-checker.js +80 -13
  14. package/lib/src/utils/branch-annotation-helpers.d.ts +54 -0
  15. package/lib/src/utils/branch-annotation-helpers.js +148 -0
  16. package/lib/src/utils/storyReferenceUtils.d.ts +47 -0
  17. package/lib/src/utils/storyReferenceUtils.js +111 -0
  18. package/lib/tests/cli-error-handling.test.d.ts +1 -0
  19. package/lib/tests/cli-error-handling.test.js +44 -0
  20. package/lib/tests/integration/cli-integration.test.js +3 -5
  21. package/lib/tests/maintenance/index.test.d.ts +1 -0
  22. package/lib/tests/maintenance/index.test.js +25 -0
  23. package/lib/tests/plugin-setup-error.test.d.ts +5 -0
  24. package/lib/tests/plugin-setup-error.test.js +37 -0
  25. package/lib/tests/rules/error-reporting.test.d.ts +1 -0
  26. package/lib/tests/rules/error-reporting.test.js +47 -0
  27. package/lib/tests/rules/require-story-annotation.test.js +101 -25
  28. package/lib/tests/utils/branch-annotation-helpers.test.d.ts +1 -0
  29. package/lib/tests/utils/branch-annotation-helpers.test.js +46 -0
  30. package/package.json +4 -3
@@ -11,59 +11,39 @@ Object.defineProperty(exports, "__esModule", { value: true });
11
11
  * @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
12
12
  * @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
13
13
  */
14
- const fs_1 = __importDefault(require("fs"));
15
14
  const path_1 = __importDefault(require("path"));
15
+ const storyReferenceUtils_1 = require("../utils/storyReferenceUtils");
16
16
  const defaultStoryDirs = ["docs/stories", "stories"];
17
- const fileExistCache = new Map();
18
17
  /**
19
- * Build possible file paths for a given storyPath.
18
+ * Extract the story path from the annotation line and delegate validation.
20
19
  * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
21
- * @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
22
- */
23
- function buildCandidates(storyPath, cwd, storyDirs) {
24
- const candidates = [];
25
- if (storyPath.startsWith("./") || storyPath.startsWith("../")) {
26
- candidates.push(path_1.default.resolve(cwd, storyPath));
27
- }
28
- else {
29
- candidates.push(path_1.default.resolve(cwd, storyPath));
30
- for (const dir of storyDirs) {
31
- candidates.push(path_1.default.resolve(cwd, dir, path_1.default.basename(storyPath)));
32
- }
33
- }
34
- return candidates;
35
- }
36
- /**
37
- * Check if any of the candidate files exist.
38
- * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
39
- * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
20
+ * @req REQ-ANNOTATION-VALIDATION - Ensure each annotation line is parsed
40
21
  */
41
- function existsAny(paths) {
42
- for (const candidate of paths) {
43
- let ok = fileExistCache.get(candidate);
44
- if (ok === undefined) {
45
- ok = fs_1.default.existsSync(candidate) && fs_1.default.statSync(candidate).isFile();
46
- fileExistCache.set(candidate, ok);
47
- }
48
- if (ok) {
49
- return true;
50
- }
51
- }
52
- return false;
22
+ function validateStoryPath(opts) {
23
+ const { line, commentNode, context, cwd, storyDirs, allowAbsolute, requireExt, } = opts;
24
+ const parts = line.split(/\s+/);
25
+ const storyPath = parts[1];
26
+ if (!storyPath)
27
+ return;
28
+ processStoryPath({
29
+ storyPath,
30
+ commentNode,
31
+ context,
32
+ cwd,
33
+ storyDirs,
34
+ allowAbsolute,
35
+ requireExt,
36
+ });
53
37
  }
54
38
  /**
55
- * Validate a single @story annotation line.
39
+ * Process and validate the story path for security, extension, and existence.
56
40
  * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
57
41
  * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
58
42
  * @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
59
43
  * @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
60
44
  */
61
- function validateStoryPath(line, commentNode, context, cwd, storyDirs, allowAbsolute, requireExt) {
62
- const parts = line.split(/\s+/);
63
- const storyPath = parts[1];
64
- if (!storyPath) {
65
- return;
66
- }
45
+ function processStoryPath(opts) {
46
+ const { storyPath, commentNode, context, cwd, storyDirs, allowAbsolute, requireExt, } = opts;
67
47
  // Absolute path check
68
48
  if (path_1.default.isAbsolute(storyPath)) {
69
49
  if (!allowAbsolute) {
@@ -75,10 +55,9 @@ function validateStoryPath(line, commentNode, context, cwd, storyDirs, allowAbso
75
55
  }
76
56
  return;
77
57
  }
78
- // Path traversal prevention
79
- if (storyPath.includes("..")) {
80
- const normalized = path_1.default.normalize(storyPath);
81
- const full = path_1.default.resolve(cwd, normalized);
58
+ // Path traversal check
59
+ if ((0, storyReferenceUtils_1.containsPathTraversal)(storyPath)) {
60
+ const full = path_1.default.resolve(cwd, path_1.default.normalize(storyPath));
82
61
  if (!full.startsWith(cwd + path_1.default.sep)) {
83
62
  context.report({
84
63
  node: commentNode,
@@ -89,7 +68,7 @@ function validateStoryPath(line, commentNode, context, cwd, storyDirs, allowAbso
89
68
  }
90
69
  }
91
70
  // Extension check
92
- if (requireExt && !storyPath.endsWith(".story.md")) {
71
+ if (requireExt && !(0, storyReferenceUtils_1.hasValidExtension)(storyPath)) {
93
72
  context.report({
94
73
  node: commentNode,
95
74
  messageId: "invalidExtension",
@@ -97,9 +76,8 @@ function validateStoryPath(line, commentNode, context, cwd, storyDirs, allowAbso
97
76
  });
98
77
  return;
99
78
  }
100
- // Build candidate paths and check existence
101
- const candidates = buildCandidates(storyPath, cwd, storyDirs);
102
- if (!existsAny(candidates)) {
79
+ // Existence check
80
+ if (!(0, storyReferenceUtils_1.normalizeStoryPath)(storyPath, cwd, storyDirs).exists) {
103
81
  context.report({
104
82
  node: commentNode,
105
83
  messageId: "fileMissing",
@@ -112,13 +90,22 @@ function validateStoryPath(line, commentNode, context, cwd, storyDirs, allowAbso
112
90
  * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
113
91
  * @req REQ-ANNOTATION-VALIDATION - Ensure each annotation line is parsed
114
92
  */
115
- function handleComment(commentNode, context, sourceCode, cwd, storyDirs, allowAbsolute, requireExt) {
93
+ function handleComment(opts) {
94
+ const { commentNode, context, cwd, storyDirs, allowAbsolute, requireExt } = opts;
116
95
  const lines = commentNode.value
117
96
  .split(/\r?\n/)
118
97
  .map((l) => l.replace(/^[^@]*/, "").trim());
119
98
  for (const line of lines) {
120
99
  if (line.startsWith("@story")) {
121
- validateStoryPath(line, commentNode, context, cwd, storyDirs, allowAbsolute, requireExt);
100
+ validateStoryPath({
101
+ line,
102
+ commentNode,
103
+ context,
104
+ cwd,
105
+ storyDirs,
106
+ allowAbsolute,
107
+ requireExt,
108
+ });
122
109
  }
123
110
  }
124
111
  }
@@ -138,10 +125,7 @@ exports.default = {
138
125
  {
139
126
  type: "object",
140
127
  properties: {
141
- storyDirectories: {
142
- type: "array",
143
- items: { type: "string" },
144
- },
128
+ storyDirectories: { type: "array", items: { type: "string" } },
145
129
  allowAbsolutePaths: { type: "boolean" },
146
130
  requireStoryExtension: { type: "boolean" },
147
131
  },
@@ -150,17 +134,30 @@ exports.default = {
150
134
  ],
151
135
  },
152
136
  create(context) {
153
- const sourceCode = context.getSourceCode();
154
137
  const cwd = process.cwd();
155
138
  const opts = context.options[0];
156
139
  const storyDirs = opts?.storyDirectories || defaultStoryDirs;
157
140
  const allowAbsolute = opts?.allowAbsolutePaths || false;
158
141
  const requireExt = opts?.requireStoryExtension !== false;
159
142
  return {
143
+ /**
144
+ * Program-level handler: iterate comments and validate @story annotations.
145
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
146
+ * @req REQ-ANNOTATION-VALIDATION - Discover and dispatch @story annotations for validation
147
+ * @req REQ-FILE-EXISTENCE - Ensure referenced files exist
148
+ * @req REQ-PATH-RESOLUTION - Resolve using cwd and configured story directories
149
+ */
160
150
  Program() {
161
- const comments = sourceCode.getAllComments() || [];
151
+ const comments = context.getSourceCode().getAllComments() || [];
162
152
  for (const comment of comments) {
163
- handleComment(comment, context, sourceCode, cwd, storyDirs, allowAbsolute, requireExt);
153
+ handleComment({
154
+ commentNode: comment,
155
+ context,
156
+ cwd,
157
+ storyDirs,
158
+ allowAbsolute,
159
+ requireExt,
160
+ });
164
161
  }
165
162
  },
166
163
  };
@@ -1,6 +1,80 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.checkReqAnnotation = checkReqAnnotation;
4
+ /**
5
+ * Helper to retrieve the JSDoc comment for a node.
6
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
7
+ * @req REQ-ANNOTATION-GET-JSDOC - Retrieve JSDoc comment for a node
8
+ */
9
+ function getJsdocComment(sourceCode, node) {
10
+ return sourceCode.getJSDocComment(node);
11
+ }
12
+ /**
13
+ * Helper to retrieve leading comments from a node (TypeScript declare style).
14
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
15
+ * @req REQ-ANNOTATION-LEADING-COMMENTS - Collect leading comments from node
16
+ */
17
+ function getLeadingComments(node) {
18
+ return node.leadingComments || [];
19
+ }
20
+ /**
21
+ * Helper to retrieve comments before a node using the sourceCode API.
22
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
23
+ * @req REQ-ANNOTATION-COMMENTS-BEFORE - Collect comments before node via sourceCode
24
+ */
25
+ function getCommentsBefore(sourceCode, node) {
26
+ return sourceCode.getCommentsBefore(node) || [];
27
+ }
28
+ /**
29
+ * Helper to combine leading and before comments into a single array.
30
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
31
+ * @req REQ-ANNOTATION-COMBINE-COMMENTS - Combine comment arrays for checking
32
+ */
33
+ function combineComments(leading, before) {
34
+ return [...leading, ...before];
35
+ }
36
+ /**
37
+ * Predicate helper to check whether a comment contains a @req annotation.
38
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
39
+ * @req REQ-ANNOTATION-CHECK-COMMENT - Detect @req tag inside a comment
40
+ */
41
+ function commentContainsReq(c) {
42
+ return c && typeof c.value === "string" && c.value.includes("@req");
43
+ }
44
+ /**
45
+ * Helper to determine whether a JSDoc or any nearby comments contain a @req annotation.
46
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
47
+ * @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
48
+ */
49
+ function hasReqAnnotation(jsdoc, comments) {
50
+ return ((jsdoc &&
51
+ typeof jsdoc.value === "string" &&
52
+ jsdoc.value.includes("@req")) ||
53
+ comments.some(commentContainsReq));
54
+ }
55
+ /**
56
+ * Creates a fix function that inserts a missing @req JSDoc before the node.
57
+ * Returned function is a proper named function so no inline arrow is used.
58
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
59
+ * @req REQ-ANNOTATION-AUTOFIX - Provide autofix for missing @req annotation
60
+ */
61
+ function createMissingReqFix(node) {
62
+ return function missingReqFix(fixer) {
63
+ return fixer.insertTextBefore(node, "/** @req <REQ-ID> */\n");
64
+ };
65
+ }
66
+ /**
67
+ * Helper to report a missing @req annotation via the ESLint context API.
68
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
69
+ * @req REQ-ANNOTATION-REPORTING - Report missing @req annotation to context
70
+ */
71
+ function reportMissing(context, node) {
72
+ context.report({
73
+ node,
74
+ messageId: "missingReq",
75
+ fix: createMissingReqFix(node),
76
+ });
77
+ }
4
78
  /**
5
79
  * Helper to check @req annotation presence on TS declare functions and method signatures.
6
80
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
@@ -8,19 +82,12 @@ exports.checkReqAnnotation = checkReqAnnotation;
8
82
  */
9
83
  function checkReqAnnotation(context, node) {
10
84
  const sourceCode = context.getSourceCode();
11
- const jsdoc = sourceCode.getJSDocComment(node);
12
- const leading = node.leadingComments || [];
13
- const comments = sourceCode.getCommentsBefore(node) || [];
14
- const all = [...leading, ...comments];
15
- const hasReq = (jsdoc && jsdoc.value.includes("@req")) ||
16
- all.some((c) => c.value.includes("@req"));
85
+ const jsdoc = getJsdocComment(sourceCode, node);
86
+ const leading = getLeadingComments(node);
87
+ const comments = getCommentsBefore(sourceCode, node);
88
+ const all = combineComments(leading, comments);
89
+ const hasReq = hasReqAnnotation(jsdoc, all);
17
90
  if (!hasReq) {
18
- context.report({
19
- node,
20
- messageId: "missingReq",
21
- fix(fixer) {
22
- return fixer.insertTextBefore(node, "/** @req <REQ-ID> */\n");
23
- },
24
- });
91
+ reportMissing(context, node);
25
92
  }
26
93
  }
@@ -0,0 +1,54 @@
1
+ import type { Rule } from "eslint";
2
+ /**
3
+ * Valid branch types for require-branch-annotation rule.
4
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
5
+ * @req REQ-SIGNIFICANCE-CRITERIA - Define criteria for which branches require annotations
6
+ */
7
+ export declare const DEFAULT_BRANCH_TYPES: readonly ["IfStatement", "SwitchCase", "TryStatement", "CatchClause", "ForStatement", "ForOfStatement", "ForInStatement", "WhileStatement", "DoWhileStatement"];
8
+ /**
9
+ * Type for branch nodes supported by require-branch-annotation rule.
10
+ */
11
+ export type BranchType = (typeof DEFAULT_BRANCH_TYPES)[number];
12
+ /**
13
+ * Validate branchTypes configuration option and return branch types to enforce,
14
+ * or return an ESLint listener if configuration is invalid.
15
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
16
+ * @req REQ-CONFIGURABLE-SCOPE - Allow configuration of branch types for annotation enforcement
17
+ */
18
+ export declare function validateBranchTypes(context: Rule.RuleContext): BranchType[] | Rule.RuleListener;
19
+ /**
20
+ * Gather leading comment text for a branch node.
21
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
22
+ * @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
23
+ */
24
+ export declare function gatherBranchCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any): string;
25
+ /**
26
+ * Report missing @story annotation on a branch node.
27
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
28
+ * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
29
+ */
30
+ export declare function reportMissingStory(context: Rule.RuleContext, node: any, options: {
31
+ indent: string;
32
+ insertPos: number;
33
+ storyFixCountRef: {
34
+ count: number;
35
+ };
36
+ }): void;
37
+ /**
38
+ * Report missing @req annotation on a branch node.
39
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
40
+ * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
41
+ */
42
+ export declare function reportMissingReq(context: Rule.RuleContext, node: any, options: {
43
+ indent: string;
44
+ insertPos: number;
45
+ missingStory: boolean;
46
+ }): void;
47
+ /**
48
+ * Report missing annotations on a branch node.
49
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
50
+ * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
51
+ */
52
+ export declare function reportMissingAnnotations(context: Rule.RuleContext, node: any, storyFixCountRef: {
53
+ count: number;
54
+ }): void;
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_BRANCH_TYPES = void 0;
4
+ exports.validateBranchTypes = validateBranchTypes;
5
+ exports.gatherBranchCommentText = gatherBranchCommentText;
6
+ exports.reportMissingStory = reportMissingStory;
7
+ exports.reportMissingReq = reportMissingReq;
8
+ exports.reportMissingAnnotations = reportMissingAnnotations;
9
+ const PRE_COMMENT_OFFSET = 2; // number of lines above branch to inspect for comments
10
+ /**
11
+ * Valid branch types for require-branch-annotation rule.
12
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
13
+ * @req REQ-SIGNIFICANCE-CRITERIA - Define criteria for which branches require annotations
14
+ */
15
+ exports.DEFAULT_BRANCH_TYPES = [
16
+ "IfStatement",
17
+ "SwitchCase",
18
+ "TryStatement",
19
+ "CatchClause",
20
+ "ForStatement",
21
+ "ForOfStatement",
22
+ "ForInStatement",
23
+ "WhileStatement",
24
+ "DoWhileStatement",
25
+ ];
26
+ /**
27
+ * Validate branchTypes configuration option and return branch types to enforce,
28
+ * or return an ESLint listener if configuration is invalid.
29
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
30
+ * @req REQ-CONFIGURABLE-SCOPE - Allow configuration of branch types for annotation enforcement
31
+ */
32
+ function validateBranchTypes(context) {
33
+ const options = context.options[0] || {};
34
+ if (Array.isArray(options.branchTypes)) {
35
+ const invalidTypes = options.branchTypes.filter((t) => !exports.DEFAULT_BRANCH_TYPES.includes(t));
36
+ if (invalidTypes.length > 0) {
37
+ return {
38
+ Program(node) {
39
+ invalidTypes.forEach((t) => {
40
+ context.report({
41
+ node,
42
+ message: `Value "${t}" should be equal to one of the allowed values: ${exports.DEFAULT_BRANCH_TYPES.join(", ")}`,
43
+ });
44
+ });
45
+ },
46
+ };
47
+ }
48
+ }
49
+ return Array.isArray(options.branchTypes)
50
+ ? options.branchTypes
51
+ : Array.from(exports.DEFAULT_BRANCH_TYPES);
52
+ }
53
+ /**
54
+ * Gather leading comment text for a branch node.
55
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
56
+ * @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
57
+ */
58
+ function gatherBranchCommentText(sourceCode, node) {
59
+ if (node.type === "SwitchCase") {
60
+ const lines = sourceCode.lines;
61
+ const startLine = node.loc.start.line;
62
+ let i = startLine - PRE_COMMENT_OFFSET;
63
+ const comments = [];
64
+ while (i >= 0 && /^\s*(\/\/|\/\*)/.test(lines[i])) {
65
+ comments.unshift(lines[i].trim());
66
+ i--;
67
+ }
68
+ return comments.join(" ");
69
+ }
70
+ const comments = sourceCode.getCommentsBefore(node) || [];
71
+ return comments.map((c) => c.value).join(" ");
72
+ }
73
+ /**
74
+ * Report missing @story annotation on a branch node.
75
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
76
+ * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
77
+ */
78
+ function reportMissingStory(context, node, options) {
79
+ const { indent, insertPos, storyFixCountRef } = options;
80
+ if (storyFixCountRef.count === 0) {
81
+ context.report({
82
+ node,
83
+ messageId: "missingAnnotation",
84
+ data: { missing: "@story" },
85
+ fix: (fixer) => fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @story <story-file>.story.md\n`),
86
+ });
87
+ storyFixCountRef.count++;
88
+ }
89
+ else {
90
+ context.report({
91
+ node,
92
+ messageId: "missingAnnotation",
93
+ data: { missing: "@story" },
94
+ });
95
+ }
96
+ }
97
+ /**
98
+ * Report missing @req annotation on a branch node.
99
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
100
+ * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
101
+ */
102
+ function reportMissingReq(context, node, options) {
103
+ const { indent, insertPos, missingStory } = options;
104
+ if (!missingStory) {
105
+ context.report({
106
+ node,
107
+ messageId: "missingAnnotation",
108
+ data: { missing: "@req" },
109
+ fix: (fixer) => fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @req <REQ-ID>\n`),
110
+ });
111
+ }
112
+ else {
113
+ context.report({
114
+ node,
115
+ messageId: "missingAnnotation",
116
+ data: { missing: "@req" },
117
+ });
118
+ }
119
+ }
120
+ /**
121
+ * Report missing annotations on a branch node.
122
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
123
+ * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
124
+ */
125
+ function reportMissingAnnotations(context, node, storyFixCountRef) {
126
+ const sourceCode = context.getSourceCode();
127
+ const text = gatherBranchCommentText(sourceCode, node);
128
+ const missingStory = !/@story\b/.test(text);
129
+ const missingReq = !/@req\b/.test(text);
130
+ const indent = sourceCode.lines[node.loc.start.line - 1].match(/^(\s*)/)?.[1] || "";
131
+ const insertPos = sourceCode.getIndexFromLoc({
132
+ line: node.loc.start.line,
133
+ column: 0,
134
+ });
135
+ const actions = [
136
+ {
137
+ missing: missingStory,
138
+ fn: reportMissingStory,
139
+ args: [context, node, { indent, insertPos, storyFixCountRef }],
140
+ },
141
+ {
142
+ missing: missingReq,
143
+ fn: reportMissingReq,
144
+ args: [context, node, { indent, insertPos, missingStory }],
145
+ },
146
+ ];
147
+ actions.forEach(({ missing, fn, args }) => missing && fn(...args));
148
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Build candidate file paths for a given story path.
3
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
4
+ * @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
5
+ */
6
+ export declare function buildStoryCandidates(storyPath: string, cwd: string, storyDirs: string[]): string[];
7
+ export declare function storyExists(paths: string[]): boolean;
8
+ /**
9
+ * Normalize a story path to candidate absolute paths and check existence.
10
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
11
+ * @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
12
+ * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
13
+ */
14
+ export declare function normalizeStoryPath(storyPath: string, cwd: string, storyDirs: string[]): {
15
+ candidates: string[];
16
+ exists: boolean;
17
+ };
18
+ /**
19
+ * Check if the provided path is absolute.
20
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
21
+ * @req REQ-SECURITY-VALIDATION - Prevent absolute path usage
22
+ */
23
+ export declare function isAbsolutePath(p: string): boolean;
24
+ /**
25
+ * Check for path traversal patterns.
26
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
27
+ * @req REQ-SECURITY-VALIDATION - Prevent path traversal
28
+ */
29
+ export declare function containsPathTraversal(p: string): boolean;
30
+ /**
31
+ * Determine if a path is unsafe due to traversal or being absolute.
32
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
33
+ * @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
34
+ */
35
+ export declare function isTraversalUnsafe(p: string): boolean;
36
+ /**
37
+ * Validate that the story file has an allowed extension.
38
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
39
+ * @req REQ-SECURITY-VALIDATION - Enforce allowed file extensions
40
+ */
41
+ export declare function hasValidExtension(p: string): boolean;
42
+ /**
43
+ * Determine if a story path is unsafe due to traversal, being absolute, or invalid extension.
44
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
45
+ * @req REQ-SECURITY-VALIDATION - Prevent path traversal, absolute path usage, and enforce allowed file extensions
46
+ */
47
+ export declare function isUnsafeStoryPath(p: string): boolean;
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildStoryCandidates = buildStoryCandidates;
7
+ exports.storyExists = storyExists;
8
+ exports.normalizeStoryPath = normalizeStoryPath;
9
+ exports.isAbsolutePath = isAbsolutePath;
10
+ exports.containsPathTraversal = containsPathTraversal;
11
+ exports.isTraversalUnsafe = isTraversalUnsafe;
12
+ exports.hasValidExtension = hasValidExtension;
13
+ exports.isUnsafeStoryPath = isUnsafeStoryPath;
14
+ /**
15
+ * Utility functions for story path resolution and existence checking.
16
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
17
+ * @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
18
+ * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
19
+ * @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
20
+ */
21
+ const fs_1 = __importDefault(require("fs"));
22
+ const path_1 = __importDefault(require("path"));
23
+ /**
24
+ * Build candidate file paths for a given story path.
25
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
26
+ * @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
27
+ */
28
+ function buildStoryCandidates(storyPath, cwd, storyDirs) {
29
+ const candidates = [];
30
+ if (storyPath.startsWith("./") || storyPath.startsWith("../")) {
31
+ candidates.push(path_1.default.resolve(cwd, storyPath));
32
+ }
33
+ else {
34
+ candidates.push(path_1.default.resolve(cwd, storyPath));
35
+ for (const dir of storyDirs) {
36
+ candidates.push(path_1.default.resolve(cwd, dir, path_1.default.basename(storyPath)));
37
+ }
38
+ }
39
+ return candidates;
40
+ }
41
+ /**
42
+ * Check if any of the provided file paths exist.
43
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
44
+ * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
45
+ */
46
+ const fileExistCache = new Map();
47
+ function storyExists(paths) {
48
+ for (const candidate of paths) {
49
+ let ok = fileExistCache.get(candidate);
50
+ if (ok === undefined) {
51
+ ok = fs_1.default.existsSync(candidate) && fs_1.default.statSync(candidate).isFile();
52
+ fileExistCache.set(candidate, ok);
53
+ }
54
+ if (ok) {
55
+ return true;
56
+ }
57
+ }
58
+ return false;
59
+ }
60
+ /**
61
+ * Normalize a story path to candidate absolute paths and check existence.
62
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
63
+ * @req REQ-PATH-RESOLUTION - Resolve relative paths correctly and enforce configuration
64
+ * @req REQ-FILE-EXISTENCE - Validate that story file paths reference existing files
65
+ */
66
+ function normalizeStoryPath(storyPath, cwd, storyDirs) {
67
+ const candidates = buildStoryCandidates(storyPath, cwd, storyDirs);
68
+ const exists = storyExists(candidates);
69
+ return { candidates, exists };
70
+ }
71
+ /**
72
+ * Check if the provided path is absolute.
73
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
74
+ * @req REQ-SECURITY-VALIDATION - Prevent absolute path usage
75
+ */
76
+ function isAbsolutePath(p) {
77
+ return path_1.default.isAbsolute(p);
78
+ }
79
+ /**
80
+ * Check for path traversal patterns.
81
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
82
+ * @req REQ-SECURITY-VALIDATION - Prevent path traversal
83
+ */
84
+ function containsPathTraversal(p) {
85
+ const normalized = path_1.default.normalize(p);
86
+ return normalized.split(path_1.default.sep).includes("..");
87
+ }
88
+ /**
89
+ * Determine if a path is unsafe due to traversal or being absolute.
90
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
91
+ * @req REQ-SECURITY-VALIDATION - Prevent path traversal and absolute path usage
92
+ */
93
+ function isTraversalUnsafe(p) {
94
+ return isAbsolutePath(p) || containsPathTraversal(p);
95
+ }
96
+ /**
97
+ * Validate that the story file has an allowed extension.
98
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
99
+ * @req REQ-SECURITY-VALIDATION - Enforce allowed file extensions
100
+ */
101
+ function hasValidExtension(p) {
102
+ return p.endsWith(".story.md");
103
+ }
104
+ /**
105
+ * Determine if a story path is unsafe due to traversal, being absolute, or invalid extension.
106
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
107
+ * @req REQ-SECURITY-VALIDATION - Prevent path traversal, absolute path usage, and enforce allowed file extensions
108
+ */
109
+ function isUnsafeStoryPath(p) {
110
+ return isTraversalUnsafe(p) || !hasValidExtension(p);
111
+ }
@@ -0,0 +1 @@
1
+ export {};