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
@@ -1,184 +1,206 @@
1
1
  "use strict";
2
- /**
3
- * Rule to enforce @story annotation on functions, function expressions, arrow functions, and methods
4
- * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
5
- * @req REQ-ANNOTATION-REQUIRED - Require @story annotation on functions
6
- * @req REQ-OPTIONS-SCOPE - Support configuring which function types to enforce via options
7
- * @req REQ-EXPORT-PRIORITY - Add exportPriority option to target exported or non-exported
8
- * @req REQ-UNIFIED-CHECK - Implement unified checkNode for all supported node types
9
- * @req REQ-FUNCTION-DETECTION - Detect function declarations, expressions, arrow functions, and methods
10
- * @req REQ-TYPESCRIPT-SUPPORT - Support TypeScript-specific function syntax
11
- */
12
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ // Default node types to check for function annotations
4
+ const DEFAULT_SCOPE = [
5
+ "FunctionDeclaration",
6
+ "FunctionExpression",
7
+ "ArrowFunctionExpression",
8
+ "MethodDefinition",
9
+ "TSDeclareFunction",
10
+ "TSMethodSignature",
11
+ ];
12
+ const EXPORT_PRIORITY_VALUES = ["all", "exported", "non-exported"];
13
13
  /**
14
- * Determine if a node is exported via export declaration.
14
+ * Determine if a node is in an export declaration
15
+ *
15
16
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
16
- * @req REQ-EXPORT-PRIORITY - Determine if function node has export declaration ancestor
17
+ * @req REQ-ANNOTATION-REQUIRED
18
+ * @param {any} node - AST node to check for export ancestry
19
+ * @returns {boolean} true if node is within an export declaration
17
20
  */
18
21
  function isExportedNode(node) {
19
- let current = node;
20
- while (current) {
21
- if (current.type === "ExportNamedDeclaration" ||
22
- current.type === "ExportDefaultDeclaration") {
22
+ let p = node.parent;
23
+ while (p) {
24
+ if (p.type === "ExportNamedDeclaration" ||
25
+ p.type === "ExportDefaultDeclaration") {
23
26
  return true;
24
27
  }
25
- current = current.parent;
28
+ p = p.parent;
26
29
  }
27
30
  return false;
28
31
  }
32
+ // Path to the story file for annotations
33
+ const STORY_PATH = "docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md";
34
+ const ANNOTATION = `/** @story ${STORY_PATH} */`;
29
35
  /**
30
- * Find nearest ancestor node of specified types.
36
+ * Check if @story annotation already present in JSDoc or preceding comments
37
+ *
31
38
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
32
- * @req REQ-OPTIONS-SCOPE - Support configuring which function types to enforce via options
39
+ * @req REQ-ANNOTATION-REQUIRED
40
+ * @param {any} sourceCode - ESLint sourceCode object
41
+ * @param {any} node - AST node to inspect for existing annotations
42
+ * @returns {boolean} true if @story annotation already present
33
43
  */
34
- function findAncestorNode(node, types) {
35
- let current = node.parent;
36
- while (current) {
37
- if (types.includes(current.type)) {
38
- return current;
39
- }
40
- current = current.parent;
44
+ function hasStoryAnnotation(sourceCode, node) {
45
+ const jsdoc = sourceCode.getJSDocComment(node);
46
+ if (jsdoc?.value.includes("@story")) {
47
+ return true;
41
48
  }
42
- return null;
49
+ const comments = sourceCode.getCommentsBefore(node) || [];
50
+ return comments.some((c) => c.value.includes("@story"));
43
51
  }
44
52
  /**
45
- * Determine if node should be checked based on scope and exportPriority.
53
+ * Get the name of the function-like node
54
+ *
46
55
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
47
- * @req REQ-OPTIONS-SCOPE
48
- * @req REQ-EXPORT-PRIORITY
49
- * @req REQ-UNIFIED-CHECK
56
+ * @req REQ-ANNOTATION-REQUIRED
57
+ * @param {any} node - AST node representing a function-like construct
58
+ * @returns {string} the resolved name or "<unknown>"
50
59
  */
51
- function shouldCheckNode(node, scope, exportPriority) {
52
- if (node.type === "FunctionExpression" &&
53
- node.parent?.type === "MethodDefinition") {
54
- return false;
55
- }
56
- if (!scope.includes(node.type)) {
57
- return false;
58
- }
59
- const exported = isExportedNode(node);
60
- if ((exportPriority === "exported" && !exported) ||
61
- (exportPriority === "non-exported" && exported)) {
62
- return false;
60
+ function getNodeName(node) {
61
+ let current = node;
62
+ while (current) {
63
+ if (current.type === "VariableDeclarator" &&
64
+ current.id &&
65
+ typeof current.id.name === "string") {
66
+ return current.id.name;
67
+ }
68
+ if ((current.type === "FunctionDeclaration" ||
69
+ current.type === "TSDeclareFunction") &&
70
+ current.id &&
71
+ typeof current.id.name === "string") {
72
+ return current.id.name;
73
+ }
74
+ if ((current.type === "MethodDefinition" ||
75
+ current.type === "TSMethodSignature") &&
76
+ current.key &&
77
+ typeof current.key.name === "string") {
78
+ return current.key.name;
79
+ }
80
+ current = current.parent;
63
81
  }
64
- return true;
82
+ return "<unknown>";
65
83
  }
66
84
  /**
67
- * Resolve the AST node to annotate or check.
85
+ * Determine AST node where annotation should be inserted
86
+ *
68
87
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
69
- * @req REQ-UNIFIED-CHECK
88
+ * @req REQ-ANNOTATION-REQUIRED
89
+ * @param {any} sourceCode - ESLint sourceCode object (unused but kept for parity)
90
+ * @param {any} node - function-like AST node to resolve target for
91
+ * @returns {any} AST node that should receive the annotation
70
92
  */
71
93
  function resolveTargetNode(sourceCode, node) {
72
- let target = node;
73
- if (node.type === "FunctionDeclaration") {
74
- const exp = findAncestorNode(node, [
75
- "ExportNamedDeclaration",
76
- "ExportDefaultDeclaration",
77
- ]);
78
- if (exp) {
79
- target = exp;
80
- }
94
+ if (node.type === "TSMethodSignature") {
95
+ // Interface method signature -> insert on interface
96
+ return node.parent.parent;
81
97
  }
82
- else if (node.type === "FunctionExpression" ||
98
+ if (node.type === "FunctionExpression" ||
83
99
  node.type === "ArrowFunctionExpression") {
84
- const exp = findAncestorNode(node, [
85
- "ExportNamedDeclaration",
86
- "ExportDefaultDeclaration",
87
- ]);
88
- if (exp) {
89
- target = exp;
90
- }
91
- else {
92
- const anc = findAncestorNode(node, [
93
- "VariableDeclaration",
94
- "ExpressionStatement",
95
- ]);
96
- if (anc) {
97
- target = anc;
100
+ const parent = node.parent;
101
+ if (parent.type === "VariableDeclarator") {
102
+ const varDecl = parent.parent;
103
+ if (varDecl.parent && varDecl.parent.type === "ExportNamedDeclaration") {
104
+ return varDecl.parent;
98
105
  }
106
+ return varDecl;
99
107
  }
100
- }
101
- else if (node.type === "TSMethodSignature") {
102
- const exp = findAncestorNode(node, ["TSInterfaceDeclaration"]);
103
- if (exp) {
104
- target = exp;
108
+ if (parent.type === "ExportNamedDeclaration") {
109
+ return parent;
110
+ }
111
+ if (parent.type === "ExpressionStatement") {
112
+ return parent;
105
113
  }
106
114
  }
107
- else if (node.type === "MethodDefinition") {
108
- target = node;
109
- }
110
- return target;
115
+ return node;
111
116
  }
112
117
  /**
113
- * Check if the target node has @story annotation.
118
+ * Report missing @story annotation on function or method
119
+ *
114
120
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
115
121
  * @req REQ-ANNOTATION-REQUIRED
122
+ * @param {Rule.RuleContext} context - ESLint rule context
123
+ * @param {any} sourceCode - ESLint sourceCode object
124
+ * @param {any} node - function AST node missing annotation
125
+ * @param {any} target - AST node where annotation should be inserted
116
126
  */
117
- function hasStoryAnnotation(sourceCode, target) {
118
- const jsdoc = sourceCode.getJSDocComment(target);
119
- if (jsdoc?.value.includes("@story")) {
120
- return true;
127
+ function reportMissing(context, sourceCode, node, target) {
128
+ if (hasStoryAnnotation(sourceCode, node) ||
129
+ hasStoryAnnotation(sourceCode, target)) {
130
+ return;
121
131
  }
122
- const comments = sourceCode.getCommentsBefore(target) || [];
123
- return comments.some((c) => c.value.includes("@story"));
132
+ let name = getNodeName(node);
133
+ if (node.type === "TSDeclareFunction" && node.id && node.id.name) {
134
+ name = node.id.name;
135
+ }
136
+ context.report({
137
+ node,
138
+ messageId: "missingStory",
139
+ data: { name },
140
+ suggest: [
141
+ {
142
+ desc: `Add JSDoc @story annotation for function '${name}', e.g., ${ANNOTATION}`,
143
+ fix: (fixer) => fixer.insertTextBefore(target, `${ANNOTATION}\n`),
144
+ },
145
+ ],
146
+ });
124
147
  }
125
148
  /**
126
- * Check for @story annotation on function-like nodes.
149
+ * Report missing @story annotation on class methods
150
+ *
127
151
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
128
- * @req REQ-UNIFIED-CHECK
129
152
  * @req REQ-ANNOTATION-REQUIRED
153
+ * @param {Rule.RuleContext} context - ESLint rule context
154
+ * @param {any} sourceCode - ESLint sourceCode object
155
+ * @param {any} node - MethodDefinition AST node
130
156
  */
131
- function checkStoryAnnotation(sourceCode, context, node, scope, exportPriority) {
132
- if (!shouldCheckNode(node, scope, exportPriority)) {
133
- return;
134
- }
135
- // Special handling for TSMethodSignature: allow annotation on the method itself
136
- if (node.type === "TSMethodSignature") {
137
- // If annotated on the method signature, skip
138
- if (hasStoryAnnotation(sourceCode, node)) {
139
- return;
140
- }
141
- // Otherwise, check on interface declaration
142
- const intf = resolveTargetNode(sourceCode, node);
143
- if (hasStoryAnnotation(sourceCode, intf)) {
144
- return;
145
- }
146
- // Missing annotation: report on the interface declaration
147
- context.report({
148
- node: intf,
149
- messageId: "missingStory",
150
- fix(fixer) {
151
- const indentLevel = intf.loc.start.column;
152
- const indent = " ".repeat(indentLevel);
153
- const insertPos = intf.range[0] - indentLevel;
154
- return fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}/** @story <story-file>.story.md */\n`);
155
- },
156
- });
157
- return;
158
- }
159
- const target = resolveTargetNode(sourceCode, node);
160
- if (hasStoryAnnotation(sourceCode, target)) {
157
+ function reportMethod(context, sourceCode, node) {
158
+ if (hasStoryAnnotation(sourceCode, node)) {
161
159
  return;
162
160
  }
163
161
  context.report({
164
162
  node,
165
163
  messageId: "missingStory",
166
- fix(fixer) {
167
- const indentLevel = target.loc.start.column;
168
- const indent = " ".repeat(indentLevel);
169
- const insertPos = target.range[0] - indentLevel;
170
- return fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}/** @story <story-file>.story.md */\n`);
171
- },
164
+ data: { name: getNodeName(node) },
165
+ suggest: [
166
+ {
167
+ desc: `Add JSDoc @story annotation for function '${getNodeName(node)}', e.g., ${ANNOTATION}`,
168
+ fix: (fixer) => fixer.insertTextBefore(node, `${ANNOTATION}\n `),
169
+ },
170
+ ],
172
171
  });
173
172
  }
174
- exports.default = {
173
+ /**
174
+ * Check if this node is within scope and matches exportPriority
175
+ *
176
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
177
+ * @req REQ-ANNOTATION-REQUIRED
178
+ * @param {any} node - AST node to evaluate
179
+ * @param {string[]} scope - allowed node types
180
+ * @param {string} exportPriority - 'all' | 'exported' | 'non-exported'
181
+ * @returns {boolean} whether node should be processed
182
+ */
183
+ function shouldProcessNode(node, scope, exportPriority) {
184
+ if (!scope.includes(node.type)) {
185
+ return false;
186
+ }
187
+ const exported = isExportedNode(node);
188
+ if (exportPriority === "exported" && !exported) {
189
+ return false;
190
+ }
191
+ if (exportPriority === "non-exported" && exported) {
192
+ return false;
193
+ }
194
+ return true;
195
+ }
196
+ const rule = {
175
197
  meta: {
176
198
  type: "problem",
177
199
  docs: {
178
- description: "Require @story annotations on selected functions",
200
+ description: "Require @story annotations on functions",
179
201
  recommended: "error",
180
202
  },
181
- fixable: "code",
203
+ hasSuggestions: true,
182
204
  messages: {
183
205
  missingStory: "Missing @story annotation (REQ-ANNOTATION-REQUIRED)",
184
206
  },
@@ -188,61 +210,69 @@ exports.default = {
188
210
  properties: {
189
211
  scope: {
190
212
  type: "array",
191
- items: {
192
- enum: [
193
- "FunctionDeclaration",
194
- "FunctionExpression",
195
- "ArrowFunctionExpression",
196
- "MethodDefinition",
197
- "TSDeclareFunction",
198
- "TSMethodSignature",
199
- ],
200
- },
213
+ items: { type: "string", enum: DEFAULT_SCOPE },
201
214
  uniqueItems: true,
202
215
  },
203
- exportPriority: {
204
- enum: ["all", "exported", "non-exported"],
205
- },
216
+ exportPriority: { type: "string", enum: EXPORT_PRIORITY_VALUES },
206
217
  },
207
218
  additionalProperties: false,
208
219
  },
209
220
  ],
210
221
  },
222
+ /**
223
+ * Create the rule visitor functions for require-story-annotation.
224
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
225
+ * @req REQ-CREATE-HOOK - Provide create(context) hook for rule behavior
226
+ */
211
227
  create(context) {
212
228
  const sourceCode = context.getSourceCode();
213
- const options = context.options[0] || {};
214
- const scope = options.scope || [
215
- "FunctionDeclaration",
216
- "FunctionExpression",
217
- "ArrowFunctionExpression",
218
- "MethodDefinition",
219
- "TSDeclareFunction",
220
- "TSMethodSignature",
221
- ];
222
- const exportPriority = options.exportPriority || "all";
229
+ const opts = context.options[0] ||
230
+ {};
231
+ const scope = opts.scope || DEFAULT_SCOPE;
232
+ const exportPriority = opts.exportPriority || "all";
223
233
  return {
224
234
  FunctionDeclaration(node) {
225
- checkStoryAnnotation(sourceCode, context, node, scope, exportPriority);
235
+ if (!shouldProcessNode(node, scope, exportPriority))
236
+ return;
237
+ let target = node;
238
+ if (node.parent &&
239
+ (node.parent.type === "ExportNamedDeclaration" ||
240
+ node.parent.type === "ExportDefaultDeclaration")) {
241
+ target = node.parent;
242
+ }
243
+ reportMissing(context, sourceCode, node, target);
226
244
  },
227
245
  FunctionExpression(node) {
228
- checkStoryAnnotation(sourceCode, context, node, scope, exportPriority);
246
+ if (!shouldProcessNode(node, scope, exportPriority))
247
+ return;
248
+ if (node.parent && node.parent.type === "MethodDefinition")
249
+ return;
250
+ const target = resolveTargetNode(sourceCode, node);
251
+ reportMissing(context, sourceCode, node, target);
229
252
  },
230
253
  ArrowFunctionExpression(node) {
231
- checkStoryAnnotation(sourceCode, context, node, scope, exportPriority);
254
+ if (!shouldProcessNode(node, scope, exportPriority))
255
+ return;
256
+ const target = resolveTargetNode(sourceCode, node);
257
+ reportMissing(context, sourceCode, node, target);
232
258
  },
233
- MethodDefinition(node) {
234
- checkStoryAnnotation(sourceCode, context, node, scope, exportPriority);
235
- },
236
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
237
- // @req REQ-FUNCTION-DETECTION - Detect TS-specific function syntax
238
259
  TSDeclareFunction(node) {
239
- checkStoryAnnotation(sourceCode, context, node, scope, exportPriority);
260
+ if (!shouldProcessNode(node, scope, exportPriority))
261
+ return;
262
+ reportMissing(context, sourceCode, node, node);
240
263
  },
241
- // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
242
- // @req REQ-FUNCTION-DETECTION - Detect TS-specific function syntax
243
264
  TSMethodSignature(node) {
244
- checkStoryAnnotation(sourceCode, context, node, scope, exportPriority);
265
+ if (!shouldProcessNode(node, scope, exportPriority))
266
+ return;
267
+ const target = resolveTargetNode(sourceCode, node);
268
+ reportMissing(context, sourceCode, node, target);
269
+ },
270
+ MethodDefinition(node) {
271
+ if (!shouldProcessNode(node, scope, exportPriority))
272
+ return;
273
+ reportMethod(context, sourceCode, node);
245
274
  },
246
275
  };
247
276
  },
248
277
  };
278
+ exports.default = rule;
@@ -21,9 +21,20 @@ exports.default = {
21
21
  },
22
22
  schema: [],
23
23
  },
24
+ /**
25
+ * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
26
+ * @req REQ-SYNTAX-VALIDATION - Ensure rule create function validates annotations syntax
27
+ * @req REQ-FORMAT-SPECIFICATION - Implement formatting checks per specification
28
+ */
24
29
  create(context) {
25
30
  const sourceCode = context.getSourceCode();
26
31
  return {
32
+ /**
33
+ * Program-level handler that inspects all comments for @story and @req tags
34
+ * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
35
+ * @req REQ-PATH-FORMAT - Validate @story paths follow expected patterns
36
+ * @req REQ-REQ-FORMAT - Validate @req identifiers follow expected patterns
37
+ */
27
38
  Program() {
28
39
  const comments = sourceCode.getAllComments() || [];
29
40
  comments.forEach((comment) => {
@@ -19,6 +19,9 @@ const path_1 = __importDefault(require("path"));
19
19
  * Parses comment.value lines for @story annotation.
20
20
  * @param comment any JSDoc comment node
21
21
  * @returns story path or null if not found
22
+ *
23
+ * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
24
+ * @req REQ-DEEP-PARSE - Extracts @story annotation from comment content
22
25
  */
23
26
  function extractStoryPath(comment) {
24
27
  const rawLines = comment.value.split(/\r?\n/);
@@ -34,14 +37,17 @@ function extractStoryPath(comment) {
34
37
  /**
35
38
  * Validate a @req annotation line against the extracted story content.
36
39
  * Performs path validation, file reading, caching, and requirement existence checks.
37
- * @param comment any JSDoc comment node
38
- * @param context ESLint rule context
39
- * @param line the @req annotation line
40
- * @param storyPath current story path
41
- * @param cwd current working directory
42
- * @param reqCache cache mapping story paths to sets of requirement IDs
40
+ *
41
+ * @param opts options bag
42
+ *
43
+ * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
44
+ * @req REQ-DEEP-PATH - Validates and protects against path traversal and absolute paths
45
+ * @req REQ-DEEP-CACHE - Caches parsed story files to avoid repeated IO
46
+ * @req REQ-DEEP-MATCH - Ensures referenced requirement IDs exist in the story file
47
+ * @req REQ-DEEP-PARSE - Parses story file content to find REQ- identifiers
43
48
  */
44
- function validateReqLine(comment, context, line, storyPath, cwd, reqCache) {
49
+ function validateReqLine(opts) {
50
+ const { comment, context, line, storyPath, cwd, reqCache } = opts;
45
51
  const parts = line.split(/\s+/);
46
52
  const reqId = parts[1];
47
53
  if (!reqId || !storyPath) {
@@ -93,43 +99,62 @@ function validateReqLine(comment, context, line, storyPath, cwd, reqCache) {
93
99
  * Handle a single annotation line.
94
100
  * @story Updates the current story path when encountering an @story annotation
95
101
  * @req Validates the requirement reference against the current story content
96
- * @param line the trimmed annotation line
97
- * @param comment JSDoc comment node
98
- * @param context ESLint rule context
99
- * @param cwd current working directory
100
- * @param reqCache cache mapping story paths to sets of requirement IDs
101
- * @param storyPath current story path or null
102
- * @returns updated story path or null
102
+ *
103
+ * @param opts handler options
104
+ *
105
+ * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
106
+ * @req REQ-DEEP-PARSE - Recognizes @story and @req annotation lines
107
+ * @req REQ-DEEP-MATCH - Delegates @req validation to validateReqLine
103
108
  */
104
- function handleAnnotationLine(line, comment, context, cwd, reqCache, storyPath) {
109
+ function handleAnnotationLine(opts) {
110
+ const { line, comment, context, cwd, reqCache, storyPath } = opts;
105
111
  if (line.startsWith("@story")) {
106
112
  const newPath = extractStoryPath(comment);
107
113
  return newPath || storyPath;
108
114
  }
109
115
  else if (line.startsWith("@req")) {
110
- validateReqLine(comment, context, line, storyPath, cwd, reqCache);
116
+ validateReqLine({ comment, context, line, storyPath, cwd, reqCache });
111
117
  return storyPath;
112
118
  }
113
119
  return storyPath;
114
120
  }
115
121
  /**
116
122
  * Handle JSDoc story and req annotations.
117
- * @param comment any JSDoc comment node
118
- * @param context ESLint rule context
119
- * @param cwd current working directory
120
- * @param reqCache cache mapping story paths to sets of requirement IDs
121
- * @param rawStoryPath the last extracted story path or null
122
- * @returns updated story path or null
123
+ *
124
+ * @param opts options for comment handling
125
+ *
126
+ * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
127
+ * @req REQ-DEEP-PARSE - Parses comment blocks to extract annotation lines
128
+ * @req REQ-DEEP-MATCH - Uses handleAnnotationLine to validate @req entries
129
+ * @req REQ-DEEP-CACHE - Passes shared cache for parsed story files
123
130
  */
124
- function handleComment(comment, context, cwd, reqCache, rawStoryPath) {
131
+ function handleComment(opts) {
132
+ const { comment, context, cwd, reqCache, rawStoryPath } = opts;
125
133
  let storyPath = rawStoryPath;
126
134
  const rawLines = comment.value.split(/\r?\n/);
127
135
  for (const rawLine of rawLines) {
128
136
  const line = rawLine.trim().replace(/^\*+\s*/, "");
129
- storyPath = handleAnnotationLine(line, comment, context, cwd, reqCache, storyPath);
137
+ storyPath = handleAnnotationLine({
138
+ line,
139
+ comment,
140
+ context,
141
+ cwd,
142
+ reqCache,
143
+ storyPath,
144
+ });
130
145
  }
131
146
  return storyPath;
132
147
  }
148
+ /**
149
+ * Create a Program listener that iterates comments and validates annotations.
150
+ *
151
+ * @param context ESLint rule context
152
+ * @returns Program visitor function
153
+ *
154
+ * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
155
+ * @req REQ-DEEP-CACHE - Maintains a cache across comment processing
156
+ * @req REQ-DEEP-PATH - Resolves and protects story paths against traversal
157
+ */
133
158
  function programListener(context) {
134
159
  const sourceCode = context.getSourceCode();
135
160
  const cwd = process.cwd();
@@ -138,7 +163,13 @@ function programListener(context) {
138
163
  return function Program() {
139
164
  const comments = sourceCode.getAllComments() || [];
140
165
  comments.forEach((comment) => {
141
- rawStoryPath = handleComment(comment, context, cwd, reqCache, rawStoryPath);
166
+ rawStoryPath = handleComment({
167
+ comment,
168
+ context,
169
+ cwd,
170
+ reqCache,
171
+ rawStoryPath,
172
+ });
142
173
  });
143
174
  };
144
175
  }
@@ -155,6 +186,15 @@ exports.default = {
155
186
  },
156
187
  schema: [],
157
188
  },
189
+ /**
190
+ * Rule create entrypoint that returns the Program visitor.
191
+ *
192
+ * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
193
+ * @req REQ-DEEP-MATCH - Entrypoint orchestrates validation of @req annotations
194
+ * @req REQ-DEEP-PARSE - Uses parsing helpers to extract annotations and story paths
195
+ * @req REQ-DEEP-CACHE - Establishes cache used during validation
196
+ * @req REQ-DEEP-PATH - Ensures path validation is applied during checks
197
+ */
158
198
  create(context) {
159
199
  return { Program: programListener(context) };
160
200
  },