eslint-plugin-traceability 1.6.2 → 1.6.4

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.
@@ -37,37 +37,107 @@ exports.detectStaleAnnotations = detectStaleAnnotations;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const utils_1 = require("./utils");
40
+ const storyReferenceUtils_1 = require("../utils/storyReferenceUtils");
40
41
  /**
41
42
  * Detect stale annotation references that point to moved or deleted story files
42
43
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
43
44
  * @req REQ-MAINT-DETECT - Detect stale annotation references
44
45
  */
45
46
  function detectStaleAnnotations(codebasePath) {
46
- // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md // @req REQ-MAINT-DETECT - Ensure codebase path exists and is a directory
47
- if (!fs.existsSync(codebasePath) ||
48
- !fs.statSync(codebasePath).isDirectory()) {
47
+ const cwd = process.cwd();
48
+ // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
49
+ // @req REQ-MAINT-DETECT - Treat codebasePath as a workspace root resolved from process.cwd()
50
+ const workspaceRoot = path.resolve(cwd, codebasePath);
51
+ // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
52
+ // @req REQ-MAINT-DETECT - Return empty result if workspaceRoot does not exist or is not a directory
53
+ if (!fs.existsSync(workspaceRoot) ||
54
+ !fs.statSync(workspaceRoot).isDirectory()) {
49
55
  return [];
50
56
  }
51
- const cwd = process.cwd();
52
- const baseDir = path.resolve(cwd, codebasePath);
53
57
  const stale = new Set();
54
- const files = (0, utils_1.getAllFiles)(codebasePath);
55
- // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md // @req REQ-MAINT-DETECT - Iterate over all files in the codebase
58
+ // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
59
+ // @req REQ-MAINT-DETECT - Iterate over all files in the isolated workspace root
60
+ const files = (0, utils_1.getAllFiles)(workspaceRoot);
56
61
  for (const file of files) {
57
- const content = fs.readFileSync(file, "utf8");
58
- const regex = /@story\s+([^\s]+)/g;
59
- let match;
60
- // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md // @req REQ-MAINT-DETECT - Iterate over regex matches for @story annotations
61
- while ((match = regex.exec(content)) !== null) {
62
- const storyPath = match[1];
63
- const storyProjectPath = path.resolve(cwd, storyPath);
64
- const storyCodebasePath = path.resolve(baseDir, storyPath);
65
- // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md // @req REQ-MAINT-DETECT - Check if referenced story file is missing in both project and codebase paths
66
- if (!fs.existsSync(storyProjectPath) &&
67
- !fs.existsSync(storyCodebasePath)) {
68
- stale.add(storyPath);
69
- }
70
- }
62
+ processFileForStaleAnnotations(file, workspaceRoot, cwd, stale);
71
63
  }
72
64
  return Array.from(stale);
73
65
  }
66
+ /**
67
+ * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
68
+ * @req REQ-MAINT-DETECT - Process individual files to detect stale @story annotations
69
+ */
70
+ function processFileForStaleAnnotations(file, workspaceRoot, cwd, stale) {
71
+ let content;
72
+ // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
73
+ // @req REQ-MAINT-DETECT - Handle file read errors gracefully
74
+ try {
75
+ content = fs.readFileSync(file, "utf8");
76
+ }
77
+ catch {
78
+ return;
79
+ }
80
+ const regex = /@story\s+([^\s]+)/g;
81
+ let match;
82
+ // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
83
+ // @req REQ-MAINT-DETECT - Iterate over regex matches for @story annotations
84
+ while ((match = regex.exec(content)) !== null) {
85
+ handleStoryMatch(match[1], workspaceRoot, cwd, stale);
86
+ }
87
+ }
88
+ /**
89
+ * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
90
+ * @req REQ-MAINT-DETECT - Handle individual @story matches within a file
91
+ */
92
+ function handleStoryMatch(storyPath, workspaceRoot, cwd, stale) {
93
+ // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
94
+ // @req REQ-MAINT-DETECT - Skip traversal/absolute-unsafe story paths before any filesystem or boundary checks
95
+ if ((0, storyReferenceUtils_1.isTraversalUnsafe)(storyPath)) {
96
+ return;
97
+ }
98
+ // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
99
+ // @req REQ-MAINT-DETECT - Compute project and codebase candidates relative to cwd and workspaceRoot
100
+ const storyProjectCandidate = path.resolve(cwd, storyPath);
101
+ const storyCodebaseCandidate = path.resolve(workspaceRoot, storyPath);
102
+ // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
103
+ // @req REQ-MAINT-DETECT - Enforce workspaceRoot as the project boundary for resolved story paths
104
+ let projectBoundary;
105
+ let codebaseBoundary;
106
+ try {
107
+ projectBoundary = (0, storyReferenceUtils_1.enforceProjectBoundary)(storyProjectCandidate, workspaceRoot);
108
+ }
109
+ catch {
110
+ projectBoundary = {
111
+ isWithinProject: false,
112
+ candidate: storyProjectCandidate,
113
+ };
114
+ }
115
+ try {
116
+ codebaseBoundary = (0, storyReferenceUtils_1.enforceProjectBoundary)(storyCodebaseCandidate, workspaceRoot);
117
+ }
118
+ catch {
119
+ codebaseBoundary = {
120
+ isWithinProject: false,
121
+ candidate: storyCodebaseCandidate,
122
+ };
123
+ }
124
+ const inProjectCandidates = [];
125
+ if (projectBoundary.isWithinProject) {
126
+ inProjectCandidates.push(projectBoundary.candidate);
127
+ }
128
+ if (codebaseBoundary.isWithinProject) {
129
+ inProjectCandidates.push(codebaseBoundary.candidate);
130
+ }
131
+ // If both candidates are out-of-project, do not mark as stale and skip FS checks
132
+ if (inProjectCandidates.length === 0) {
133
+ return;
134
+ }
135
+ // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
136
+ // @req REQ-MAINT-DETECT - Only check existence for in-project candidates
137
+ const anyExists = inProjectCandidates.some((p) => fs.existsSync(p));
138
+ // @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
139
+ // @req REQ-MAINT-DETECT - Mark story as stale if any in-project candidate exists conceptually but none exist on disk
140
+ if (!anyExists) {
141
+ stale.add(storyPath);
142
+ }
143
+ }
@@ -31,13 +31,22 @@ function linesBeforeHasStory(sourceCode, node, lookback = exports.LOOKBACK_LINES
31
31
  const startLine = node && node.loc && typeof node.loc.start?.line === "number"
32
32
  ? node.loc.start.line
33
33
  : null;
34
+ // Guard against missing or non-array source lines or an invalid start line before scanning.
35
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
36
+ // @req REQ-ANNOTATION-REQUIRED - Fail gracefully when source lines or locations are unavailable
34
37
  if (!Array.isArray(lines) || typeof startLine !== "number") {
35
38
  return false;
36
39
  }
37
40
  const from = Math.max(0, startLine - 1 - lookback);
38
41
  const to = Math.max(0, startLine - 1);
42
+ // Walk each physical line in the configured lookback window to search for an inline @story marker.
43
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
44
+ // @req REQ-ANNOTATION-REQUIRED - Scan preceding lines for existing story annotations
39
45
  for (let i = from; i < to; i++) {
40
46
  const text = lines[i];
47
+ // Treat any line containing "@story" as evidence that the function is already annotated.
48
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
49
+ // @req REQ-ANNOTATION-REQUIRED - Detect explicit @story markers in raw source text
41
50
  if (typeof text === "string" && text.includes("@story")) {
42
51
  return true;
43
52
  }
@@ -84,24 +93,39 @@ function parentChainHasStory(sourceCode, node) {
84
93
  * @req REQ-ANNOTATION-REQUIRED - Provide fallback textual inspection when other heuristics fail
85
94
  */
86
95
  function fallbackTextBeforeHasStory(sourceCode, node) {
96
+ // Skip fallback text inspection when the sourceCode API or node range information is not available.
97
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
98
+ // @req REQ-ANNOTATION-REQUIRED - Avoid throwing when source text or range metadata cannot be read
87
99
  if (typeof sourceCode?.getText !== "function" ||
88
100
  !Array.isArray((node && node.range) || [])) {
89
101
  return false;
90
102
  }
91
103
  const range = node.range;
104
+ // Guard against malformed range values that cannot provide a numeric start index for slicing.
105
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
106
+ // @req REQ-ANNOTATION-REQUIRED - Validate node range structure before computing fallback window
92
107
  if (!Array.isArray(range) || typeof range[0] !== "number") {
93
108
  return false;
94
109
  }
95
110
  try {
96
- // Limit the fallback inspect window to a reasonable size
111
+ // Limit the fallback inspection window to a bounded region immediately preceding the node.
112
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
113
+ // @req REQ-ANNOTATION-REQUIRED - Restrict fallback text scanning to a safe, fixed-size window
97
114
  const start = Math.max(0, range[0] - exports.FALLBACK_WINDOW);
98
115
  const textBefore = sourceCode.getText().slice(start, range[0]);
116
+ // Detect any @story marker that appears within the bounded fallback window.
117
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
118
+ // @req REQ-ANNOTATION-REQUIRED - Recognize story annotations discovered via fallback text scanning
99
119
  if (typeof textBefore === "string" && textBefore.includes("@story")) {
100
120
  return true;
101
121
  }
102
122
  }
103
123
  catch {
104
- /* noop */
124
+ /*
125
+ * Swallow low-level IO or slicing errors so annotation detection never breaks lint execution.
126
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
127
+ * @req REQ-ANNOTATION-REQUIRED - Treat fallback text inspection failures as "no annotation" instead of raising
128
+ */
105
129
  }
106
130
  return false;
107
131
  }
@@ -26,7 +26,7 @@
26
26
  * Get a readable name for a given AST node.
27
27
  *
28
28
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
29
- * @req REQ-ANNOTATION-REQUIRED
29
+ * @req REQ-ANNOTATION-REQUIRED - Provide a unified way to obtain a stable, human-readable name from AST nodes
30
30
  *
31
31
  * @param node - An AST node (ESTree/TSESTree/JSX-like). Can be null/undefined.
32
32
  * @returns The resolved name string, or null if a stable name cannot be determined.
@@ -29,7 +29,7 @@ exports.getNodeName = getNodeName;
29
29
  * Check for identifier-like nodes and return their name when available.
30
30
  *
31
31
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
32
- * @req REQ-ANNOTATION-REQUIRED
32
+ * @req REQ-ANNOTATION-REQUIRED - Recognize Identifier/JSXIdentifier nodes and return their name
33
33
  *
34
34
  * @param node - AST node to inspect
35
35
  * @returns the identifier name or null
@@ -47,7 +47,7 @@ function isIdentifierLike(node) {
47
47
  * Convert a Literal node to a string when it represents a stable primitive (string/number/boolean).
48
48
  *
49
49
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
50
- * @req REQ-ANNOTATION-REQUIRED
50
+ * @req REQ-ANNOTATION-REQUIRED - Convert simple Literal nodes into stable string names when possible
51
51
  *
52
52
  * @param node - AST node expected to be a Literal
53
53
  * @returns the literal as string or null if not stable/resolvable
@@ -67,7 +67,7 @@ function literalToString(node) {
67
67
  * Convert a TemplateLiteral node to a string if it contains no expressions.
68
68
  *
69
69
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
70
- * @req REQ-ANNOTATION-REQUIRED
70
+ * @req REQ-ANNOTATION-REQUIRED - Support simple, expression-free TemplateLiteral names for reporting
71
71
  *
72
72
  * @param node - AST node expected to be a TemplateLiteral
73
73
  * @returns the cooked/raw concatenated template string or null if it contains expressions
@@ -95,7 +95,7 @@ function templateLiteralToString(node) {
95
95
  * Resolve a MemberExpression / TSQualifiedName / JSXMemberExpression-like node to a name when non-computed.
96
96
  *
97
97
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
98
- * @req REQ-ANNOTATION-REQUIRED
98
+ * @req REQ-ANNOTATION-REQUIRED - Resolve non-computed member-like nodes into property names when safe
99
99
  *
100
100
  * @param node - AST node to inspect
101
101
  * @returns resolved member/property name or null
@@ -121,7 +121,7 @@ function memberExpressionName(node) {
121
121
  * Extract the key name from Property/ObjectProperty nodes.
122
122
  *
123
123
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
124
- * @req REQ-ANNOTATION-REQUIRED
124
+ * @req REQ-ANNOTATION-REQUIRED - Extract key names from Property/ObjectProperty nodes used in function containers
125
125
  *
126
126
  * @param node - AST node expected to be Property/ObjectProperty
127
127
  * @returns the resolved key name or null
@@ -141,7 +141,7 @@ function propertyKeyName(node) {
141
141
  * Branch-level traceability: prefer direct .key.name early (common on variable declarators, properties)
142
142
  *
143
143
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
144
- * @req REQ-ANNOTATION-REQUIRED
144
+ * @req REQ-ANNOTATION-REQUIRED - Prefer direct id/key names before falling back to deeper AST inspection
145
145
  *
146
146
  * @param node - AST node to inspect for direct .id/.key name
147
147
  * @returns the resolved direct name or null
@@ -173,7 +173,7 @@ function directName(node) {
173
173
  * Get a readable name for a given AST node.
174
174
  *
175
175
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
176
- * @req REQ-ANNOTATION-REQUIRED
176
+ * @req REQ-ANNOTATION-REQUIRED - Provide a unified way to obtain a stable, human-readable name from AST nodes
177
177
  *
178
178
  * @param node - An AST node (ESTree/TSESTree/JSX-like). Can be null/undefined.
179
179
  * @returns The resolved name string, or null if a stable name cannot be determined.
@@ -13,6 +13,12 @@ const require_story_helpers_1 = require("./require-story-helpers");
13
13
  * @req REQ-BUILD-VISITORS-FNDECL - Provide visitor for FunctionDeclaration
14
14
  */
15
15
  function buildFunctionDeclarationVisitor(context, sourceCode, options) {
16
+ /**
17
+ * Debug flag for optional visitor logging.
18
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
19
+ * @req REQ-DEBUG-LOG-TOGGLE - Allow opt-in debug logging via TRACEABILITY_DEBUG
20
+ */
21
+ const debugEnabled = process.env.TRACEABILITY_DEBUG === "1";
16
22
  /**
17
23
  * Handle FunctionDeclaration nodes.
18
24
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
@@ -24,9 +30,11 @@ function buildFunctionDeclarationVisitor(context, sourceCode, options) {
24
30
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
25
31
  * @req REQ-DEBUG-LOG - Provide debug logging for visitor entry
26
32
  */
27
- console.debug("require-story-annotation:FunctionDeclaration", typeof context.getFilename === "function"
28
- ? context.getFilename()
29
- : "<unknown>", node && node.id ? node.id.name : "<anonymous>");
33
+ if (debugEnabled) {
34
+ console.debug("require-story-annotation:FunctionDeclaration", typeof context.getFilename === "function"
35
+ ? context.getFilename()
36
+ : "<unknown>", node && node.id ? node.id.name : "<anonymous>");
37
+ }
30
38
  if (!options.shouldProcessNode(node))
31
39
  return;
32
40
  const target = (0, require_story_helpers_1.resolveTargetNode)(sourceCode, node);
@@ -63,15 +63,25 @@ const rule = {
63
63
  const opts = (context.options && context.options[0]) || {};
64
64
  const scope = opts.scope || require_story_helpers_1.DEFAULT_SCOPE;
65
65
  const exportPriority = opts.exportPriority || "all";
66
+ /**
67
+ * Environment-gated debug logging to avoid leaking file paths unless
68
+ * explicitly enabled.
69
+ *
70
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
71
+ * @req REQ-DEBUG-LOG
72
+ */
73
+ const debugEnabled = process.env.TRACEABILITY_DEBUG === "1";
66
74
  /**
67
75
  * Debug log at the start of create to help diagnose rule activation in tests.
68
76
  *
69
77
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
70
78
  * @req REQ-DEBUG-LOG
71
79
  */
72
- console.debug("require-story-annotation:create", typeof context.getFilename === "function"
73
- ? context.getFilename()
74
- : "<unknown>");
80
+ if (debugEnabled) {
81
+ console.debug("require-story-annotation:create", typeof context.getFilename === "function"
82
+ ? context.getFilename()
83
+ : "<unknown>");
84
+ }
75
85
  // Local closure that binds configured scope and export priority to the helper.
76
86
  const should = (node) => (0, require_story_helpers_1.shouldProcessNode)(node, scope, exportPriority);
77
87
  // Delegate visitor construction to helper to keep this file concise.
@@ -36,18 +36,37 @@ function validateStoryPath(opts) {
36
36
  });
37
37
  }
38
38
  /**
39
- * Report any problems related to the existence or accessibility of the
40
- * referenced story file. Filesystem and I/O errors are surfaced with a
41
- * dedicated diagnostic that differentiates them from missing files.
39
+ * Analyze candidate paths against the project boundary, returning whether any
40
+ * are within the project and whether any are outside.
41
+ *
42
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
43
+ * @req REQ-PROJECT-BOUNDARY - Validate files are within project boundaries
44
+ * @req REQ-CONFIGURABLE-PATHS - Respect configured storyDirectories while enforcing project boundaries
45
+ */
46
+ function analyzeCandidateBoundaries(candidates, cwd) {
47
+ let hasInProjectCandidate = false;
48
+ let hasOutOfProjectCandidate = false;
49
+ for (const candidate of candidates) {
50
+ const boundary = (0, storyReferenceUtils_1.enforceProjectBoundary)(candidate, cwd);
51
+ if (boundary.isWithinProject) {
52
+ hasInProjectCandidate = true;
53
+ }
54
+ else {
55
+ hasOutOfProjectCandidate = true;
56
+ }
57
+ }
58
+ return { hasInProjectCandidate, hasOutOfProjectCandidate };
59
+ }
60
+ /**
61
+ * Handle existence status and report appropriate diagnostics for missing
62
+ * or filesystem-error conditions, assuming project-boundary checks have
63
+ * already been applied.
42
64
  *
43
65
  * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
44
66
  * @req REQ-FILE-EXISTENCE - Ensure referenced files exist
45
67
  * @req REQ-ERROR-HANDLING - Differentiate missing files from filesystem errors
46
68
  */
47
- function reportExistenceProblems(opts) {
48
- const { storyPath, commentNode, context, cwd, storyDirs } = opts;
49
- const result = (0, storyReferenceUtils_1.normalizeStoryPath)(storyPath, cwd, storyDirs);
50
- const existenceResult = result.existence;
69
+ function reportExistenceStatus(existenceResult, storyPath, commentNode, context) {
51
70
  if (!existenceResult || existenceResult.status === "exists") {
52
71
  return;
53
72
  }
@@ -81,6 +100,48 @@ function reportExistenceProblems(opts) {
81
100
  });
82
101
  }
83
102
  }
103
+ /**
104
+ * Report any problems related to the existence or accessibility of the
105
+ * referenced story file. Filesystem and I/O errors are surfaced with a
106
+ * dedicated diagnostic that differentiates them from missing files.
107
+ *
108
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
109
+ * @req REQ-FILE-EXISTENCE - Ensure referenced files exist
110
+ * @req REQ-ERROR-HANDLING - Differentiate missing files from filesystem errors
111
+ * @req REQ-PROJECT-BOUNDARY - Ensure resolved candidate paths remain within the project root
112
+ * @req REQ-CONFIGURABLE-PATHS - Respect configured storyDirectories while enforcing project boundaries
113
+ */
114
+ function reportExistenceProblems(opts) {
115
+ const { storyPath, commentNode, context, cwd, storyDirs } = opts;
116
+ const result = (0, storyReferenceUtils_1.normalizeStoryPath)(storyPath, cwd, storyDirs);
117
+ const existenceResult = result.existence;
118
+ const candidates = result.candidates || [];
119
+ if (candidates.length > 0) {
120
+ const { hasInProjectCandidate, hasOutOfProjectCandidate } = analyzeCandidateBoundaries(candidates, cwd);
121
+ if (hasOutOfProjectCandidate && !hasInProjectCandidate) {
122
+ context.report({
123
+ node: commentNode,
124
+ messageId: "invalidPath",
125
+ data: { path: storyPath },
126
+ });
127
+ return;
128
+ }
129
+ }
130
+ if (existenceResult &&
131
+ existenceResult.status === "exists" &&
132
+ existenceResult.matchedPath) {
133
+ const boundary = (0, storyReferenceUtils_1.enforceProjectBoundary)(existenceResult.matchedPath, cwd);
134
+ if (!boundary.isWithinProject) {
135
+ context.report({
136
+ node: commentNode,
137
+ messageId: "invalidPath",
138
+ data: { path: storyPath },
139
+ });
140
+ return;
141
+ }
142
+ }
143
+ reportExistenceStatus(existenceResult, storyPath, commentNode, context);
144
+ }
84
145
  /**
85
146
  * Process and validate the story path for security, extension, and existence.
86
147
  * Filesystem and I/O errors are handled inside the underlying utilities
@@ -103,8 +164,10 @@ function processStoryPath(opts) {
103
164
  messageId: "invalidPath",
104
165
  data: { path: storyPath },
105
166
  });
167
+ return;
106
168
  }
107
- return;
169
+ // When absolute paths are allowed, we still enforce extension and
170
+ // project-boundary checks below via the existence phase.
108
171
  }
109
172
  // Path traversal check
110
173
  if ((0, storyReferenceUtils_1.containsPathTraversal)(storyPath)) {
@@ -223,7 +286,7 @@ exports.default = {
223
286
  ],
224
287
  },
225
288
  create(context) {
226
- const cwd = process.cwd();
289
+ const cwd = context.cwd ?? process.cwd();
227
290
  const opts = context.options[0];
228
291
  const storyDirs = opts?.storyDirectories || defaultStoryDirs;
229
292
  const allowAbsolute = opts?.allowAbsolutePaths || false;
@@ -6,6 +6,10 @@
6
6
  * @req REQ-TYPESCRIPT-SUPPORT - Support TypeScript-specific function syntax
7
7
  * @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
8
8
  * @req REQ-ANNOTATION-REPORTING - Report missing @req annotation to context
9
+ * @param context - ESLint rule context used to obtain source and report problems
10
+ * @param node - Function-like AST node whose surrounding comments should be inspected
11
+ * @param options - Optional configuration controlling behaviour (e.g., enableFix)
12
+ * @returns void
9
13
  */
10
14
  export declare function checkReqAnnotation(context: any, node: any, options?: {
11
15
  enableFix?: boolean;
@@ -45,19 +45,30 @@ function commentContainsReq(c) {
45
45
  }
46
46
  /**
47
47
  * Line-based helper adapted from linesBeforeHasStory to detect @req.
48
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
49
+ * @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in preceding source lines
48
50
  */
49
51
  function linesBeforeHasReq(sourceCode, node) {
50
52
  const lines = sourceCode && sourceCode.lines;
51
53
  const startLine = node && node.loc && typeof node.loc.start?.line === "number"
52
54
  ? node.loc.start.line
53
55
  : null;
56
+ // Guard against missing or malformed source/loc information before scanning.
57
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
58
+ // @req REQ-ANNOTATION-REQ-DETECTION - Avoid false positives when sourceCode/loc is incomplete
54
59
  if (!Array.isArray(lines) || typeof startLine !== "number") {
55
60
  return false;
56
61
  }
57
62
  const from = Math.max(0, startLine - 1 - require_story_io_1.LOOKBACK_LINES);
58
63
  const to = Math.max(0, startLine - 1);
64
+ // Scan each physical line in the configured lookback window for an @req marker.
65
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
66
+ // @req REQ-ANNOTATION-REQ-DETECTION - Search preceding lines for @req text
59
67
  for (let i = from; i < to; i++) {
60
68
  const text = lines[i];
69
+ // When a line contains @req we treat the function as already annotated.
70
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
71
+ // @req REQ-ANNOTATION-REQ-DETECTION - Detect @req marker in raw source lines
61
72
  if (typeof text === "string" && text.includes("@req")) {
62
73
  return true;
63
74
  }
@@ -66,20 +77,29 @@ function linesBeforeHasReq(sourceCode, node) {
66
77
  }
67
78
  /**
68
79
  * Parent-chain helper adapted from parentChainHasStory to detect @req.
80
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
81
+ * @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in parent-chain comments
69
82
  */
70
83
  function parentChainHasReq(sourceCode, node) {
71
84
  let p = node && node.parent;
85
+ // Walk up the parent chain and inspect comments attached to each ancestor.
86
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
87
+ // @req REQ-ANNOTATION-REQ-DETECTION - Traverse parent nodes when local comments are absent
72
88
  while (p) {
73
89
  const pComments = typeof sourceCode?.getCommentsBefore === "function"
74
90
  ? sourceCode.getCommentsBefore(p) || []
75
91
  : [];
76
- if (Array.isArray(pComments) &&
77
- pComments.some((c) => typeof c.value === "string" && c.value.includes("@req"))) {
92
+ // Look for @req in comments immediately preceding each parent node.
93
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
94
+ // @req REQ-ANNOTATION-REQ-DETECTION - Detect @req markers in parent comments
95
+ if (Array.isArray(pComments) && pComments.some(commentContainsReq)) {
78
96
  return true;
79
97
  }
80
98
  const pLeading = p.leadingComments || [];
81
- if (Array.isArray(pLeading) &&
82
- pLeading.some((c) => typeof c.value === "string" && c.value.includes("@req"))) {
99
+ // Also inspect leadingComments attached directly to the parent node.
100
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
101
+ // @req REQ-ANNOTATION-REQ-DETECTION - Detect @req markers in parent leadingComments
102
+ if (Array.isArray(pLeading) && pLeading.some(commentContainsReq)) {
83
103
  return true;
84
104
  }
85
105
  p = p.parent;
@@ -88,24 +108,38 @@ function parentChainHasReq(sourceCode, node) {
88
108
  }
89
109
  /**
90
110
  * Fallback text window helper adapted from fallbackTextBeforeHasStory to detect @req.
111
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
112
+ * @req REQ-ANNOTATION-REQ-DETECTION - Detect @req in fallback text window before node
91
113
  */
92
114
  function fallbackTextBeforeHasReq(sourceCode, node) {
115
+ // Guard against unsupported sourceCode or nodes without a usable range.
116
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
117
+ // @req REQ-ANNOTATION-REQ-DETECTION - Ensure we only inspect text when range information is available
93
118
  if (typeof sourceCode?.getText !== "function" ||
94
119
  !Array.isArray((node && node.range) || [])) {
95
120
  return false;
96
121
  }
97
122
  const range = node.range;
123
+ // Guard when the node range cannot provide a numeric start index.
124
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
125
+ // @req REQ-ANNOTATION-REQ-DETECTION - Avoid scanning when range start is not a number
98
126
  if (!Array.isArray(range) || typeof range[0] !== "number") {
99
127
  return false;
100
128
  }
101
129
  try {
102
130
  const start = Math.max(0, range[0] - require_story_io_1.FALLBACK_WINDOW);
103
131
  const textBefore = sourceCode.getText().slice(start, range[0]);
132
+ // Detect @req in the bounded text window immediately preceding the node.
133
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
134
+ // @req REQ-ANNOTATION-REQ-DETECTION - Detect @req marker in fallback text window
104
135
  if (typeof textBefore === "string" && textBefore.includes("@req")) {
105
136
  return true;
106
137
  }
107
138
  }
108
139
  catch {
140
+ // Swallow detection errors to avoid breaking lint runs due to malformed source.
141
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
142
+ // @req REQ-ANNOTATION-REQ-DETECTION - Treat IO/detection failures as "no annotation" instead of throwing
109
143
  /* noop */
110
144
  }
111
145
  return false;
@@ -120,6 +154,9 @@ function hasReqAnnotation(jsdoc, comments, context, node) {
120
154
  const sourceCode = context && typeof context.getSourceCode === "function"
121
155
  ? context.getSourceCode()
122
156
  : undefined;
157
+ // Prefer robust, location-based heuristics when sourceCode and node are available.
158
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
159
+ // @req REQ-ANNOTATION-REQ-DETECTION - Use multiple heuristics to detect @req markers around the node
123
160
  if (sourceCode && node) {
124
161
  if (linesBeforeHasReq(sourceCode, node) ||
125
162
  parentChainHasReq(sourceCode, node) ||
@@ -130,6 +167,8 @@ function hasReqAnnotation(jsdoc, comments, context, node) {
130
167
  }
131
168
  catch {
132
169
  // Swallow detection errors and fall through to simple checks.
170
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
171
+ // @req REQ-ANNOTATION-REQ-DETECTION - Fail gracefully when advanced detection heuristics throw
133
172
  }
134
173
  // BRANCH @req detection on JSDoc or comments
135
174
  // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
@@ -148,19 +187,28 @@ function hasReqAnnotation(jsdoc, comments, context, node) {
148
187
  */
149
188
  function getFixTargetNode(node) {
150
189
  const parent = node && node.parent;
190
+ // When there is no parent, attach the annotation directly to the node itself.
191
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
192
+ // @req REQ-ANNOTATION-AUTOFIX - Default to annotating the node when it has no parent
151
193
  if (!parent) {
152
194
  return node;
153
195
  }
154
196
  // If the node is part of a class/obj method definition, attach to the MethodDefinition
197
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
198
+ // @req REQ-ANNOTATION-AUTOFIX - Attach fixes to the MethodDefinition wrapper for methods
155
199
  if (parent.type === "MethodDefinition") {
156
200
  return parent;
157
201
  }
158
202
  // If the node is the init of a variable declarator, attach to the VariableDeclarator
203
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
204
+ // @req REQ-ANNOTATION-AUTOFIX - Attach fixes to the VariableDeclarator for function initializers
159
205
  if (parent.type === "VariableDeclarator" && parent.init === node) {
160
206
  return parent;
161
207
  }
162
208
  // If the parent is an expression statement (e.g. IIFE or assigned via expression),
163
209
  // attach to the outer ExpressionStatement.
210
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
211
+ // @req REQ-ANNOTATION-AUTOFIX - Attach fixes to the ExpressionStatement wrapper for IIFEs
164
212
  if (parent.type === "ExpressionStatement") {
165
213
  return parent;
166
214
  }
@@ -174,6 +222,11 @@ function getFixTargetNode(node) {
174
222
  */
175
223
  function createMissingReqFix(node) {
176
224
  const target = getFixTargetNode(node);
225
+ /**
226
+ * Fixer used to insert a default @req annotation before the chosen target node.
227
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
228
+ * @req REQ-ANNOTATION-AUTOFIX - Provide autofix for missing @req annotation
229
+ */
177
230
  return function missingReqFix(fixer) {
178
231
  return fixer.insertTextBefore(target, "/** @req <REQ-ID> */\n");
179
232
  };
@@ -202,6 +255,9 @@ function reportMissing(context, node, enableFix = true) {
202
255
  messageId: "missingReq",
203
256
  data: { name, functionName: name },
204
257
  };
258
+ // Conditionally attach an autofix only when enabled in the rule options.
259
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
260
+ // @req REQ-ANNOTATION-AUTOFIX - Only provide autofix suggestions when explicitly enabled
205
261
  if (enableFix) {
206
262
  reportOptions.fix = createMissingReqFix(node);
207
263
  }
@@ -215,6 +271,10 @@ function reportMissing(context, node, enableFix = true) {
215
271
  * @req REQ-TYPESCRIPT-SUPPORT - Support TypeScript-specific function syntax
216
272
  * @req REQ-ANNOTATION-REQ-DETECTION - Determine presence of @req annotation
217
273
  * @req REQ-ANNOTATION-REPORTING - Report missing @req annotation to context
274
+ * @param context - ESLint rule context used to obtain source and report problems
275
+ * @param node - Function-like AST node whose surrounding comments should be inspected
276
+ * @param options - Optional configuration controlling behaviour (e.g., enableFix)
277
+ * @returns void
218
278
  */
219
279
  function checkReqAnnotation(context, node, options) {
220
280
  const { enableFix = true } = options ?? {};
@@ -31,6 +31,34 @@ export interface StoryExistenceResult {
31
31
  matchedPath?: string;
32
32
  error?: unknown;
33
33
  }
34
+ /**
35
+ * Result of validating that a candidate path stays within the project boundary.
36
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
37
+ * @req REQ-PROJECT-BOUNDARY - Validate files are within project boundaries
38
+ */
39
+ export interface ProjectBoundaryCheckResult {
40
+ candidate: string;
41
+ isWithinProject: boolean;
42
+ }
43
+ /**
44
+ * Validate that a candidate path stays within the project boundary.
45
+ * This compares the resolved candidate path against the normalized cwd
46
+ * prefix, ensuring that even when storyDirectories are misconfigured, we
47
+ * never treat files outside the project as valid story references.
48
+ *
49
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
50
+ * @req REQ-PROJECT-BOUNDARY - Validate files are within project boundaries
51
+ */
52
+ export declare function enforceProjectBoundary(candidate: string, cwd: string): ProjectBoundaryCheckResult;
53
+ /**
54
+ * Internal helper to reset the filesystem existence cache. This is primarily
55
+ * intended for tests that need to run multiple scenarios with different
56
+ * mocked filesystem behavior without carrying over cached results.
57
+ *
58
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
59
+ * @req REQ-PERFORMANCE-OPTIMIZATION - Allow safe cache reset in tests to avoid stale entries
60
+ */
61
+ export declare function __resetStoryExistenceCacheForTests(): void;
34
62
  /**
35
63
  * Build candidate file paths for a given story path.
36
64
  * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
@@ -3,6 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.enforceProjectBoundary = enforceProjectBoundary;
7
+ exports.__resetStoryExistenceCacheForTests = __resetStoryExistenceCacheForTests;
6
8
  exports.buildStoryCandidates = buildStoryCandidates;
7
9
  exports.getStoryExistence = getStoryExistence;
8
10
  exports.storyExists = storyExists;
@@ -22,6 +24,36 @@ exports.isUnsafeStoryPath = isUnsafeStoryPath;
22
24
  */
23
25
  const fs_1 = __importDefault(require("fs"));
24
26
  const path_1 = __importDefault(require("path"));
27
+ /**
28
+ * Validate that a candidate path stays within the project boundary.
29
+ * This compares the resolved candidate path against the normalized cwd
30
+ * prefix, ensuring that even when storyDirectories are misconfigured, we
31
+ * never treat files outside the project as valid story references.
32
+ *
33
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
34
+ * @req REQ-PROJECT-BOUNDARY - Validate files are within project boundaries
35
+ */
36
+ function enforceProjectBoundary(candidate, cwd) {
37
+ const normalizedCwd = path_1.default.resolve(cwd);
38
+ const normalizedCandidate = path_1.default.resolve(candidate);
39
+ const isWithinProject = normalizedCandidate === normalizedCwd ||
40
+ normalizedCandidate.startsWith(normalizedCwd + path_1.default.sep);
41
+ return {
42
+ candidate: normalizedCandidate,
43
+ isWithinProject,
44
+ };
45
+ }
46
+ /**
47
+ * Internal helper to reset the filesystem existence cache. This is primarily
48
+ * intended for tests that need to run multiple scenarios with different
49
+ * mocked filesystem behavior without carrying over cached results.
50
+ *
51
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
52
+ * @req REQ-PERFORMANCE-OPTIMIZATION - Allow safe cache reset in tests to avoid stale entries
53
+ */
54
+ function __resetStoryExistenceCacheForTests() {
55
+ fileExistStatusCache.clear();
56
+ }
25
57
  /**
26
58
  * Build candidate file paths for a given story path.
27
59
  * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
@@ -29,11 +61,20 @@ const path_1 = __importDefault(require("path"));
29
61
  */
30
62
  function buildStoryCandidates(storyPath, cwd, storyDirs) {
31
63
  const candidates = [];
64
+ // When the story path is already explicitly relative, resolve it only against the current working directory.
65
+ // @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
66
+ // @req REQ-PATH-RESOLUTION - Preserve explicit relative story path semantics when building candidate locations
32
67
  if (storyPath.startsWith("./") || storyPath.startsWith("../")) {
33
68
  candidates.push(path_1.default.resolve(cwd, storyPath));
34
69
  }
35
70
  else {
71
+ // For bare paths, first try resolving directly under the current working directory.
72
+ // @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
73
+ // @req REQ-PATH-RESOLUTION - Attempt direct resolution from cwd before probing configured story directories
36
74
  candidates.push(path_1.default.resolve(cwd, storyPath));
75
+ // Probe each configured story directory for a matching story filename.
76
+ // @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
77
+ // @req REQ-PATH-RESOLUTION - Expand search across configured storyDirectories while staying within project
37
78
  for (const dir of storyDirs) {
38
79
  candidates.push(path_1.default.resolve(cwd, dir, path_1.default.basename(storyPath)));
39
80
  }
@@ -59,17 +100,26 @@ const fileExistStatusCache = new Map();
59
100
  */
60
101
  function checkSingleCandidate(candidate) {
61
102
  const cached = fileExistStatusCache.get(candidate);
103
+ // Reuse any cached filesystem result to avoid redundant disk IO for the same candidate.
104
+ // @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
105
+ // @req REQ-PERFORMANCE-OPTIMIZATION - Short-circuit on cached existence checks
62
106
  if (cached) {
63
107
  return cached;
64
108
  }
65
109
  let result;
66
110
  try {
67
111
  const exists = fs_1.default.existsSync(candidate);
112
+ // When the path does not exist at all, record a simple "missing" status.
113
+ // @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
114
+ // @req REQ-FILE-EXISTENCE - Distinguish non-existent story paths from other failure modes
68
115
  if (!exists) {
69
116
  result = { path: candidate, status: "missing" };
70
117
  }
71
118
  else {
72
119
  const stat = fs_1.default.statSync(candidate);
120
+ // Treat existing regular files as valid story candidates; other entry types are considered missing.
121
+ // @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
122
+ // @req REQ-FILE-EXISTENCE - Only regular files may satisfy a story path reference
73
123
  if (stat.isFile()) {
74
124
  result = { path: candidate, status: "exists" };
75
125
  }
@@ -80,7 +130,9 @@ function checkSingleCandidate(candidate) {
80
130
  }
81
131
  }
82
132
  catch (error) {
83
- // Any filesystem error is captured and surfaced as fs-error.
133
+ // Any filesystem error is captured and surfaced as an fs-error status instead of throwing.
134
+ // @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
135
+ // @req REQ-ERROR-HANDLING - Represent filesystem failures as fs-error results while keeping callers resilient
84
136
  result = { path: candidate, status: "fs-error", error };
85
137
  }
86
138
  fileExistStatusCache.set(candidate, result);
@@ -104,6 +156,9 @@ function getStoryExistence(candidates) {
104
156
  let firstFsError;
105
157
  for (const candidate of candidates) {
106
158
  const res = checkSingleCandidate(candidate);
159
+ // As soon as a candidate file is confirmed to exist, return a successful existence result.
160
+ // @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
161
+ // @req REQ-FILE-EXISTENCE - Prefer the first positively-matched story file
107
162
  if (res.status === "exists") {
108
163
  return {
109
164
  candidates,
@@ -111,10 +166,16 @@ function getStoryExistence(candidates) {
111
166
  matchedPath: res.path,
112
167
  };
113
168
  }
169
+ // Remember the first filesystem error so callers can inspect a representative failure if no files exist.
170
+ // @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
171
+ // @req REQ-ERROR-HANDLING - Surface a single representative filesystem error without failing fast
114
172
  if (res.status === "fs-error" && !firstFsError) {
115
173
  firstFsError = res;
116
174
  }
117
175
  }
176
+ // Prefer reporting a filesystem error over a generic missing status when at least one candidate failed to read.
177
+ // @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
178
+ // @req REQ-ERROR-HANDLING - Distinguish IO failures from simple "missing" results in existence checks
118
179
  if (firstFsError) {
119
180
  return {
120
181
  candidates,
@@ -1,4 +1,37 @@
1
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
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -16,6 +49,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
16
49
  const eslint_1 = require("eslint");
17
50
  const valid_story_reference_1 = __importDefault(require("../../src/rules/valid-story-reference"));
18
51
  const storyReferenceUtils_1 = require("../../src/utils/storyReferenceUtils");
52
+ const path = __importStar(require("path"));
19
53
  const ruleTester = new eslint_1.RuleTester({
20
54
  languageOptions: { parserOptions: { ecmaVersion: 2020 } },
21
55
  });
@@ -73,6 +107,233 @@ describe("Valid Story Reference Rule (Story 006.0-DEV-FILE-VALIDATION)", () => {
73
107
  ],
74
108
  });
75
109
  });
110
+ // @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
111
+ // @req REQ-CONFIGURABLE-PATHS - Verify custom storyDirectories behavior
112
+ const configurablePathsTester = new eslint_1.RuleTester({
113
+ languageOptions: { parserOptions: { ecmaVersion: 2020 } },
114
+ });
115
+ configurablePathsTester.run("valid-story-reference", valid_story_reference_1.default, {
116
+ valid: [
117
+ {
118
+ name: "[REQ-CONFIGURABLE-PATHS] honors custom storyDirectories using docs/stories",
119
+ code: `// @story 001.0-DEV-PLUGIN-SETUP.story.md\n// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md`,
120
+ options: [{ storyDirectories: ["docs/stories"] }],
121
+ },
122
+ ],
123
+ invalid: [],
124
+ });
125
+ // @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
126
+ // @req REQ-CONFIGURABLE-PATHS - Verify allowAbsolutePaths behavior
127
+ const allowAbsolutePathsTester = new eslint_1.RuleTester({
128
+ languageOptions: { parserOptions: { ecmaVersion: 2020 } },
129
+ });
130
+ const absoluteStoryPath = path.resolve(process.cwd(), "docs/stories/001.0-DEV-PLUGIN-SETUP.story.md");
131
+ allowAbsolutePathsTester.run("valid-story-reference", valid_story_reference_1.default, {
132
+ valid: [
133
+ {
134
+ name: "[REQ-CONFIGURABLE-PATHS] allowAbsolutePaths accepts existing absolute .story.md inside project",
135
+ code: `// @story ${absoluteStoryPath}\n// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md`,
136
+ options: [
137
+ {
138
+ allowAbsolutePaths: true,
139
+ storyDirectories: ["docs/stories"],
140
+ },
141
+ ],
142
+ },
143
+ ],
144
+ invalid: [
145
+ {
146
+ name: "[REQ-CONFIGURABLE-PATHS] disallows absolute paths when allowAbsolutePaths is false",
147
+ code: `// @story ${absoluteStoryPath}\n// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md`,
148
+ options: [
149
+ {
150
+ allowAbsolutePaths: false,
151
+ storyDirectories: ["docs/stories"],
152
+ },
153
+ ],
154
+ errors: [
155
+ {
156
+ messageId: "invalidPath",
157
+ },
158
+ ],
159
+ },
160
+ ],
161
+ });
162
+ // @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
163
+ // @req REQ-CONFIGURABLE-PATHS - Verify requireStoryExtension behavior
164
+ const relaxedExtensionTester = new eslint_1.RuleTester({
165
+ languageOptions: { parserOptions: { ecmaVersion: 2020 } },
166
+ });
167
+ relaxedExtensionTester.run("valid-story-reference", valid_story_reference_1.default, {
168
+ valid: [
169
+ {
170
+ name: "[REQ-CONFIGURABLE-PATHS] accepts .story.md story path when requireStoryExtension is false (still valid and existing)",
171
+ code: `// @story docs/stories/001.0-DEV-PLUGIN-SETUP.story.md\n// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md`,
172
+ options: [
173
+ {
174
+ storyDirectories: ["docs/stories"],
175
+ requireStoryExtension: false,
176
+ },
177
+ ],
178
+ },
179
+ ],
180
+ invalid: [],
181
+ });
182
+ // @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
183
+ // @req REQ-PROJECT-BOUNDARY - Verify project boundary handling
184
+ const projectBoundaryTester = new eslint_1.RuleTester({
185
+ languageOptions: { parserOptions: { ecmaVersion: 2020 } },
186
+ });
187
+ projectBoundaryTester.run("valid-story-reference", valid_story_reference_1.default, {
188
+ valid: [],
189
+ invalid: [
190
+ {
191
+ name: "[REQ-PROJECT-BOUNDARY] story reference outside project root is rejected when discovered via absolute path",
192
+ code: `// @story ${path.resolve(path.sep, "outside-project", "001.0-DEV-PLUGIN-SETUP.story.md")}\n// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md`,
193
+ options: [
194
+ {
195
+ allowAbsolutePaths: true,
196
+ storyDirectories: [path.resolve(path.sep, "outside-project")],
197
+ },
198
+ ],
199
+ errors: [
200
+ {
201
+ messageId: "invalidPath",
202
+ },
203
+ ],
204
+ },
205
+ ],
206
+ });
207
+ describe("Valid Story Reference Rule Configuration and Boundaries (Story 006.0-DEV-FILE-VALIDATION)", () => {
208
+ const fs = require("fs");
209
+ const pathModule = require("path");
210
+ let tempDirs = [];
211
+ afterEach(() => {
212
+ for (const dir of tempDirs) {
213
+ try {
214
+ fs.rmSync(dir, { recursive: true, force: true });
215
+ }
216
+ catch {
217
+ // ignore cleanup errors
218
+ }
219
+ }
220
+ tempDirs = [];
221
+ (0, storyReferenceUtils_1.__resetStoryExistenceCacheForTests)();
222
+ jest.restoreAllMocks();
223
+ });
224
+ it("[REQ-CONFIGURABLE-PATHS] uses storyDirectories when resolving relative paths (Story 006.0-DEV-FILE-VALIDATION)", () => {
225
+ const storyPath = pathModule.join(process.cwd(), "docs/stories/001.0-DEV-PLUGIN-SETUP.story.md");
226
+ jest.spyOn(fs, "existsSync").mockImplementation((...args) => {
227
+ const p = args[0];
228
+ return p === storyPath;
229
+ });
230
+ jest.spyOn(fs, "statSync").mockImplementation((...args) => {
231
+ const p = args[0];
232
+ if (p === storyPath) {
233
+ return {
234
+ isFile: () => true,
235
+ };
236
+ }
237
+ throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
238
+ });
239
+ const diagnostics = runRuleOnCode(`// @story 001.0-DEV-PLUGIN-SETUP.story.md`, [{ storyDirectories: ["docs/stories"] }]);
240
+ // When storyDirectories is configured, the underlying resolution should
241
+ // treat the path as valid; absence of errors is asserted via RuleTester
242
+ // above. Here we just ensure no crash path via storyExists cache reset.
243
+ expect(Array.isArray(diagnostics)).toBe(true);
244
+ });
245
+ it("[REQ-CONFIGURABLE-PATHS] allowAbsolutePaths permits absolute paths inside project when enabled (Story 006.0-DEV-FILE-VALIDATION)", () => {
246
+ const absPath = pathModule.resolve(process.cwd(), "docs/stories/001.0-DEV-PLUGIN-SETUP.story.md");
247
+ const diagnostics = runRuleOnCode(`// @story ${absPath}`, [
248
+ {
249
+ allowAbsolutePaths: true,
250
+ storyDirectories: ["docs/stories"],
251
+ },
252
+ ]);
253
+ // Detailed behavior is verified by RuleTester above; this Jest test
254
+ // ensures helper path construction does not throw and diagnostics are collected.
255
+ expect(Array.isArray(diagnostics)).toBe(true);
256
+ });
257
+ it("[REQ-PROJECT-BOUNDARY] storyDirectories cannot escape project even when normalize resolves outside cwd (Story 006.0-DEV-FILE-VALIDATION)", () => {
258
+ const ruleModule = require("../../src/rules/valid-story-reference");
259
+ const originalCreate = ruleModule.default.create || ruleModule.create;
260
+ // Spy on create to intercept normalizeStoryPath behavior indirectly if needed
261
+ expect(typeof originalCreate).toBe("function");
262
+ const diagnostics = runRuleOnCode(`// @story ../outside-boundary.story.md\n// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md`);
263
+ // Behavior of reporting invalidPath for outside project is ensured
264
+ // in RuleTester projectBoundaryTester above; here ensure diagnostics collected.
265
+ expect(Array.isArray(diagnostics)).toBe(true);
266
+ });
267
+ /**
268
+ * @req REQ-PROJECT-BOUNDARY - Verify misconfigured storyDirectories pointing outside
269
+ * the project cannot cause external files to be treated as valid, and invalidPath is reported.
270
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
271
+ */
272
+ it("[REQ-PROJECT-BOUNDARY] misconfigured storyDirectories outside project cannot validate external files", () => {
273
+ const fs = require("fs");
274
+ const pathModule = require("path");
275
+ const outsideDir = pathModule.resolve(pathModule.sep, "tmp", "outside");
276
+ const outsideFile = pathModule.join(outsideDir, "external-story.story.md");
277
+ jest.spyOn(fs, "existsSync").mockImplementation((...args) => {
278
+ const p = args[0];
279
+ return p === outsideFile;
280
+ });
281
+ jest.spyOn(fs, "statSync").mockImplementation((...args) => {
282
+ const p = args[0];
283
+ if (p === outsideFile) {
284
+ return {
285
+ isFile: () => true,
286
+ };
287
+ }
288
+ const err = new Error("ENOENT");
289
+ err.code = "ENOENT";
290
+ throw err;
291
+ });
292
+ const diagnostics = runRuleOnCode(`// @story ${outsideFile}\n// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md`, [
293
+ {
294
+ allowAbsolutePaths: true,
295
+ storyDirectories: [outsideDir],
296
+ },
297
+ ]);
298
+ expect(Array.isArray(diagnostics)).toBe(true);
299
+ const invalidPathDiagnostics = diagnostics.filter((d) => d.messageId === "invalidPath");
300
+ expect(invalidPathDiagnostics.length).toBeGreaterThan(0);
301
+ });
302
+ /**
303
+ * @req REQ-CONFIGURABLE-PATHS - Verify requireStoryExtension: false allows .md story
304
+ * files that do not end with .story.md when they exist in storyDirectories.
305
+ * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
306
+ */
307
+ it("[REQ-CONFIGURABLE-PATHS] requireStoryExtension=false accepts existing .md story file", () => {
308
+ const fs = require("fs");
309
+ const pathModule = require("path");
310
+ const storyPath = pathModule.join(process.cwd(), "docs/stories/developer-story.map.md");
311
+ jest.spyOn(fs, "existsSync").mockImplementation((...args) => {
312
+ const p = args[0];
313
+ return p === storyPath;
314
+ });
315
+ jest.spyOn(fs, "statSync").mockImplementation((...args) => {
316
+ const p = args[0];
317
+ if (p === storyPath) {
318
+ return {
319
+ isFile: () => true,
320
+ };
321
+ }
322
+ const err = new Error("ENOENT");
323
+ err.code = "ENOENT";
324
+ throw err;
325
+ });
326
+ const diagnostics = runRuleOnCode(`// @story docs/stories/developer-story.map.md\n// @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md`, [
327
+ {
328
+ storyDirectories: ["docs/stories"],
329
+ requireStoryExtension: false,
330
+ },
331
+ ]);
332
+ expect(Array.isArray(diagnostics)).toBe(true);
333
+ const invalidExtensionDiagnostics = diagnostics.filter((d) => d.messageId === "invalidExtension");
334
+ expect(invalidExtensionDiagnostics.length).toBe(0);
335
+ });
336
+ });
76
337
  /**
77
338
  * Helper to run the valid-story-reference rule against a single source string
78
339
  * and collect reported diagnostics.
@@ -80,7 +341,7 @@ describe("Valid Story Reference Rule (Story 006.0-DEV-FILE-VALIDATION)", () => {
80
341
  * @req REQ-ERROR-HANDLING - Used to verify fileAccessError reporting behavior
81
342
  * @story docs/stories/006.0-DEV-FILE-VALIDATION.story.md
82
343
  */
83
- function runRuleOnCode(code) {
344
+ function runRuleOnCode(code, options = []) {
84
345
  const messages = [];
85
346
  const context = {
86
347
  report: (descriptor) => {
@@ -95,7 +356,7 @@ function runRuleOnCode(code) {
95
356
  },
96
357
  ],
97
358
  }),
98
- options: [],
359
+ options,
99
360
  parserOptions: { ecmaVersion: 2020 },
100
361
  };
101
362
  const listeners = valid_story_reference_1.default.create(context);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
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/src/index.js",
6
6
  "types": "lib/src/index.d.ts",