eslint-plugin-traceability 1.15.0 → 1.16.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 (34) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/README.md +61 -13
  3. package/lib/src/index.js +59 -0
  4. package/lib/src/rules/helpers/require-story-core.js +1 -1
  5. package/lib/src/rules/helpers/require-story-helpers.d.ts +10 -3
  6. package/lib/src/rules/helpers/require-story-helpers.js +84 -7
  7. package/lib/src/rules/no-redundant-annotation.js +73 -55
  8. package/lib/src/rules/require-branch-annotation.js +2 -2
  9. package/lib/src/rules/require-req-annotation.js +2 -2
  10. package/lib/src/rules/require-story-annotation.js +8 -3
  11. package/lib/src/rules/require-traceability.js +8 -9
  12. package/lib/src/utils/annotation-checker.js +23 -4
  13. package/lib/tests/cli-error-handling.test.js +10 -1
  14. package/lib/tests/integration/cli-integration.test.js +5 -0
  15. package/lib/tests/integration/require-traceability-aliases.integration.test.js +126 -0
  16. package/lib/tests/plugin-default-export-and-configs.test.js +23 -0
  17. package/lib/tests/rules/auto-fix-behavior-008.test.js +7 -7
  18. package/lib/tests/rules/error-reporting.test.js +1 -1
  19. package/lib/tests/rules/no-redundant-annotation.test.js +20 -0
  20. package/lib/tests/rules/require-story-annotation.test.js +75 -10
  21. package/lib/tests/rules/require-story-helpers.test.js +224 -0
  22. package/lib/tests/rules/require-story-utils.test.d.ts +7 -0
  23. package/lib/tests/rules/require-story-utils.test.js +158 -0
  24. package/lib/tests/utils/annotation-checker-branches.test.d.ts +5 -0
  25. package/lib/tests/utils/annotation-checker-branches.test.js +103 -0
  26. package/lib/tests/utils/annotation-scope-analyzer.test.js +134 -0
  27. package/lib/tests/utils/branch-annotation-helpers.test.js +66 -0
  28. package/package.json +2 -2
  29. package/user-docs/api-reference.md +71 -15
  30. package/user-docs/examples.md +24 -13
  31. package/user-docs/migration-guide.md +127 -4
  32. package/user-docs/traceability-overview.md +116 -0
  33. package/lib/tests/integration/dogfooding-validation.test.js +0 -129
  34. /package/lib/tests/integration/{dogfooding-validation.test.d.ts → require-traceability-aliases.integration.test.d.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,9 +1,9 @@
1
- # [1.15.0](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.14.1...v1.15.0) (2025-12-08)
1
+ ## [1.16.1](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.16.0...v1.16.1) (2025-12-09)
2
2
 
3
3
 
4
- ### Features
4
+ ### Bug Fixes
5
5
 
6
- * add unified require-traceability rule and exports ([c92ce8f](https://github.com/voder-ai/eslint-plugin-traceability/commit/c92ce8f467cc4968dc2dea2abb0927eca08c9ace))
6
+ * broaden test callback exclusion coverage for function annotations ([8078745](https://github.com/voder-ai/eslint-plugin-traceability/commit/80787455dcba39724158f378fce54ae78a75b59b))
7
7
 
8
8
  # Changelog
9
9
 
package/README.md CHANGED
@@ -46,16 +46,59 @@ export default [
46
46
  ];
47
47
  ```
48
48
 
49
+ ### Canonical function-level rule and legacy aliases
50
+
51
+ For function-level checks, `traceability/require-traceability` is the **canonical** rule. It ensures that in-scope functions and methods have both story coverage and requirement coverage, and it understands both the modern `@supports` format and the legacy `@story` / `@req` pairs.
52
+
53
+ The older rule keys:
54
+
55
+ - `traceability/require-story-annotation`
56
+ - `traceability/require-req-annotation`
57
+
58
+ remain available as **backward-compatible aliases** that are wired to the same underlying engine. Existing configurations that reference these legacy keys will continue to behave as before. New configurations should normally prefer the unified `traceability/require-traceability` rule and treat the legacy keys as compatibility shims or for gradual migration.
59
+
60
+ A concise flat-config example that enables the unified function-level rule and commonly used supporting rules explicitly:
61
+
62
+ ```js
63
+ // eslint.config.js
64
+ import traceability from "eslint-plugin-traceability";
65
+
66
+ export default [
67
+ {
68
+ plugins: {
69
+ traceability,
70
+ },
71
+ rules: {
72
+ // Canonical function-level rule (preferred for new configs)
73
+ "traceability/require-traceability": "error",
74
+
75
+ // Common supporting rules
76
+ "traceability/require-branch-annotation": "warn",
77
+ "traceability/valid-annotation-format": "error",
78
+ "traceability/valid-story-reference": "error",
79
+ "traceability/valid-req-reference": "error",
80
+
81
+ // Optional: enforce test traceability conventions
82
+ "traceability/require-test-traceability": "warn",
83
+ },
84
+ },
85
+ ];
86
+ ```
87
+
49
88
  ### Available Rules
50
89
 
51
- - `traceability/require-story-annotation` Enforces presence of `@story` annotations. (See the rule documentation in the plugin's user guide.)
52
- - `traceability/require-req-annotation` Enforces presence of `@req` annotations. (See the rule documentation in the plugin's user guide.)
53
- - `traceability/require-branch-annotation` Enforces presence of branch annotations. (See the rule documentation in the plugin's user guide.)
54
- - `traceability/valid-annotation-format` Enforces correct format of traceability annotations. (See the rule documentation in the plugin's user guide.)
55
- - `traceability/valid-story-reference` Validates that `@story` references point to existing story files. (See the rule documentation in the plugin's user guide.)
56
- - `traceability/valid-req-reference` Validates that `@req` references point to existing requirement IDs. (See the rule documentation in the plugin's user guide.)
57
- - `traceability/require-test-traceability` Enforces traceability conventions in test files by requiring file-level `@supports` annotations, story references in `describe` blocks, and `[REQ-...]` prefixes in `it`/`test` names. (See the rule documentation in the plugin's user guide.)
58
- - `traceability/prefer-supports-annotation` Recommends migration from legacy `@story`/`@req` annotations to `@supports` (opt-in; disabled by default in the presets and must be explicitly enabled). The legacy rule name `traceability/prefer-implements-annotation` remains available as a deprecated alias. (See the rule documentation in the plugin's user guide.)
90
+ The plugin exposes several rules. For **new configurations**, the unified function-level rule and `@supports` annotations are the canonical choice; the `@story` and `@req` forms remain available primarily for backward compatibility and gradual migration.
91
+
92
+ - `traceability/require-traceability` **Unified function-level traceability rule.** Ensures that in-scope functions and methods have both story coverage and requirement coverage. It accepts either `@supports` (preferred for new code) or legacy `@story` / `@req` annotations and is enabled by default in the plugin's `recommended` and `strict` presets.
93
+ - `traceability/require-story-annotation` Legacy function-level rule key that focuses on the **story** side of function-level traceability. It is kept for backward compatibility and is wired to the same underlying engine as `traceability/require-traceability`, so existing configurations that refer to this rule continue to work. New configurations should normally rely on `traceability/require-traceability` instead of enabling this rule directly.
94
+ - `traceability/require-req-annotation` Legacy function-level rule key that focuses on the **requirement** side of function-level traceability. Like `traceability/require-story-annotation`, it is retained for backward compatibility and conceptually composes the same checks exposed by `traceability/require-traceability`. New configurations can usually rely on the unified rule alone unless you have specific reasons to tune the legacy keys separately.
95
+ - `traceability/require-branch-annotation` Enforces presence of branch annotations on significant control-flow branches (if/else, switch cases, loops, try/catch). Branch annotations can use a single `@supports` line (preferred) or the older `@story`/`@req` pair for backward compatibility. (See the rule documentation in the plugin's user guide.)
96
+ - `traceability/valid-annotation-format` Enforces correct format of traceability annotations, including `@supports` (preferred), `@story`, and `@req`. (See the rule documentation in the plugin's user guide.)
97
+ - `traceability/valid-story-reference` Validates that story references (whether written via `@story` or embedded in `@supports`) point to existing story files. (See the rule documentation in the plugin's user guide.)
98
+ - `traceability/valid-req-reference` – Validates that requirement identifiers (whether written via `@req` or embedded in `@supports`) point to existing requirement IDs in your story files. (See the rule documentation in the plugin's user guide.)
99
+ - `traceability/require-test-traceability` – Enforces traceability conventions in test files by requiring file-level `@supports` annotations, story references in `describe` blocks, and `[REQ-...]` prefixes in `it`/`test` names. (See the rule documentation in the plugin's user guide.)
100
+ - `traceability/no-redundant-annotation` – Detects and optionally removes redundant traceability annotations on simple leaf statements that are already covered by an enclosing annotated scope. It is enabled at severity `warn` in both the `recommended` and `strict` presets by default; consumers can override its severity (including promoting it to `error`) or disable it explicitly in their ESLint configuration if they prefer.
101
+ - `traceability/prefer-supports-annotation` – Optional migration helper that recommends converting legacy single-story `@story`/`@req` JSDoc blocks and inline comments into the newer `@supports` format. It is disabled by default and must be explicitly enabled. The legacy rule name `traceability/prefer-implements-annotation` remains available as a deprecated alias. (See the rule documentation in the plugin's user guide.)
59
102
 
60
103
  Configuration options: For detailed per-rule options (such as scopes, branch types, and story directory settings), see the individual rule docs in the plugin's user guide and the consolidated [API Reference](user-docs/api-reference.md).
61
104
 
@@ -83,15 +126,17 @@ export default [
83
126
 
84
127
  ```js
85
128
  /**
86
- * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
87
- * // Point this to your own project's story/requirements file, not to this plugin's internal docs.
88
- * @req REQ-ANNOTATION-REQUIRED
129
+ * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED
130
+ * // Prefer @supports for new implementations; @story/@req remain supported for
131
+ * // legacy and simple single-story code paths.
89
132
  */
90
133
  function initAuth() {
91
134
  // implementation...
92
135
  }
93
136
  ```
94
137
 
138
+ `@supports` is the canonical format for new, multi-story integrations and richer traceability. The legacy `@story` and `@req` forms are kept for backward compatibility and remain appropriate for simple, single-story functions or where a gradual migration is preferred.
139
+
95
140
  3. Run ESLint:
96
141
 
97
142
  ```bash
@@ -148,10 +193,12 @@ For a full description of options and JSON payloads, see the [Maintenance API an
148
193
  You can validate the plugin by running ESLint CLI with the plugin on a sample file:
149
194
 
150
195
  ```bash
151
- # Validate missing @story annotation (should report an error)
152
- npx eslint --no-eslintrc --config eslint.config.js sample.js --rule 'traceability/require-story-annotation:error'
196
+ # Validate missing function-level traceability (should report an error)
197
+ npx eslint --no-eslintrc --config eslint.config.js sample.js --rule 'traceability/require-traceability:error'
153
198
  ```
154
199
 
200
+ If you have existing configurations that reference the legacy function-level keys, you can also validate them directly by enabling `traceability/require-story-annotation` and `traceability/require-req-annotation` instead.
201
+
155
202
  This command runs ESLint with the plugin, pointing at `eslint.config.js` flat config.
156
203
 
157
204
  Replace `sample.js` with your JavaScript or TypeScript file.
@@ -237,6 +284,7 @@ For the canonical, user-facing security policy (including how to report vulnerab
237
284
  - ESLint v9 Setup Guide: [user-docs/eslint-9-setup-guide.md](user-docs/eslint-9-setup-guide.md)
238
285
  - API Reference: [user-docs/api-reference.md](user-docs/api-reference.md)
239
286
  - Examples: [user-docs/examples.md](user-docs/examples.md)
287
+ - Traceability Overview and FAQ: [user-docs/traceability-overview.md](user-docs/traceability-overview.md)
240
288
  - Migration Guide: [user-docs/migration-guide.md](user-docs/migration-guide.md)
241
289
  - Full README: <https://github.com/voder-ai/eslint-plugin-traceability#readme>
242
290
  - Contribution guide: <https://github.com/voder-ai/eslint-plugin-traceability/blob/main/CONTRIBUTING.md>
package/lib/src/index.js CHANGED
@@ -76,6 +76,65 @@ RULE_NAMES.forEach(
76
76
  };
77
77
  }
78
78
  });
79
+ /**
80
+ * Wire up the unified function-annotation rule and its backward-compatible
81
+ * aliases so that:
82
+ * - traceability/require-traceability is the canonical rule implementation
83
+ * - traceability/require-story-annotation and
84
+ * traceability/require-req-annotation act as aliases that share the same
85
+ * underlying logic while preserving their legacy metadata (docs, schema,
86
+ * and diagnostics).
87
+ *
88
+ * @supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED REQ-CONFIGURABLE-SCOPE REQ-EXPORT-PRIORITY
89
+ */
90
+ {
91
+ const unifiedRule = rules["require-traceability"];
92
+ const legacyStoryRule = rules["require-story-annotation"];
93
+ const legacyReqRule = rules["require-req-annotation"];
94
+ if (unifiedRule) {
95
+ const createAliasRule = (legacyRule) => {
96
+ if (!legacyRule) {
97
+ return unifiedRule;
98
+ }
99
+ const baseMeta = (unifiedRule.meta ?? {});
100
+ const legacyMeta = (legacyRule.meta ?? {});
101
+ const mergedMeta = {
102
+ ...baseMeta,
103
+ ...legacyMeta,
104
+ docs: {
105
+ ...(baseMeta.docs ?? {}),
106
+ ...(legacyMeta.docs ?? {}),
107
+ },
108
+ messages: {
109
+ ...(baseMeta.messages ?? {}),
110
+ ...(legacyMeta.messages ?? {}),
111
+ },
112
+ schema: legacyMeta.schema ??
113
+ baseMeta.schema ??
114
+ [],
115
+ hasSuggestions: legacyMeta.hasSuggestions ??
116
+ baseMeta.hasSuggestions,
117
+ fixable: legacyMeta.fixable ??
118
+ baseMeta.fixable,
119
+ deprecated: legacyMeta.deprecated ??
120
+ baseMeta.deprecated,
121
+ replacedBy: legacyMeta.replacedBy ??
122
+ baseMeta.replacedBy,
123
+ type: legacyMeta.type ??
124
+ baseMeta.type ??
125
+ "problem",
126
+ };
127
+ const aliasRule = {
128
+ ...unifiedRule,
129
+ meta: mergedMeta,
130
+ create: unifiedRule.create,
131
+ };
132
+ return aliasRule;
133
+ };
134
+ rules["require-story-annotation"] = createAliasRule(legacyStoryRule);
135
+ rules["require-req-annotation"] = createAliasRule(legacyReqRule);
136
+ }
137
+ }
79
138
  /**
80
139
  * @supports docs/stories/010.3-DEV-MIGRATE-TO-SUPPORTS.story.md REQ-RULE-NAME
81
140
  * Wire up traceability/prefer-supports-annotation as the primary rule name and
@@ -133,7 +133,7 @@ function createMissingStoryReportDescriptor(config) {
133
133
  fix: allowFix ? baseFix : undefined,
134
134
  suggest: [
135
135
  {
136
- desc: `Add JSDoc @story annotation for function '${name}', e.g., ${effectiveTemplate}`,
136
+ desc: `Add traceability annotation for function '${name}' using @supports (preferred) or @story (legacy), for example: ${effectiveTemplate.replace("@story", "@supports")}`,
137
137
  fix: baseFix,
138
138
  },
139
139
  ],
@@ -17,10 +17,15 @@ import { DEFAULT_SCOPE, EXPORT_PRIORITY_VALUES, STORY_PATH } from "./require-sto
17
17
  interface ReportOptions {
18
18
  annotationTemplateOverride?: string;
19
19
  autoFixToggle?: boolean;
20
+ excludeTestCallbacks?: boolean;
20
21
  }
21
22
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
22
- declare function getAnnotationTemplate(override?: string): string;
23
- declare function shouldApplyAutoFix(autoFix: boolean | undefined): boolean;
23
+ declare function getAnnotationTemplate(override?: string, _options?: {
24
+ excludeTestCallbacks?: boolean;
25
+ }): string;
26
+ declare function shouldApplyAutoFix(autoFix: boolean | undefined, _options?: {
27
+ excludeTestCallbacks?: boolean;
28
+ }): boolean;
24
29
  /**
25
30
  * Determine if a node is in an export declaration
26
31
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
@@ -65,7 +70,9 @@ declare function resolveTargetNode(sourceCode: any, node: any): any;
65
70
  */
66
71
  declare function extractName(node: any): string;
67
72
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
68
- declare function shouldProcessNode(node: any, scope: string[], exportPriority?: string): boolean;
73
+ declare function shouldProcessNode(node: any, scope: string[], exportPriority?: string, options?: {
74
+ excludeTestCallbacks?: boolean;
75
+ }): boolean;
69
76
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
70
77
  declare function reportMissing(context: Rule.RuleContext, sourceCode: any, config: {
71
78
  node: any;
@@ -23,14 +23,46 @@ const require_story_core_1 = require("./require-story-core");
23
23
  Object.defineProperty(exports, "DEFAULT_SCOPE", { enumerable: true, get: function () { return require_story_core_1.DEFAULT_SCOPE; } });
24
24
  Object.defineProperty(exports, "EXPORT_PRIORITY_VALUES", { enumerable: true, get: function () { return require_story_core_1.EXPORT_PRIORITY_VALUES; } });
25
25
  Object.defineProperty(exports, "STORY_PATH", { enumerable: true, get: function () { return require_story_core_1.STORY_PATH; } });
26
+ /**
27
+ * Known test framework function names and variants.
28
+ * Includes Jest, Mocha, Vitest and their focused/skipped/concurrent variants.
29
+ * @req REQ-TEST-CALLBACK-EXCLUSION
30
+ */
31
+ const TEST_FUNCTION_NAMES = new Set([
32
+ // Core test/describe-style functions (Jest, Mocha, Vitest share many of these)
33
+ "it",
34
+ "test",
35
+ "describe",
36
+ "suite",
37
+ // Focused variants
38
+ "fit",
39
+ "ftest",
40
+ "fdescribe",
41
+ "fsuite",
42
+ // Skipped variants
43
+ "xit",
44
+ "xtest",
45
+ "xdescribe",
46
+ "xsuite",
47
+ // Additional common aliases
48
+ "context",
49
+ "specify",
50
+ "before",
51
+ "after",
52
+ "beforeEach",
53
+ "afterEach",
54
+ "beforeAll",
55
+ "afterAll",
56
+ ]);
57
+ const TEST_FUNCTION_CONCURRENT_PROP = "concurrent";
26
58
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
27
- function getAnnotationTemplate(override) {
59
+ function getAnnotationTemplate(override, _options) {
28
60
  if (typeof override === "string" && override.trim().length > 0) {
29
61
  return override.trim();
30
62
  }
31
63
  return `/** @story ${require_story_core_1.STORY_PATH} */`;
32
64
  }
33
- function shouldApplyAutoFix(autoFix) {
65
+ function shouldApplyAutoFix(autoFix, _options) {
34
66
  if (autoFix === false) {
35
67
  return false;
36
68
  }
@@ -42,8 +74,10 @@ function shouldApplyAutoFix(autoFix) {
42
74
  */
43
75
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
44
76
  function buildTemplateConfig(options) {
45
- const effectiveTemplate = getAnnotationTemplate(options?.annotationTemplateOverride);
46
- const allowFix = shouldApplyAutoFix(options?.autoFixToggle);
77
+ const effectiveTemplate = getAnnotationTemplate(options?.annotationTemplateOverride, { excludeTestCallbacks: options?.excludeTestCallbacks });
78
+ const allowFix = shouldApplyAutoFix(options?.autoFixToggle, {
79
+ excludeTestCallbacks: options?.excludeTestCallbacks,
80
+ });
47
81
  return { effectiveTemplate, allowFix };
48
82
  }
49
83
  /**
@@ -89,6 +123,46 @@ function isEffectivelyAnonymousFunction(node) {
89
123
  }
90
124
  return true;
91
125
  }
126
+ /**
127
+ * Determine whether a node represents a callback passed to a known test
128
+ * framework function (Jest, Mocha, Vitest, etc).
129
+ *
130
+ * Supports:
131
+ * - it(), test(), describe(), suite(), context(), specify()
132
+ * - lifecycle hooks: before(), after(), beforeEach(), afterEach(), beforeAll(), afterAll()
133
+ * - focused variants: fit(), ftest(), fdescribe(), fsuite()
134
+ * - skipped variants and helpers: xit(), xtest(), xdescribe(), xsuite()
135
+ * - their .concurrent variants (e.g., it.concurrent(), test.concurrent())
136
+ *
137
+ * @req REQ-TEST-CALLBACK-EXCLUSION
138
+ */
139
+ function isTestFrameworkCallback(node, options) {
140
+ if (options?.excludeTestCallbacks === false) {
141
+ return false;
142
+ }
143
+ if (!node || node.type !== "ArrowFunctionExpression") {
144
+ return false;
145
+ }
146
+ const parent = node.parent;
147
+ if (!parent || parent.type !== "CallExpression") {
148
+ return false;
149
+ }
150
+ const callee = parent.callee;
151
+ if (callee.type === "Identifier") {
152
+ return TEST_FUNCTION_NAMES.has(callee.name);
153
+ }
154
+ if (callee.type === "MemberExpression" &&
155
+ !callee.computed &&
156
+ callee.property &&
157
+ callee.property.type === "Identifier" &&
158
+ callee.property.name === TEST_FUNCTION_CONCURRENT_PROP) {
159
+ const obj = callee.object;
160
+ if (obj && obj.type === "Identifier") {
161
+ return TEST_FUNCTION_NAMES.has(obj.name);
162
+ }
163
+ }
164
+ return false;
165
+ }
92
166
  /**
93
167
  * Determine whether a function node is required to carry its own annotation
94
168
  * according to Story 004.0-DEV-BRANCH-ANNOTATIONS rules.
@@ -102,7 +176,10 @@ function isEffectivelyAnonymousFunction(node) {
102
176
  *
103
177
  * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-ARROW-FUNCTION-EXCLUDED REQ-NESTED-FUNCTION-INHERITANCE
104
178
  */
105
- function requiresOwnFunctionAnnotation(node) {
179
+ function requiresOwnFunctionAnnotation(node, options) {
180
+ if (isTestFrameworkCallback(node, options)) {
181
+ return false;
182
+ }
106
183
  // Anonymous arrow functions used as callbacks are excluded from function-level
107
184
  // requirements when they are nested inside another function or method.
108
185
  if (isAnonymousArrowFunction(node) &&
@@ -306,12 +383,12 @@ function extractName(node) {
306
383
  return "(anonymous)";
307
384
  }
308
385
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
309
- function shouldProcessNode(node, scope, exportPriority = "all") {
386
+ function shouldProcessNode(node, scope, exportPriority = "all", options) {
310
387
  if (node &&
311
388
  (node.type === "FunctionDeclaration" ||
312
389
  node.type === "FunctionExpression" ||
313
390
  node.type === "ArrowFunctionExpression") &&
314
- !requiresOwnFunctionAnnotation(node)) {
391
+ !requiresOwnFunctionAnnotation(node, options)) {
315
392
  return false;
316
393
  }
317
394
  if (!scope.includes(node.type)) {
@@ -48,49 +48,13 @@ function normalizeOptions(raw) {
48
48
  };
49
49
  }
50
50
  /**
51
- * Compute the story/requirement pairs for annotations that apply to the
52
- * given scope node.
53
- *
54
- * For branch scopes we reuse the same comment-gathering helper used by
55
- * the require-branch-annotation rule so that REQ-SCOPE-INHERITANCE
56
- * aligns with existing behavior.
51
+ * Collect comments around a scope node using JSDoc, leading comments,
52
+ * and any comments that appear immediately before the node.
57
53
  *
58
54
  * @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SCOPE-ANALYSIS REQ-SCOPE-INHERITANCE
59
55
  */
60
- function getScopePairs(context, scopeNode, parent) {
61
- const sourceCode = context.getSourceCode();
62
- // Branch-style scope: use the branch helpers to collect comment text.
63
- if (branch_annotation_helpers_1.DEFAULT_BRANCH_TYPES.includes(scopeNode.type)) {
64
- const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, scopeNode, parent);
65
- return (0, annotation_scope_analyzer_1.extractStoryReqPairsFromText)(text);
66
- }
67
- // Function-like scopes: collect from JSDoc and leading/before comments
68
- const FUNCTION_LIKE_TYPES = new Set([
69
- "FunctionDeclaration",
70
- "FunctionExpression",
71
- "ArrowFunctionExpression",
72
- "MethodDefinition",
73
- "TSDeclareFunction",
74
- "TSMethodSignature",
75
- ]);
56
+ function getScopeCommentsFromJSDocAndLeading(sourceCode, scopeNode) {
76
57
  const comments = [];
77
- if (FUNCTION_LIKE_TYPES.has(scopeNode.type)) {
78
- const jsdoc = sourceCode.getJSDocComment
79
- ? sourceCode.getJSDocComment(scopeNode)
80
- : null;
81
- const before = sourceCode.getCommentsBefore
82
- ? sourceCode.getCommentsBefore(scopeNode) || []
83
- : [];
84
- if (jsdoc) {
85
- comments.push(jsdoc);
86
- }
87
- if (Array.isArray(scopeNode.leadingComments)) {
88
- comments.push(...scopeNode.leadingComments);
89
- }
90
- comments.push(...before);
91
- return (0, annotation_scope_analyzer_1.extractStoryReqPairsFromComments)(comments);
92
- }
93
- // Fallback: inspect JSDoc and leading comments around the scope node.
94
58
  const jsdoc = sourceCode.getJSDocComment
95
59
  ? sourceCode.getJSDocComment(scopeNode)
96
60
  : null;
@@ -104,6 +68,28 @@ function getScopePairs(context, scopeNode, parent) {
104
68
  comments.push(...scopeNode.leadingComments);
105
69
  }
106
70
  comments.push(...before);
71
+ return comments;
72
+ }
73
+ /**
74
+ * Compute the story/requirement pairs for annotations that apply to the
75
+ * given scope node.
76
+ *
77
+ * For branch scopes we reuse the same comment-gathering helper used by
78
+ * the require-branch-annotation rule so that REQ-SCOPE-INHERITANCE
79
+ * aligns with existing behavior. For non-branch scopes, we reuse a
80
+ * shared helper that collects JSDoc, leading, and immediately-before
81
+ * comments around the scope node.
82
+ *
83
+ * @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SCOPE-ANALYSIS REQ-SCOPE-INHERITANCE
84
+ */
85
+ function getScopePairs(context, scopeNode, parent) {
86
+ const sourceCode = context.getSourceCode();
87
+ // Branch-style scope: use the branch helpers to collect comment text.
88
+ if (branch_annotation_helpers_1.DEFAULT_BRANCH_TYPES.includes(scopeNode.type)) {
89
+ const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, scopeNode, parent);
90
+ return (0, annotation_scope_analyzer_1.extractStoryReqPairsFromText)(text);
91
+ }
92
+ const comments = getScopeCommentsFromJSDocAndLeading(sourceCode, scopeNode);
107
93
  return (0, annotation_scope_analyzer_1.extractStoryReqPairsFromComments)(comments);
108
94
  }
109
95
  /**
@@ -162,14 +148,12 @@ function collectScopePairs(context, startingScopeNode, maxScopeDepth) {
162
148
  return result;
163
149
  }
164
150
  /**
165
- * Determine whether a statement is redundant relative to the provided
166
- * scopePairs and options, and when so return the associated annotation
167
- * comments. Returns null when the statement should not be treated as
168
- * redundant.
151
+ * Extract statement-level comments and story/requirement pairs that are
152
+ * relevant for redundancy analysis within a given scope.
169
153
  *
170
- * @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-REDUNDANCY-PATTERNS REQ-SAFE-REMOVAL REQ-STATEMENT-SIGNIFICANCE REQ-CONFIGURABLE-STRICTNESS
154
+ * @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-REDUNDANCY-PATTERNS REQ-STATEMENT-SIGNIFICANCE REQ-SCOPE-ANALYSIS
171
155
  */
172
- function getRedundantStatementContext(context, stmt, scopePairs, options) {
156
+ function getStatementPairsForRedundancy(context, stmt, scopePairs, options) {
173
157
  if (scopePairs.size === 0) {
174
158
  return null;
175
159
  }
@@ -187,22 +171,56 @@ function getRedundantStatementContext(context, stmt, scopePairs, options) {
187
171
  if (stmtPairs.size === 0) {
188
172
  return null;
189
173
  }
190
- // When emphasis duplication is allowed, treat a single fully-covered
191
- // pair as intentional emphasis and skip reporting.
192
- if (options.allowEmphasisDuplication && stmtPairs.size === 1) {
193
- if ((0, annotation_scope_analyzer_1.arePairsFullyCovered)(stmtPairs, scopePairs)) {
194
- return null;
195
- }
174
+ return { comments: stmtComments, pairs: stmtPairs };
175
+ }
176
+ /**
177
+ * Decide whether the provided statement-level pairs should be considered
178
+ * redundant within the given scope, respecting configuration options.
179
+ *
180
+ * @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-REDUNDANCY-PATTERNS REQ-SAFE-REMOVAL REQ-CONFIGURABLE-STRICTNESS
181
+ */
182
+ function isStatementRedundantWithinScope(stmtPairs, scopePairs, options) {
183
+ if (options.allowEmphasisDuplication &&
184
+ stmtPairs.size === 1 &&
185
+ (0, annotation_scope_analyzer_1.arePairsFullyCovered)(stmtPairs, scopePairs)) {
186
+ return false;
196
187
  }
197
188
  if (!(0, annotation_scope_analyzer_1.arePairsFullyCovered)(stmtPairs, scopePairs)) {
198
- return null;
189
+ return false;
199
190
  }
200
- // At this point the statement-level annotations are fully
201
- // covered by the parent/ancestor scopes and therefore redundant.
202
- const annotationComments = stmtComments.filter((comment) => {
191
+ return true;
192
+ }
193
+ /**
194
+ * Filter a list of comments down to those that contain traceability
195
+ * annotations relevant for redundancy detection.
196
+ *
197
+ * @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SAFE-REMOVAL REQ-REDUNDANCY-PATTERNS
198
+ */
199
+ function getAnnotationCommentsFromStatement(comments) {
200
+ return comments.filter((comment) => {
203
201
  const commentText = typeof comment.value === "string" ? comment.value : "";
204
202
  return /@story\b|@req\b|@supports\b/.test(commentText);
205
203
  });
204
+ }
205
+ /**
206
+ * Determine whether a statement is redundant relative to the provided
207
+ * scopePairs and options, using helper functions to gather statement
208
+ * pairs, apply redundancy rules, and collect the associated annotation
209
+ * comments. Returns null when the statement should not be treated as
210
+ * redundant.
211
+ *
212
+ * @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-REDUNDANCY-PATTERNS REQ-SAFE-REMOVAL REQ-STATEMENT-SIGNIFICANCE REQ-CONFIGURABLE-STRICTNESS
213
+ */
214
+ function getRedundantStatementContext(context, stmt, scopePairs, options) {
215
+ const stmtInfo = getStatementPairsForRedundancy(context, stmt, scopePairs, options);
216
+ if (!stmtInfo) {
217
+ return null;
218
+ }
219
+ const { comments, pairs } = stmtInfo;
220
+ if (!isStatementRedundantWithinScope(pairs, scopePairs, options)) {
221
+ return null;
222
+ }
223
+ const annotationComments = getAnnotationCommentsFromStatement(comments);
206
224
  if (annotationComments.length === 0) {
207
225
  return null;
208
226
  }
@@ -79,7 +79,7 @@ const rule = {
79
79
  meta: {
80
80
  type: "problem",
81
81
  docs: {
82
- description: "Require @story and @req annotations on code branches",
82
+ description: "Require traceability annotations on significant code branches, preferring @supports for combined story and requirement coverage while still accepting legacy @story and @req comments.",
83
83
  recommended: "error",
84
84
  },
85
85
  fixable: "code",
@@ -88,7 +88,7 @@ const rule = {
88
88
  * @story docs/stories/007.0-DEV-ERROR-REPORTING.story.md
89
89
  * @req REQ-ERROR-CONSISTENCY - Use shared branch error message convention with {{missing}} placeholder
90
90
  */
91
- missingAnnotation: "Branch is missing required annotation: {{missing}}.",
91
+ missingAnnotation: "Branch is missing required traceability annotation: {{missing}}. Prefer using a single @supports line that links this branch to its story and requirements (for example, '@supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-BRANCH-DETECTION'), or add the missing legacy tag if you are not yet using @supports.",
92
92
  },
93
93
  schema: [
94
94
  {
@@ -7,7 +7,7 @@ const rule = {
7
7
  type: "problem",
8
8
  fixable: "code",
9
9
  docs: {
10
- description: "Require @req annotations on function-like exports (declarations, expressions, and methods, excluding arrow functions)",
10
+ description: "Require traceability annotations on function-like exports, preferring @supports for requirement coverage while still accepting legacy @req annotations.",
11
11
  recommended: "error",
12
12
  },
13
13
  messages: {
@@ -23,7 +23,7 @@ const rule = {
23
23
  * This rule uses ESLint's message data placeholders for the function name,
24
24
  * specifically the {{name}} placeholder populated via context.report.
25
25
  */
26
- missingReq: "Function '{{functionName}}' is missing a required @req annotation. Add a JSDoc or line comment with @req (for example, '@req REQ-EXAMPLE') referencing the appropriate requirement from the story file.",
26
+ missingReq: "Function '{{functionName}}' is missing required traceability annotations. Prefer adding an @supports line that links this function to at least one requirement (for example, '@supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-EXAMPLE'), or, when you are limited to a single-story context, add a legacy @req annotation such as '@req REQ-EXAMPLE' referencing the appropriate requirement from the story file.",
27
27
  },
28
28
  schema: [
29
29
  {
@@ -18,7 +18,7 @@ const rule = {
18
18
  meta: {
19
19
  type: "problem",
20
20
  docs: {
21
- description: "Require @story annotations on functions and auto-fix missing annotations where possible",
21
+ description: "Require traceability annotations on functions and methods, preferring @supports for story coverage while still accepting legacy @story annotations, and provide optional auto-fix for missing annotations.",
22
22
  recommended: "error",
23
23
  },
24
24
  hasSuggestions: true,
@@ -33,7 +33,7 @@ const rule = {
33
33
  */
34
34
  fixable: "code",
35
35
  messages: {
36
- missingStory: "Function '{{name}}' must have an explicit @story annotation. Add a JSDoc or line comment with @story that points to the implementing story file, such as docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md.",
36
+ missingStory: "Function '{{name}}' must declare a traceability annotation. Prefer adding an @supports line that links this function to at least one story (for example, '@supports docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED'), or, when you only need a single-story reference, add a legacy @story annotation that points to the implementing story file, such as docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md.",
37
37
  },
38
38
  schema: [
39
39
  {
@@ -48,6 +48,7 @@ const rule = {
48
48
  annotationTemplate: { type: "string" },
49
49
  methodAnnotationTemplate: { type: "string" },
50
50
  autoFix: { type: "boolean" },
51
+ excludeTestCallbacks: { type: "boolean" },
51
52
  },
52
53
  additionalProperties: false,
53
54
  },
@@ -75,6 +76,9 @@ const rule = {
75
76
  ? opts.methodAnnotationTemplate.trim()
76
77
  : undefined;
77
78
  const autoFix = typeof opts.autoFix === "boolean" ? opts.autoFix : true;
79
+ const excludeTestCallbacks = typeof opts.excludeTestCallbacks === "boolean"
80
+ ? opts.excludeTestCallbacks
81
+ : true;
78
82
  /**
79
83
  * Optional debug logging for troubleshooting this rule.
80
84
  * Developers can temporarily uncomment the block below to log when the rule
@@ -90,7 +94,7 @@ const rule = {
90
94
  // : "<unknown>",
91
95
  // );
92
96
  // Local closure that binds configured scope and export priority to the helper.
93
- const should = (node) => (0, require_story_helpers_1.shouldProcessNode)(node, scope, exportPriority);
97
+ const should = (node) => (0, require_story_helpers_1.shouldProcessNode)(node, scope, exportPriority, { excludeTestCallbacks });
94
98
  // Delegate visitor construction to helper to keep this file concise.
95
99
  return (0, require_story_visitors_1.buildVisitors)(context, sourceCode, {
96
100
  shouldProcessNode: should,
@@ -99,6 +103,7 @@ const rule = {
99
103
  annotationTemplate,
100
104
  methodAnnotationTemplate,
101
105
  autoFix,
106
+ excludeTestCallbacks,
102
107
  });
103
108
  },
104
109
  };