eslint-plugin-traceability 1.6.5 → 1.7.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 (48) hide show
  1. package/README.md +39 -1
  2. package/lib/src/index.d.ts +30 -27
  3. package/lib/src/index.js +51 -31
  4. package/lib/src/maintenance/cli.d.ts +12 -0
  5. package/lib/src/maintenance/cli.js +279 -0
  6. package/lib/src/maintenance/detect.js +27 -12
  7. package/lib/src/maintenance/update.js +42 -34
  8. package/lib/src/maintenance/utils.js +30 -30
  9. package/lib/src/rules/helpers/require-story-io.js +51 -15
  10. package/lib/src/rules/helpers/valid-annotation-format-internal.d.ts +30 -0
  11. package/lib/src/rules/helpers/valid-annotation-format-internal.js +36 -0
  12. package/lib/src/rules/helpers/valid-annotation-options.d.ts +118 -0
  13. package/lib/src/rules/helpers/valid-annotation-options.js +167 -0
  14. package/lib/src/rules/helpers/valid-annotation-utils.d.ts +68 -0
  15. package/lib/src/rules/helpers/valid-annotation-utils.js +103 -0
  16. package/lib/src/rules/helpers/valid-implements-utils.d.ts +75 -0
  17. package/lib/src/rules/helpers/valid-implements-utils.js +149 -0
  18. package/lib/src/rules/helpers/valid-story-reference-helpers.d.ts +67 -0
  19. package/lib/src/rules/helpers/valid-story-reference-helpers.js +92 -0
  20. package/lib/src/rules/prefer-implements-annotation.d.ts +39 -0
  21. package/lib/src/rules/prefer-implements-annotation.js +276 -0
  22. package/lib/src/rules/valid-annotation-format.js +255 -208
  23. package/lib/src/rules/valid-req-reference.js +210 -29
  24. package/lib/src/rules/valid-story-reference.d.ts +7 -0
  25. package/lib/src/rules/valid-story-reference.js +38 -80
  26. package/lib/src/utils/annotation-checker.js +2 -145
  27. package/lib/src/utils/branch-annotation-helpers.js +12 -3
  28. package/lib/src/utils/reqAnnotationDetection.d.ts +6 -0
  29. package/lib/src/utils/reqAnnotationDetection.js +152 -0
  30. package/lib/tests/maintenance/cli.test.d.ts +1 -0
  31. package/lib/tests/maintenance/cli.test.js +172 -0
  32. package/lib/tests/plugin-default-export-and-configs.test.js +3 -0
  33. package/lib/tests/rules/prefer-implements-annotation.test.d.ts +1 -0
  34. package/lib/tests/rules/prefer-implements-annotation.test.js +84 -0
  35. package/lib/tests/rules/require-branch-annotation.test.js +3 -2
  36. package/lib/tests/rules/require-req-annotation.test.js +57 -68
  37. package/lib/tests/rules/require-story-annotation.test.js +13 -28
  38. package/lib/tests/rules/require-story-core-edgecases.test.js +3 -58
  39. package/lib/tests/rules/require-story-core.autofix.test.js +5 -41
  40. package/lib/tests/rules/valid-annotation-format.test.js +395 -40
  41. package/lib/tests/rules/valid-req-reference.test.js +34 -0
  42. package/lib/tests/utils/annotation-checker.test.d.ts +23 -0
  43. package/lib/tests/utils/annotation-checker.test.js +24 -17
  44. package/lib/tests/utils/require-story-core-test-helpers.d.ts +10 -0
  45. package/lib/tests/utils/require-story-core-test-helpers.js +75 -0
  46. package/lib/tests/utils/ts-language-options.d.ts +22 -0
  47. package/lib/tests/utils/ts-language-options.js +27 -0
  48. package/package.json +12 -3
@@ -36,6 +36,45 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.updateAnnotationReferences = updateAnnotationReferences;
37
37
  const fs = __importStar(require("fs"));
38
38
  const utils_1 = require("./utils");
39
+ /**
40
+ * Helper to process a single file for annotation reference updates
41
+ * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
42
+ * @req REQ-MAINT-UPDATE
43
+ */
44
+ function processFileForAnnotationUpdates(fullPath, regex, newPath, replacementCountRef) {
45
+ const stat = fs.statSync(fullPath);
46
+ /**
47
+ * Skip non-files in iteration
48
+ * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
49
+ * @req REQ-MAINT-UPDATE
50
+ */
51
+ /**
52
+ * Skip entries that are not regular files (e.g., directories)
53
+ * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
54
+ * @req REQ-MAINT-UPDATE
55
+ */
56
+ if (!stat.isFile())
57
+ return;
58
+ const content = fs.readFileSync(fullPath, "utf8");
59
+ const newContent = content.replace(regex,
60
+ /**
61
+ * Replacement callback to update annotation references
62
+ * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
63
+ * @req REQ-MAINT-UPDATE
64
+ */
65
+ (match, p1) => {
66
+ replacementCountRef.count++;
67
+ return `${p1}${newPath}`;
68
+ });
69
+ /**
70
+ * Write file only if content changed
71
+ * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
72
+ * @req REQ-MAINT-UPDATE
73
+ */
74
+ if (newContent !== content) {
75
+ fs.writeFileSync(fullPath, newContent, "utf8");
76
+ }
77
+ }
39
78
  /**
40
79
  * Update annotation references when story files are moved or renamed
41
80
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
@@ -54,7 +93,7 @@ function updateAnnotationReferences(codebasePath, oldPath, newPath) {
54
93
  !fs.statSync(codebasePath).isDirectory()) {
55
94
  return 0;
56
95
  }
57
- let replacementCount = 0;
96
+ const replacementCountRef = { count: 0 };
58
97
  const escapedOldPath = oldPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
59
98
  const regex = new RegExp(`(@story\\s*)${escapedOldPath}`, "g");
60
99
  const files = (0, utils_1.getAllFiles)(codebasePath);
@@ -69,38 +108,7 @@ function updateAnnotationReferences(codebasePath, oldPath, newPath) {
69
108
  * @req REQ-MAINT-UPDATE
70
109
  */
71
110
  for (const fullPath of files) {
72
- const stat = fs.statSync(fullPath);
73
- /**
74
- * Skip non-files in iteration
75
- * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
76
- * @req REQ-MAINT-UPDATE
77
- */
78
- /**
79
- * Skip entries that are not regular files (e.g., directories)
80
- * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
81
- * @req REQ-MAINT-UPDATE
82
- */
83
- if (!stat.isFile())
84
- continue;
85
- const content = fs.readFileSync(fullPath, "utf8");
86
- const newContent = content.replace(regex,
87
- /**
88
- * Replacement callback to update annotation references
89
- * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
90
- * @req REQ-MAINT-UPDATE
91
- */
92
- (match, p1) => {
93
- replacementCount++;
94
- return `${p1}${newPath}`;
95
- });
96
- /**
97
- * Write file only if content changed
98
- * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
99
- * @req REQ-MAINT-UPDATE
100
- */
101
- if (newContent !== content) {
102
- fs.writeFileSync(fullPath, newContent, "utf8");
103
- }
111
+ processFileForAnnotationUpdates(fullPath, regex, newPath, replacementCountRef);
104
112
  }
105
- return replacementCount;
113
+ return replacementCountRef.count;
106
114
  }
@@ -56,45 +56,45 @@ function getAllFiles(dir) {
56
56
  if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
57
57
  return fileList;
58
58
  }
59
+ traverseDirectory(dir, fileList);
60
+ return fileList;
61
+ }
62
+ /**
63
+ * Recursively traverse a directory and collect file paths.
64
+ * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
65
+ * @req REQ-MAINT-UTILS-TRAVERSE - Helper traversal function used by getAllFiles
66
+ */
67
+ function traverseDirectory(currentDir, fileList) {
68
+ const entries = fs.readdirSync(currentDir);
59
69
  /**
60
- * Recursively traverse a directory and collect file paths.
70
+ * Iterate over directory entries using a for-of loop.
61
71
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
62
- * @req REQ-MAINT-UTILS-TRAVERSE - Helper traversal function used by getAllFiles
72
+ * @req REQ-MAINT-UTILS-TRAVERSE-FOROF - Traceability for ForOfStatement branch handling entries
63
73
  */
64
- function traverse(currentDir) {
65
- const entries = fs.readdirSync(currentDir);
74
+ for (const entry of entries) {
75
+ const fullPath = path.join(currentDir, entry);
76
+ const stat = fs.statSync(fullPath);
66
77
  /**
67
- * Iterate over directory entries using a for-of loop.
78
+ * Recurse into directories to continue traversal.
68
79
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
69
- * @req REQ-MAINT-UTILS-TRAVERSE-FOROF - Traceability for ForOfStatement branch handling entries
80
+ * @req REQ-MAINT-UTILS-TRAVERSE-DIR - Handle directory entries during traversal
70
81
  */
71
- for (const entry of entries) {
72
- const fullPath = path.join(currentDir, entry);
73
- const stat = fs.statSync(fullPath);
82
+ if (stat.isDirectory()) {
83
+ traverseDirectory(fullPath, fileList);
74
84
  /**
75
- * Recurse into directories to continue traversal.
85
+ * Collect regular file entries during traversal.
76
86
  * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
77
- * @req REQ-MAINT-UTILS-TRAVERSE-DIR - Handle directory entries during traversal
87
+ * @req REQ-MAINT-UTILS-TRAVERSE-FILE - Handle file entries during traversal
78
88
  */
79
- if (stat.isDirectory()) {
80
- traverse(fullPath);
81
- /**
82
- * Collect regular file entries during traversal.
83
- * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
84
- * @req REQ-MAINT-UTILS-TRAVERSE-FILE - Handle file entries during traversal
85
- */
86
- }
87
- /**
88
- * Skip non-file entries encountered during traversal.
89
- * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
90
- * @req REQ-MAINT-UTILS-TRAVERSE-SKIP-NONFILE - Traceability for skipping non-file entries
91
- */
92
- if (!stat.isFile()) {
93
- continue;
94
- }
95
- fileList.push(fullPath);
96
89
  }
90
+ /**
91
+ * Skip non-file entries encountered during traversal.
92
+ * @story docs/stories/009.0-DEV-MAINTENANCE-TOOLS.story.md
93
+ * @req REQ-MAINT-UTILS-TRAVERSE-SKIP-NONFILE - Traceability for skipping non-file entries
94
+ */
95
+ if (!stat.isFile()) {
96
+ continue;
97
+ }
98
+ fileList.push(fullPath);
97
99
  }
98
- traverse(dir);
99
- return fileList;
100
100
  }
@@ -22,23 +22,41 @@ exports.LOOKBACK_LINES = 4;
22
22
  */
23
23
  exports.FALLBACK_WINDOW = 800;
24
24
  /**
25
- * Inspect a fixed number of physical source lines before the node for @story text
25
+ * Shared predicate to determine if a given comment node contains an @story marker.
26
26
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
27
- * @req REQ-ANNOTATION-REQUIRED - Extract line-based detection into helper
27
+ * @req REQ-ANNOTATION-REQUIRED - Centralize @story detection logic for comment value inspection
28
28
  */
29
- function linesBeforeHasStory(sourceCode, node, lookback = exports.LOOKBACK_LINES) {
29
+ function commentContainsStory(comment) {
30
+ return typeof comment?.value === "string" && comment.value.includes("@story");
31
+ }
32
+ /**
33
+ * Safely extract the physical source lines array from sourceCode for scanning.
34
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
35
+ * @req REQ-ANNOTATION-REQUIRED - Centralize guards for safe access to source lines
36
+ */
37
+ function getSourceLines(sourceCode) {
30
38
  const lines = sourceCode && sourceCode.lines;
31
- const startLine = node && node.loc && typeof node.loc.start?.line === "number"
32
- ? node.loc.start.line
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
37
- if (!Array.isArray(lines) || typeof startLine !== "number") {
38
- return false;
39
+ return Array.isArray(lines) ? lines : null;
40
+ }
41
+ /**
42
+ * Safely resolve the starting line number of a node for use in lookback scans.
43
+ * Returns null when the node does not provide a valid numeric start line.
44
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
45
+ * @req REQ-ANNOTATION-REQUIRED - Centralize guards for safe access to node location metadata
46
+ */
47
+ function getNodeStartLine(node) {
48
+ if (!node || !node.loc) {
49
+ return null;
39
50
  }
40
- const from = Math.max(0, startLine - 1 - lookback);
41
- const to = Math.max(0, startLine - 1);
51
+ const line = node.loc.start?.line;
52
+ return typeof line === "number" ? line : null;
53
+ }
54
+ /**
55
+ * Generic helper to scan a range of physical source lines for the presence of an @story marker.
56
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
57
+ * @req REQ-ANNOTATION-REQUIRED - Reuse line scanning logic for story annotations across helpers
58
+ */
59
+ function scanLinesForMarker(lines, from, to) {
42
60
  // Walk each physical line in the configured lookback window to search for an inline @story marker.
43
61
  // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
44
62
  // @req REQ-ANNOTATION-REQUIRED - Scan preceding lines for existing story annotations
@@ -53,6 +71,24 @@ function linesBeforeHasStory(sourceCode, node, lookback = exports.LOOKBACK_LINES
53
71
  }
54
72
  return false;
55
73
  }
74
+ /**
75
+ * Inspect a fixed number of physical source lines before the node for @story text
76
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
77
+ * @req REQ-ANNOTATION-REQUIRED - Extract line-based detection into helper
78
+ */
79
+ function linesBeforeHasStory(sourceCode, node, lookback = exports.LOOKBACK_LINES) {
80
+ const lines = getSourceLines(sourceCode);
81
+ const startLine = getNodeStartLine(node);
82
+ // Guard against missing or non-array source lines or an invalid start line before scanning.
83
+ // @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
84
+ // @req REQ-ANNOTATION-REQUIRED - Fail gracefully when source lines or locations are unavailable
85
+ if (!lines || typeof startLine !== "number") {
86
+ return false;
87
+ }
88
+ const from = Math.max(0, startLine - 1 - lookback);
89
+ const to = Math.max(0, startLine - 1);
90
+ return scanLinesForMarker(lines, from, to);
91
+ }
56
92
  /**
57
93
  * Walk parent chain and check comments before each parent and their leadingComments
58
94
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
@@ -70,7 +106,7 @@ function parentChainHasStory(sourceCode, node) {
70
106
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
71
107
  * @req REQ-ANNOTATION-REQUIRED - Detect @story in parent comments via value inspection
72
108
  */
73
- (c) => typeof c.value === "string" && c.value.includes("@story"))) {
109
+ (c) => commentContainsStory(c))) {
74
110
  return true;
75
111
  }
76
112
  const pLeading = p.leadingComments || [];
@@ -80,7 +116,7 @@ function parentChainHasStory(sourceCode, node) {
80
116
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
81
117
  * @req REQ-ANNOTATION-REQUIRED - Detect @story in parent leadingComments via value inspection
82
118
  */
83
- (c) => typeof c.value === "string" && c.value.includes("@story"))) {
119
+ (c) => commentContainsStory(c))) {
84
120
  return true;
85
121
  }
86
122
  p = p.parent;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Internal helpers and types for the valid-annotation-format rule.
3
+ *
4
+ * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
5
+ * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
6
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
7
+ * @req REQ-MULTILINE-SUPPORT - Handle annotations split across multiple lines
8
+ * @req REQ-FLEXIBLE-PARSING - Support reasonable variations in whitespace and formatting
9
+ * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
10
+ * @req REQ-IMPLEMENTS-PARSE - Parse @implements annotations without affecting @story/@req
11
+ * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
12
+ */
13
+ /**
14
+ * Pending annotation state tracked while iterating through comment lines.
15
+ */
16
+ export interface PendingAnnotation {
17
+ type: "story" | "req";
18
+ value: string;
19
+ hasValue: boolean;
20
+ }
21
+ /**
22
+ * Normalize a raw comment line to make annotation parsing more robust.
23
+ *
24
+ * This function trims whitespace, keeps any annotation tags that appear
25
+ * later in the line, and supports common JSDoc styles such as leading "*".
26
+ *
27
+ * It detects @story, @req, and @implements tags while preserving the rest
28
+ * of the line for downstream logic.
29
+ */
30
+ export declare function normalizeCommentLine(rawLine: string): string;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ /**
3
+ * Internal helpers and types for the valid-annotation-format rule.
4
+ *
5
+ * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
6
+ * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
7
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
8
+ * @req REQ-MULTILINE-SUPPORT - Handle annotations split across multiple lines
9
+ * @req REQ-FLEXIBLE-PARSING - Support reasonable variations in whitespace and formatting
10
+ * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
11
+ * @req REQ-IMPLEMENTS-PARSE - Parse @implements annotations without affecting @story/@req
12
+ * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.normalizeCommentLine = normalizeCommentLine;
16
+ /**
17
+ * Normalize a raw comment line to make annotation parsing more robust.
18
+ *
19
+ * This function trims whitespace, keeps any annotation tags that appear
20
+ * later in the line, and supports common JSDoc styles such as leading "*".
21
+ *
22
+ * It detects @story, @req, and @implements tags while preserving the rest
23
+ * of the line for downstream logic.
24
+ */
25
+ function normalizeCommentLine(rawLine) {
26
+ const trimmed = rawLine.trim();
27
+ if (!trimmed) {
28
+ return "";
29
+ }
30
+ const annotationMatch = trimmed.match(/@story\b|@req\b|@implements\b/);
31
+ if (!annotationMatch || annotationMatch.index === undefined) {
32
+ const withoutLeadingStar = trimmed.replace(/^\*\s?/, "");
33
+ return withoutLeadingStar;
34
+ }
35
+ return trimmed.slice(annotationMatch.index);
36
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Shared option handling for the valid-annotation-format rule.
3
+ *
4
+ * @story docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md
5
+ * @req REQ-PATTERN-CONFIG - Support configuration of custom story path and requirement ID patterns
6
+ * @req REQ-REGEX-VALIDATION - Validate that configured patterns are valid regular expressions
7
+ * @req REQ-BACKWARD-COMPAT - Maintain current behavior when no custom patterns configured
8
+ * @req REQ-EXAMPLE-MESSAGES - Support optional example strings in error messages
9
+ * @req REQ-SCHEMA-VALIDATION - Use JSON Schema to validate configuration options
10
+ */
11
+ export interface AnnotationRuleOptions {
12
+ story?: {
13
+ /**
14
+ * Regex (string) the collapsed story path must match.
15
+ * Default: /^docs\/stories\/[0-9]+\.[0-9]+-DEV-[\w-]+\.story\.md$/
16
+ */
17
+ pattern?: string;
18
+ /**
19
+ * Human-readable example path used in error messages.
20
+ * Default: "docs/stories/005.0-DEV-EXAMPLE.story.md"
21
+ */
22
+ example?: string;
23
+ };
24
+ req?: {
25
+ /**
26
+ * Regex (string) the collapsed requirement ID must match.
27
+ * Default: /^REQ-[A-Z0-9-]+$/
28
+ */
29
+ pattern?: string;
30
+ /**
31
+ * Human-readable example requirement ID used in error messages.
32
+ * Default: "REQ-EXAMPLE"
33
+ */
34
+ example?: string;
35
+ };
36
+ /**
37
+ * Shorthand for story.pattern.
38
+ * Regex (string) the collapsed story path must match.
39
+ */
40
+ storyPathPattern?: string;
41
+ /**
42
+ * Shorthand for story.example.
43
+ * Human-readable example story path used in error messages.
44
+ */
45
+ storyPathExample?: string;
46
+ /**
47
+ * Shorthand for req.pattern.
48
+ * Regex (string) the collapsed requirement ID must match.
49
+ */
50
+ requirementIdPattern?: string;
51
+ /**
52
+ * Shorthand for req.example.
53
+ * Human-readable example requirement ID used in error messages.
54
+ */
55
+ requirementIdExample?: string;
56
+ }
57
+ /**
58
+ * Resolved, runtime-ready options for the rule.
59
+ */
60
+ export interface ResolvedAnnotationOptions {
61
+ storyPattern: RegExp;
62
+ storyExample: string;
63
+ reqPattern: RegExp;
64
+ reqExample: string;
65
+ }
66
+ export declare function getDefaultReqExample(): string;
67
+ export declare function getResolvedDefaults(): ResolvedAnnotationOptions;
68
+ export declare function getOptionErrors(): string[];
69
+ /**
70
+ * Resolve user options into concrete, validated configuration.
71
+ * Falls back to existing defaults when options are not provided or invalid.
72
+ */
73
+ export declare function resolveOptions(rawOptions: unknown[]): ResolvedAnnotationOptions;
74
+ /**
75
+ * Build the JSON schema for rule options.
76
+ */
77
+ export declare function getRuleSchema(): {
78
+ type: string;
79
+ properties: {
80
+ story: {
81
+ type: string;
82
+ properties: {
83
+ pattern: {
84
+ type: string;
85
+ };
86
+ example: {
87
+ type: string;
88
+ };
89
+ };
90
+ additionalProperties: boolean;
91
+ };
92
+ req: {
93
+ type: string;
94
+ properties: {
95
+ pattern: {
96
+ type: string;
97
+ };
98
+ example: {
99
+ type: string;
100
+ };
101
+ };
102
+ additionalProperties: boolean;
103
+ };
104
+ storyPathPattern: {
105
+ type: string;
106
+ };
107
+ storyPathExample: {
108
+ type: string;
109
+ };
110
+ requirementIdPattern: {
111
+ type: string;
112
+ };
113
+ requirementIdExample: {
114
+ type: string;
115
+ };
116
+ };
117
+ additionalProperties: boolean;
118
+ }[];
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDefaultReqExample = getDefaultReqExample;
4
+ exports.getResolvedDefaults = getResolvedDefaults;
5
+ exports.getOptionErrors = getOptionErrors;
6
+ exports.resolveOptions = resolveOptions;
7
+ exports.getRuleSchema = getRuleSchema;
8
+ function getDefaultStoryPattern() {
9
+ return /^docs\/stories\/[0-9]+\.[0-9]+-DEV-[\w-]+\.story\.md$/;
10
+ }
11
+ function getDefaultStoryExample() {
12
+ return "docs/stories/005.0-DEV-EXAMPLE.story.md";
13
+ }
14
+ function getDefaultReqPattern() {
15
+ return /^REQ-[A-Z0-9-]+$/;
16
+ }
17
+ function getDefaultReqExample() {
18
+ return "REQ-EXAMPLE";
19
+ }
20
+ /**
21
+ * Global cache of the last resolved options for helpers that need access
22
+ * without having options explicitly passed in.
23
+ */
24
+ let resolvedDefaults = {
25
+ storyPattern: getDefaultStoryPattern(),
26
+ storyExample: getDefaultStoryExample(),
27
+ reqPattern: getDefaultReqPattern(),
28
+ reqExample: getDefaultReqExample(),
29
+ };
30
+ /**
31
+ * Collected configuration errors encountered while resolving options.
32
+ */
33
+ let optionErrors = [];
34
+ function getResolvedDefaults() {
35
+ return resolvedDefaults;
36
+ }
37
+ function getOptionErrors() {
38
+ return optionErrors;
39
+ }
40
+ /**
41
+ * Build a stable, engine-independent configuration error message
42
+ * for invalid regex options.
43
+ */
44
+ function buildInvalidRegexError(field, pattern) {
45
+ return `Invalid regular expression for option "${field}": "${pattern}"`;
46
+ }
47
+ /**
48
+ * Normalize raw rule options into a single AnnotationRuleOptions object.
49
+ *
50
+ * @story docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md
51
+ * @req REQ-PATTERN-CONFIG
52
+ * @req REQ-BACKWARD-COMPAT
53
+ */
54
+ function normalizeUserOptions(rawOptions) {
55
+ if (!rawOptions || rawOptions.length === 0) {
56
+ return undefined;
57
+ }
58
+ const first = rawOptions[0];
59
+ if (!first || typeof first !== "object") {
60
+ return undefined;
61
+ }
62
+ return first;
63
+ }
64
+ /**
65
+ * Resolve a user-configured regex pattern, handling both nested and flat
66
+ * configuration shapes and accumulating validation errors.
67
+ *
68
+ * @story docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md
69
+ * @req REQ-PATTERN-CONFIG
70
+ * @req REQ-REGEX-VALIDATION
71
+ * @req REQ-BACKWARD-COMPAT
72
+ */
73
+ // eslint-disable-next-line max-params -- Small, centralized helper; keeping parameters explicit is clearer than introducing an options object here.
74
+ function resolvePattern(nestedPattern, nestedFieldName, flatPattern, flatFieldName, defaultPattern) {
75
+ const effective = typeof nestedPattern === "string"
76
+ ? { value: nestedPattern, field: nestedFieldName }
77
+ : typeof flatPattern === "string"
78
+ ? { value: flatPattern, field: flatFieldName }
79
+ : null;
80
+ if (!effective) {
81
+ return defaultPattern;
82
+ }
83
+ try {
84
+ return new RegExp(effective.value);
85
+ }
86
+ catch {
87
+ optionErrors.push(buildInvalidRegexError(effective.field, effective.value));
88
+ return defaultPattern;
89
+ }
90
+ }
91
+ /**
92
+ * Resolve an example string, preferring nested over flat configuration,
93
+ * and falling back to the provided default when necessary.
94
+ *
95
+ * @story docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md
96
+ * @req REQ-EXAMPLE-MESSAGES
97
+ * @req REQ-BACKWARD-COMPAT
98
+ */
99
+ function resolveExample(nestedExample, flatExample, defaultExample) {
100
+ if (typeof nestedExample === "string" && nestedExample.trim()) {
101
+ return nestedExample;
102
+ }
103
+ if (typeof flatExample === "string" && flatExample.trim()) {
104
+ return flatExample;
105
+ }
106
+ return defaultExample;
107
+ }
108
+ /**
109
+ * Resolve user options into concrete, validated configuration.
110
+ * Falls back to existing defaults when options are not provided or invalid.
111
+ */
112
+ function resolveOptions(rawOptions) {
113
+ optionErrors = [];
114
+ const user = normalizeUserOptions(rawOptions);
115
+ const nestedStoryPattern = user?.story?.pattern;
116
+ const flatStoryPattern = user?.storyPathPattern;
117
+ const nestedStoryExample = user?.story?.example;
118
+ const flatStoryExample = user?.storyPathExample;
119
+ const nestedReqPattern = user?.req?.pattern;
120
+ const flatReqPattern = user?.requirementIdPattern;
121
+ const nestedReqExample = user?.req?.example;
122
+ const flatReqExample = user?.requirementIdExample;
123
+ const storyPattern = resolvePattern(nestedStoryPattern, "story.pattern", flatStoryPattern, "storyPathPattern", getDefaultStoryPattern());
124
+ const reqPattern = resolvePattern(nestedReqPattern, "req.pattern", flatReqPattern, "requirementIdPattern", getDefaultReqPattern());
125
+ const storyExample = resolveExample(nestedStoryExample, flatStoryExample, getDefaultStoryExample());
126
+ const reqExample = resolveExample(nestedReqExample, flatReqExample, getDefaultReqExample());
127
+ resolvedDefaults = {
128
+ storyPattern,
129
+ storyExample,
130
+ reqPattern,
131
+ reqExample,
132
+ };
133
+ return resolvedDefaults;
134
+ }
135
+ /**
136
+ * Build the JSON schema for rule options.
137
+ */
138
+ function getRuleSchema() {
139
+ return [
140
+ {
141
+ type: "object",
142
+ properties: {
143
+ story: {
144
+ type: "object",
145
+ properties: {
146
+ pattern: { type: "string" },
147
+ example: { type: "string" },
148
+ },
149
+ additionalProperties: false,
150
+ },
151
+ req: {
152
+ type: "object",
153
+ properties: {
154
+ pattern: { type: "string" },
155
+ example: { type: "string" },
156
+ },
157
+ additionalProperties: false,
158
+ },
159
+ storyPathPattern: { type: "string" },
160
+ storyPathExample: { type: "string" },
161
+ requirementIdPattern: { type: "string" },
162
+ requirementIdExample: { type: "string" },
163
+ },
164
+ additionalProperties: false,
165
+ },
166
+ ];
167
+ }