eslint-plugin-traceability 1.6.3 → 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.
@@ -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 ?? {};
@@ -61,11 +61,20 @@ function __resetStoryExistenceCacheForTests() {
61
61
  */
62
62
  function buildStoryCandidates(storyPath, cwd, storyDirs) {
63
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
64
67
  if (storyPath.startsWith("./") || storyPath.startsWith("../")) {
65
68
  candidates.push(path_1.default.resolve(cwd, storyPath));
66
69
  }
67
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
68
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
69
78
  for (const dir of storyDirs) {
70
79
  candidates.push(path_1.default.resolve(cwd, dir, path_1.default.basename(storyPath)));
71
80
  }
@@ -91,17 +100,26 @@ const fileExistStatusCache = new Map();
91
100
  */
92
101
  function checkSingleCandidate(candidate) {
93
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
94
106
  if (cached) {
95
107
  return cached;
96
108
  }
97
109
  let result;
98
110
  try {
99
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
100
115
  if (!exists) {
101
116
  result = { path: candidate, status: "missing" };
102
117
  }
103
118
  else {
104
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
105
123
  if (stat.isFile()) {
106
124
  result = { path: candidate, status: "exists" };
107
125
  }
@@ -112,7 +130,9 @@ function checkSingleCandidate(candidate) {
112
130
  }
113
131
  }
114
132
  catch (error) {
115
- // 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
116
136
  result = { path: candidate, status: "fs-error", error };
117
137
  }
118
138
  fileExistStatusCache.set(candidate, result);
@@ -136,6 +156,9 @@ function getStoryExistence(candidates) {
136
156
  let firstFsError;
137
157
  for (const candidate of candidates) {
138
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
139
162
  if (res.status === "exists") {
140
163
  return {
141
164
  candidates,
@@ -143,10 +166,16 @@ function getStoryExistence(candidates) {
143
166
  matchedPath: res.path,
144
167
  };
145
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
146
172
  if (res.status === "fs-error" && !firstFsError) {
147
173
  firstFsError = res;
148
174
  }
149
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
150
179
  if (firstFsError) {
151
180
  return {
152
181
  candidates,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.6.3",
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",