eslint-plugin-traceability 1.17.1 → 1.19.0

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.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,9 @@
1
- ## [1.17.1](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.17.0...v1.17.1) (2025-12-10)
1
+ # [1.19.0](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.18.0...v1.19.0) (2025-12-18)
2
2
 
3
3
 
4
- ### Bug Fixes
4
+ ### Features
5
5
 
6
- * avoid redundant-annotation false positives for catch blocks ([2ac69e2](https://github.com/voder-ai/eslint-plugin-traceability/commit/2ac69e2a03b54cf29bf2bc175771bf3b23aba6e9))
6
+ * enforce inside-brace placement mode for branch annotations ([5c2129a](https://github.com/voder-ai/eslint-plugin-traceability/commit/5c2129a7c25717dc4ce14bf55bc4541ae07e5539))
7
7
 
8
8
  # Changelog
9
9
 
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # eslint-plugin-traceability
2
2
 
3
- A customizable ESLint plugin that enforces traceability annotations in your code, ensuring each implementation is linked to its requirement or test case.
3
+ An ESLint plugin that creates searchable verification indices in your code, enabling systematic validation of requirement-to-code mapping.
4
+
5
+ **The Core Value:** Transforms an impossible question ("Does code exist somewhere that should support requirement X?") into a tractable question ("Does this annotated code actually support the requirement it claims to?").
6
+
7
+ The plugin requires `@supports` annotations at key verification points (functions and control flow branches), creating direct checkpoints where each annotation is a verifiable claim you can search, find, and verify locally without needing to understand the broader codebase context.
4
8
 
5
9
  ## Attribution
6
10
 
@@ -89,21 +93,89 @@ export default [
89
93
 
90
94
  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
95
 
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.)
96
+ #### Core Verification Index Rules
97
+
98
+ - `traceability/require-traceability` – **Unified function-level verification rule.** Ensures every function has a verification checkpoint. Without this, functions could exist without explaining their purpose, breaking verification completeness. 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.
99
+
100
+ - `traceability/require-story-annotation` – Legacy function-level rule key that focuses on the **story** side of function-level verification. It is kept for backward compatibility and is wired to the same underlying engine as `traceability/require-traceability`. New configurations should normally rely on `traceability/require-traceability` instead.
101
+
102
+ - `traceability/require-req-annotation` – Legacy function-level rule key that focuses on the **requirement** side of function-level verification. Like `traceability/require-story-annotation`, it is retained for backward compatibility. New configurations can usually rely on the unified rule alone unless you have specific reasons to tune the legacy keys separately.
103
+
104
+ - `traceability/require-branch-annotation` – **Creates verification checkpoints at every branch** (if/else/switch/try/catch). This enables local verification - you can check each branch independently without understanding the entire function. Without branch annotations, you must read entire functions to find branch logic. With branch annotations, you can verify each branch by searching for the requirement ID. Branch annotations can use `@supports` (preferred) or the older `@story`/`@req` pair for backward compatibility.
105
+
106
+ - `traceability/valid-annotation-format` – **Ensures annotations are searchable** with consistent patterns. Verification workflows depend on being able to search for requirement IDs reliably. Enforces correct format of traceability annotations, including `@supports` (preferred), `@story`, and `@req`.
107
+
108
+ - `traceability/valid-story-reference` – **Validates story file references** to ensure verification checkpoints point to real documentation. Validates that story references (whether written via `@story` or embedded in `@supports`) point to existing story files.
109
+
110
+ - `traceability/valid-req-reference` – **Validates requirement identifiers** to ensure verification checkpoints reference valid requirements. Validates that requirement identifiers (whether written via `@req` or embedded in `@supports`) point to existing requirement IDs in your story files.
111
+
112
+ - `traceability/require-test-traceability` – **Enforces verification conventions in test files** by requiring file-level `@supports` annotations, story references in `describe` blocks, and `[REQ-...]` prefixes in `it`/`test` names. This ensures tests are traceable back to requirements for comprehensive verification coverage.
113
+
114
+ #### Quality Rules
115
+
116
+ - `traceability/no-redundant-annotation` – **Removes annotations on simple statements** already covered by enclosing scope. These don't create useful verification checkpoints (no branches to verify), so removing them reduces noise without hurting verifiability. It is enabled at severity `warn` in both the `recommended` and `strict` presets by default; consumers can override its severity or disable it explicitly.
117
+
118
+ - `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 for better verification trails. It is disabled by default and must be explicitly enabled. The legacy rule name `traceability/prefer-implements-annotation` remains available as a deprecated alias.
102
119
 
103
120
  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).
104
121
 
105
122
  For development and contribution guidelines, see the contribution guide in the repository.
106
123
 
124
+ ## How It Works
125
+
126
+ ### The Verification Workflow
127
+
128
+ Traceability annotations enable systematic requirement verification through a simple three-step process:
129
+
130
+ 1. **Search:** `grep -r "REQ-AUTH-VALIDATION" src/`
131
+ 2. **Find:** Get a list of every location claiming to support that requirement
132
+ 3. **Verify:** For each result, read the annotation and adjacent code to confirm they match
133
+
134
+ ### Why Annotations at Every Branch
135
+
136
+ Each annotation creates a **local verification checkpoint**:
137
+
138
+ - ✅ **Searchable** - Direct search finds all implementation claims
139
+ - ✅ **Local** - Annotation is adjacent to the code being verified
140
+ - ✅ **No context needed** - Verify the claim without understanding control flow
141
+ - ✅ **Parallelizable** - Multiple reviewers work independently
142
+ - ✅ **Complete coverage** - No code can exist without explaining its purpose
143
+
144
+ This enables you to verify requirements systematically rather than trying to remember which code should implement what.
145
+
146
+ ### Example: Verifying a Switch Statement
147
+
148
+ ```javascript
149
+ switch (severity) {
150
+ // @supports docs/stories/logging.md REQ-SEVERITY-LEVELS
151
+ case "low":
152
+ logLevel = "info";
153
+ break;
154
+ // @supports docs/stories/logging.md REQ-SEVERITY-LEVELS
155
+ case "moderate":
156
+ logLevel = "warn";
157
+ break;
158
+ // @supports docs/stories/logging.md REQ-SEVERITY-LEVELS
159
+ case "high":
160
+ logLevel = "error";
161
+ break;
162
+ }
163
+ ```
164
+
165
+ **Verification process:**
166
+
167
+ - Search finds 3 annotations
168
+ - Verify each: "Does this case handle REQ-SEVERITY-LEVELS correctly?"
169
+ - Time: ~30 seconds per case, ~90 seconds total
170
+ - Can be split across team members
171
+
172
+ **Without explicit annotations:**
173
+
174
+ - Must read entire switch to understand what it does
175
+ - Must remember which requirement you're checking
176
+ - Cannot split the work effectively
177
+ - Risk missing cases or misunderstanding intent
178
+
107
179
  ## Quick Start
108
180
 
109
181
  1. Create a flat ESLint config file (`eslint.config.js`):
@@ -143,6 +215,30 @@ function initAuth() {
143
215
  npx eslint "src/**/*.js"
144
216
  ```
145
217
 
218
+ ## Common Misconceptions
219
+
220
+ ### "The annotations are redundant"
221
+
222
+ **Reality:** The apparent redundancy is intentional indexing. Each annotation is a separate verification checkpoint. When the same requirement appears in multiple branches, each annotation creates an independent checkpoint that can be verified in ~30 seconds without understanding the broader code structure.
223
+
224
+ Removing "duplicate" annotations would break the verification workflow by forcing reviewers to trace through code structure instead of verifying local claims.
225
+
226
+ ### "This only works for small codebases"
227
+
228
+ **Reality:** The plugin is specifically designed for codebases where manual review is impractical. Small codebases could use manual review; larger ones need systematic, searchable verification indices to make requirement validation tractable.
229
+
230
+ ### "This is just documentation"
231
+
232
+ **Reality:** These are verification indices, not documentation. The goal is searchability and local verification, not explaining code. Documentation aims to reduce redundancy; verification indices intentionally create checkpoints for independent validation.
233
+
234
+ ### "Inheritance would reduce boilerplate"
235
+
236
+ **Reality:** Inheritance would break verification tractability. When you search for a requirement, you need to find every place that implements it - not find a parent annotation and manually trace what inherits from it. Direct, explicit annotations at each implementation point enable grep-based verification.
237
+
238
+ ## Verification Workflow Guide
239
+
240
+ For detailed verification workflows, examples, and best practices, see the [Verification Workflow Guide](docs/verification-workflow-guide.md).
241
+
146
242
  ## API Reference
147
243
 
148
244
  Detailed API specification and configuration options can be found in the [API Reference](user-docs/api-reference.md).
@@ -86,7 +86,14 @@ function getScopePairs(context, scopeNode, parent) {
86
86
  const sourceCode = context.getSourceCode();
87
87
  // Branch-style scope: use the branch helpers to collect comment text.
88
88
  if (branch_annotation_helpers_1.DEFAULT_BRANCH_TYPES.includes(scopeNode.type)) {
89
- const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, scopeNode, parent);
89
+ /**
90
+ * Inside-brace annotations used as branch-level indicators (inside placement
91
+ * mode) should not be folded into scopePairs for redundancy purposes; only
92
+ * before-brace annotations define the covering scope here.
93
+ *
94
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-NON-REDUNDANT-INSIDE REQ-PLACEMENT-CONFIG
95
+ */
96
+ const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, scopeNode, parent, "before");
90
97
  return (0, annotation_scope_analyzer_1.extractStoryReqPairsFromText)(text);
91
98
  }
92
99
  const comments = getScopeCommentsFromJSDocAndLeading(sourceCode, scopeNode);
@@ -99,6 +99,12 @@ const rule = {
99
99
  items: { type: "string" },
100
100
  uniqueItems: true,
101
101
  },
102
+ /**
103
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG REQ-DEFAULT-BACKWARD-COMPAT
104
+ */
105
+ annotationPlacement: {
106
+ enum: ["before", "inside"],
107
+ },
102
108
  },
103
109
  additionalProperties: false,
104
110
  },
@@ -124,6 +130,16 @@ const rule = {
124
130
  return branchTypesOrListener;
125
131
  }
126
132
  const branchTypes = branchTypesOrListener;
133
+ /**
134
+ * Resolve annotation placement configuration with backward-compatible default.
135
+ *
136
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG REQ-DEFAULT-BACKWARD-COMPAT
137
+ */
138
+ const rawOptions = context.options[0] || {};
139
+ const _annotationPlacement = rawOptions.annotationPlacement === "inside" ||
140
+ rawOptions.annotationPlacement === "before"
141
+ ? rawOptions.annotationPlacement
142
+ : "before";
127
143
  const storyFixCountRef = { count: 0 };
128
144
  const handlers = {};
129
145
  branchTypes.forEach((type) => {
@@ -10,6 +10,12 @@ export declare const DEFAULT_BRANCH_TYPES: readonly ["IfStatement", "SwitchCase"
10
10
  * Type for branch nodes supported by require-branch-annotation rule.
11
11
  */
12
12
  export type BranchType = (typeof DEFAULT_BRANCH_TYPES)[number];
13
+ /**
14
+ * Placement options for branch annotations relative to their associated branch.
15
+ * @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
16
+ * @req REQ-PLACEMENT-CONFIG - Allow configuration of annotation placement (before/inside)
17
+ */
18
+ export type AnnotationPlacement = "before" | "inside";
13
19
  /**
14
20
  * Validate branchTypes configuration option and return branch types to enforce,
15
21
  * or return an ESLint listener if configuration is invalid.
@@ -33,8 +39,9 @@ export declare function scanCommentLinesInRange(lines: string[], startIndex: num
33
39
  * @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
34
40
  * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
35
41
  * @supports REQ-DUAL-POSITION-DETECTION
42
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
36
43
  */
37
- export declare function gatherBranchCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any, parent?: any): string;
44
+ export declare function gatherBranchCommentText(sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>, node: any, parent?: any, annotationPlacement?: AnnotationPlacement): string;
38
45
  /**
39
46
  * Report missing @story annotation tag on a branch node when that branch lacks a corresponding @story reference in its comments.
40
47
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
@@ -195,6 +195,55 @@ function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
195
195
  }
196
196
  return beforeText;
197
197
  }
198
+ /**
199
+ * Gather annotation text for simple IfStatement branches, honoring the configured placement.
200
+ * When placement is "before", this helper preserves the existing behavior by returning the
201
+ * leading comment text unchanged. When placement is "inside", it switches to inside-brace
202
+ * semantics and scans for comments at the top of the consequent block.
203
+ * @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
204
+ * @supports REQ-INSIDE-BRACE-PLACEMENT
205
+ * @supports REQ-PLACEMENT-CONFIG
206
+ * @supports REQ-DEFAULT-BACKWARD-COMPAT
207
+ */
208
+ function gatherSimpleIfCommentText(sourceCode, node, annotationPlacement, beforeText) {
209
+ if (annotationPlacement === "before") {
210
+ return beforeText;
211
+ }
212
+ if (annotationPlacement !== "inside") {
213
+ return beforeText;
214
+ }
215
+ if (!node.consequent || node.consequent.type !== "BlockStatement") {
216
+ return "";
217
+ }
218
+ const consequent = node.consequent;
219
+ const getCommentsInside = sourceCode.getCommentsInside;
220
+ if (typeof getCommentsInside === "function") {
221
+ try {
222
+ const insideComments = getCommentsInside(consequent) || [];
223
+ const insideText = insideComments.map(extractCommentValue).join(" ");
224
+ if (insideText) {
225
+ return insideText;
226
+ }
227
+ }
228
+ catch {
229
+ // fall through to line-based fallback
230
+ }
231
+ }
232
+ if (consequent.loc &&
233
+ consequent.loc.start &&
234
+ consequent.loc.end &&
235
+ typeof consequent.loc.start.line === "number" &&
236
+ typeof consequent.loc.end.line === "number") {
237
+ const lines = sourceCode.lines;
238
+ const startIndex = consequent.loc.start.line - 1;
239
+ const endIndex = consequent.loc.end.line - 1;
240
+ const insideText = scanCommentLinesInRange(lines, startIndex + 1, endIndex);
241
+ if (insideText) {
242
+ return insideText;
243
+ }
244
+ }
245
+ return "";
246
+ }
198
247
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
199
248
  function scanElseIfPrecedingComments(sourceCode, node) {
200
249
  const lines = sourceCode.lines;
@@ -316,8 +365,9 @@ function gatherSwitchCaseCommentText(sourceCode, node) {
316
365
  * @req REQ-COMMENT-ASSOCIATION - Associate inline comments with their corresponding code branches
317
366
  * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
318
367
  * @supports REQ-DUAL-POSITION-DETECTION
368
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
319
369
  */
320
- function gatherBranchCommentText(sourceCode, node, parent) {
370
+ function gatherBranchCommentText(sourceCode, node, parent, annotationPlacement = "before") {
321
371
  /**
322
372
  * Conditional branch for SwitchCase nodes that may include inline comments.
323
373
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
@@ -332,13 +382,21 @@ function gatherBranchCommentText(sourceCode, node, parent) {
332
382
  return gatherCatchClauseCommentText(sourceCode, node, beforeText);
333
383
  }
334
384
  /**
335
- * Conditional branch for IfStatement else-if nodes that may include inline comments
336
- * after the else-if condition but before the consequent body.
385
+ * Conditional branch for IfStatement nodes, distinguishing between else-if branches
386
+ * (which preserve dual-position behavior) and simple if-branches that can honor
387
+ * the configured annotation placement (before or inside braces).
337
388
  * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
389
+ * @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
338
390
  * @supports REQ-DUAL-POSITION-DETECTION
391
+ * @supports REQ-INSIDE-BRACE-PLACEMENT
392
+ * @supports REQ-PLACEMENT-CONFIG
393
+ * @supports REQ-DEFAULT-BACKWARD-COMPAT
339
394
  */
340
395
  if (node.type === "IfStatement") {
341
- return gatherElseIfCommentText(sourceCode, node, parent, beforeText);
396
+ if (isElseIfBranch(node, parent)) {
397
+ return gatherElseIfCommentText(sourceCode, node, parent, beforeText);
398
+ }
399
+ return gatherSimpleIfCommentText(sourceCode, node, annotationPlacement, beforeText);
342
400
  }
343
401
  /**
344
402
  * Conditional branch for loop nodes that may include annotations either on the loop
@@ -5,6 +5,7 @@ import type { Rule } from "eslint";
5
5
  * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
6
6
  * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
7
7
  * @supports REQ-DUAL-POSITION-DETECTION
8
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
8
9
  */
9
10
  export declare function reportMissingAnnotations(context: Rule.RuleContext, node: any, storyFixCountRef: {
10
11
  count: number;
@@ -22,8 +22,11 @@ function getIndentAndInsertPosForLine(sourceCode, line, fallbackIndent) {
22
22
  });
23
23
  return { indent, insertPos };
24
24
  }
25
- /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
26
- function getBaseBranchIndentAndInsertPos(sourceCode, node) {
25
+ /**
26
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
27
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
28
+ */
29
+ function getBaseBranchIndentAndInsertPos(sourceCode, node, _annotationPlacement) {
27
30
  let { indent, insertPos } = getIndentAndInsertPosForLine(sourceCode, node.loc.start.line, "");
28
31
  if (node.type === "CatchClause" && node.body) {
29
32
  const bodyNode = node.body;
@@ -50,32 +53,87 @@ function getBaseBranchIndentAndInsertPos(sourceCode, node) {
50
53
  return { indent, insertPos };
51
54
  }
52
55
  /**
53
- * Compute annotation-related metadata for a branch node.
54
- * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
55
- * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
56
- * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
57
- * @supports REQ-DUAL-POSITION-DETECTION
58
- * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
56
+ * Determine whether a node represents an else-if branch that should be used for
57
+ * determining comment insertion position.
58
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
59
59
  */
60
- function getBranchAnnotationInfo(sourceCode, node, parent) {
61
- const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node, parent);
62
- const hasSupports = /@supports\b/.test(text);
63
- const missingStory = !/@story\b/.test(text) && !hasSupports;
64
- const missingReq = !/@req\b/.test(text) && !hasSupports;
65
- let { indent, insertPos } = getBaseBranchIndentAndInsertPos(sourceCode, node);
66
- if (node.type === "IfStatement" &&
60
+ function isElseIfBranchForInsert(node, parent) {
61
+ return (node &&
62
+ node.type === "IfStatement" &&
67
63
  parent &&
68
64
  parent.type === "IfStatement" &&
69
- parent.alternate === node &&
70
- node.consequent &&
65
+ parent.alternate === node);
66
+ }
67
+ /**
68
+ * Compute indentation and insert position for IfStatement branches, handling
69
+ * both simple if and else-if cases, respecting the configured annotation
70
+ * placement and indentation rules.
71
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
72
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG REQ-INDENTATION-CORRECT
73
+ */
74
+ function getIfStatementIndentAndInsertPos(sourceCode, node, options, context) {
75
+ const { parent, annotationPlacement } = options;
76
+ let { indent, insertPos } = context;
77
+ const hasBlockConsequent = node.consequent &&
71
78
  node.consequent.type === "BlockStatement" &&
72
79
  node.consequent.loc &&
73
- node.consequent.loc.start) {
80
+ node.consequent.loc.start;
81
+ if (!hasBlockConsequent) {
82
+ return context;
83
+ }
84
+ const isElseIf = isElseIfBranchForInsert(node, parent);
85
+ const isSimpleIfInsidePlacement = annotationPlacement === "inside" && !isElseIf;
86
+ if (isSimpleIfInsidePlacement || isElseIf) {
74
87
  const commentLine = node.consequent.loc.start.line + 1;
75
88
  const commentLineInfo = getIndentAndInsertPosForLine(sourceCode, commentLine, indent);
76
89
  indent = commentLineInfo.indent;
77
90
  insertPos = commentLineInfo.insertPos;
91
+ context.indent = indent;
92
+ context.insertPos = insertPos;
78
93
  }
94
+ return context;
95
+ }
96
+ /**
97
+ * Compute which annotations are missing for a branch based on its gathered comment text.
98
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
99
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
100
+ */
101
+ function getBranchMissingFlags(sourceCode, node, parent, annotationPlacement) {
102
+ const text = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, node, parent, annotationPlacement);
103
+ const hasSupports = /@supports\b/.test(text);
104
+ const missingStory = !/@story\b/.test(text) && !hasSupports;
105
+ const missingReq = !/@req\b/.test(text) && !hasSupports;
106
+ return { missingStory, missingReq };
107
+ }
108
+ /**
109
+ * Compute indentation and insert position used for auto-fix insertion on a branch.
110
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
111
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
112
+ */
113
+ function getBranchIndentAndInsertPos(sourceCode, node, parent, annotationPlacement) {
114
+ const { indent, insertPos } = getBaseBranchIndentAndInsertPos(sourceCode, node, annotationPlacement);
115
+ if (node.type === "IfStatement") {
116
+ const context = { indent, insertPos };
117
+ const updatedContext = getIfStatementIndentAndInsertPos(sourceCode, node, { parent, annotationPlacement }, context);
118
+ return {
119
+ indent: updatedContext.indent,
120
+ insertPos: updatedContext.insertPos,
121
+ };
122
+ }
123
+ return { indent, insertPos };
124
+ }
125
+ /**
126
+ * Compute annotation-related metadata for a branch node.
127
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
128
+ * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
129
+ * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
130
+ * @supports REQ-DUAL-POSITION-DETECTION
131
+ * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
132
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
133
+ */
134
+ function getBranchAnnotationInfo(sourceCode, node, parent, annotationPlacement) {
135
+ const { missingStory, missingReq } = getBranchMissingFlags(sourceCode, node, parent, annotationPlacement);
136
+ const { indent, insertPos } = getBranchIndentAndInsertPos(sourceCode, node, parent, annotationPlacement);
79
137
  return { missingStory, missingReq, indent, insertPos };
80
138
  }
81
139
  /**
@@ -84,11 +142,18 @@ function getBranchAnnotationInfo(sourceCode, node, parent) {
84
142
  * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
85
143
  * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
86
144
  * @supports REQ-DUAL-POSITION-DETECTION
145
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
87
146
  */
88
147
  function reportMissingAnnotations(context, node, storyFixCountRef) {
89
148
  const sourceCode = context.getSourceCode();
149
+ const rawOptions = context.options && context.options[0];
150
+ const annotationPlacement = rawOptions &&
151
+ (rawOptions.annotationPlacement === "inside" ||
152
+ rawOptions.annotationPlacement === "before")
153
+ ? rawOptions.annotationPlacement
154
+ : "before";
90
155
  const parent = node.parent;
91
- const { missingStory, missingReq, indent, insertPos } = getBranchAnnotationInfo(sourceCode, node, parent);
156
+ const { missingStory, missingReq, indent, insertPos } = getBranchAnnotationInfo(sourceCode, node, parent, annotationPlacement);
92
157
  const actions = [
93
158
  {
94
159
  missing: missingStory,
@@ -42,6 +42,10 @@ describe("no-redundant-annotation rule (Story 027.0-DEV-REDUNDANT-ANNOTATION-DET
42
42
  name: "[REQ-CATCH-BLOCK-HANDLING] preserves catch block annotation from issue #6 scenario",
43
43
  code: `async function example() {\n try {\n // @story prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.story.md\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n if (isSafeVersion({ version, vulnerabilityData })) {\n return version;\n }\n\n // @story prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.story.md\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n if (!vulnerabilityData.isVulnerable) {\n return version;\n }\n } catch (error) {\n // @story prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.story.md\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n return null;\n }\n}`,
44
44
  },
45
+ {
46
+ name: "[REQ-CATCH-BLOCK-HANDLING] preserves annotations in nested catch blocks with repeated requirements",
47
+ code: `async function nestedCatches() {\n try {\n await checkPrimary();\n } catch (outerError) {\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n try {\n await attemptRecovery(outerError);\n } catch (innerError) {\n // @supports prompts/004.0-DEV-FILTER-VULNERABLE-VERSIONS.md REQ-SAFE-ONLY\n await reportFailure(innerError);\n }\n }\n}`,
48
+ },
45
49
  ],
46
50
  invalid: [
47
51
  {
@@ -92,6 +96,12 @@ describe("no-redundant-annotation rule (Story 027.0-DEV-REDUNDANT-ANNOTATION-DET
92
96
  output: `/**\n * @story docs/stories/009.0-EXAMPLE.story.md\n * @supports REQ-SUP-A, REQ-SUP-B\n */\nfunction example() {\n const supported = checkSupport();\n}`,
93
97
  errors: [{ messageId: "redundantAnnotation" }],
94
98
  },
99
+ {
100
+ name: "[REQ-SCOPE-ANALYSIS][REQ-STATEMENT-SIGNIFICANCE] flags redundant annotation in finally block that repeats try-path coverage",
101
+ code: `async function example() {\n // @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION\n try {\n await doWork();\n } finally {\n // @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION\n await cleanUp();\n }\n}`,
102
+ output: `async function example() {\n // @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION\n try {\n await doWork();\n } finally {\n await cleanUp();\n }\n}`,
103
+ errors: [{ messageId: "redundantAnnotation" }],
104
+ },
95
105
  // TODO: rule implementation exists; full invalid-case behavior tests pending refinement
96
106
  // {
97
107
  // name: "[REQ-SCOPE-ANALYSIS][REQ-STATEMENT-SIGNIFICANCE] flags redundant annotation on simple return inside annotated if",
@@ -8,15 +8,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
8
8
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
9
9
  * @story docs/stories/007.0-DEV-ERROR-REPORTING.story.md
10
10
  * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
11
+ * @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
11
12
  * @req REQ-BRANCH-DETECTION - Verify require-branch-annotation rule enforces branch annotations
12
13
  * @req REQ-ERROR-SPECIFIC - Branch-level missing-annotation error messages are specific and informative
13
14
  * @req REQ-ERROR-CONSISTENCY - Branch-level missing-annotation error messages follow shared conventions
14
15
  * @req REQ-ERROR-SUGGESTION - Branch-level missing-annotation errors include suggestions when applicable
15
16
  * @req REQ-NESTED-HANDLING - Nested branch annotations are correctly enforced without duplicative reporting
16
17
  * @req REQ-SUPPORTS-ALTERNATIVE - Branches annotated only with @supports are treated as fully annotated
18
+ * @req REQ-PLACEMENT-CONFIG - Rule supports configurable annotation placement modes
19
+ * @req REQ-DEFAULT-BACKWARD-COMPAT - Default placement remains backward compatible with existing behavior
17
20
  * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-BRANCH-DETECTION REQ-NESTED-HANDLING REQ-SUPPORTS-ALTERNATIVE
18
21
  * @supports docs/stories/007.0-DEV-ERROR-REPORTING.story.md REQ-ERROR-SPECIFIC REQ-ERROR-CONSISTENCY REQ-ERROR-SUGGESTION
19
22
  * @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION-ELSE-IF REQ-FALLBACK-LOGIC-ELSE-IF REQ-POSITION-PRIORITY-ELSE-IF REQ-PRETTIER-AUTOFIX-ELSE-IF
23
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG REQ-DEFAULT-BACKWARD-COMPAT
20
24
  */
21
25
  const eslint_1 = require("eslint");
22
26
  const require_branch_annotation_1 = __importDefault(require("../../src/rules/require-branch-annotation"));
@@ -184,6 +188,22 @@ if (outer) {
184
188
  if (condition) {}`,
185
189
  options: [{ branchTypes: ["IfStatement", "SwitchCase"] }],
186
190
  },
191
+ {
192
+ name: "[REQ-PLACEMENT-CONFIG][REQ-DEFAULT-BACKWARD-COMPAT] if-statement with before-brace annotations using annotationPlacement: 'before'",
193
+ code: `// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
194
+ // @req REQ-PLACEMENT-CONFIG
195
+ if (condition) {}`,
196
+ options: [{ annotationPlacement: "before" }],
197
+ },
198
+ {
199
+ name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-PLACEMENT-CONFIG] if-statement annotated inside block under annotationPlacement: 'inside' (Story 028.0)",
200
+ code: `if (condition) {
201
+ // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
202
+ // @req REQ-INSIDE-BRACE-PLACEMENT
203
+ doSomething();
204
+ }`,
205
+ options: [{ annotationPlacement: "inside" }],
206
+ },
187
207
  {
188
208
  name: "[REQ-SUPPORTS-ALTERNATIVE] if-statement with only @supports annotation is treated as fully annotated",
189
209
  code: `// @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
@@ -404,6 +424,22 @@ if (a) {
404
424
  }`,
405
425
  errors: makeMissingAnnotationErrors("@story", "@req", "@story", "@req"),
406
426
  },
427
+ {
428
+ name: "[REQ-INSIDE-BRACE-PLACEMENT][REQ-BEFORE-BRACE-ERROR][REQ-PLACEMENT-CONFIG] before-brace annotations ignored when annotationPlacement: 'inside'",
429
+ code: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
430
+ // @req REQ-BEFORE-BRACE-ERROR
431
+ if (condition) {
432
+ doSomething();
433
+ }`,
434
+ options: [{ annotationPlacement: "inside" }],
435
+ output: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
436
+ // @req REQ-BEFORE-BRACE-ERROR
437
+ if (condition) {
438
+ // @story <story-file>.story.md
439
+ doSomething();
440
+ }`,
441
+ errors: makeMissingAnnotationErrors("@story", "@req"),
442
+ },
407
443
  ],
408
444
  });
409
445
  runRule({
@@ -111,3 +111,55 @@ describe("validateBranchTypes helper (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () =
111
111
  expect(loopText).toBe("@story loop branch story loop details");
112
112
  });
113
113
  });
114
+ /**
115
+ * Tests for annotationPlacement wiring at helper level
116
+ * @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
117
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
118
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-DEFAULT-BACKWARD-COMPAT
119
+ */
120
+ describe("gatherBranchCommentText annotationPlacement wiring (Story 028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION)", () => {
121
+ it("[REQ-PLACEMENT-CONFIG][REQ-DEFAULT-BACKWARD-COMPAT] honors configured placement for simple if-statements", () => {
122
+ const sourceCode = {
123
+ lines: [
124
+ "function demo() {",
125
+ " if (condition) {",
126
+ " // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md",
127
+ " // @req REQ-INSIDE",
128
+ " doSomething();",
129
+ " }",
130
+ "}",
131
+ ],
132
+ getCommentsBefore: jest
133
+ .fn()
134
+ .mockReturnValue([
135
+ { value: "@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md" },
136
+ { value: "@req REQ-BEFORE" },
137
+ ]),
138
+ };
139
+ const ifNode = {
140
+ type: "IfStatement",
141
+ loc: {
142
+ start: { line: 2, column: 2 },
143
+ end: { line: 5, column: 3 },
144
+ },
145
+ consequent: {
146
+ type: "BlockStatement",
147
+ loc: {
148
+ start: { line: 2, column: 18 },
149
+ end: { line: 5, column: 3 },
150
+ },
151
+ },
152
+ };
153
+ const parent = {
154
+ type: "BlockStatement",
155
+ body: [ifNode],
156
+ };
157
+ const beforeText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, ifNode, parent, "before");
158
+ expect(beforeText).toContain("@story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md");
159
+ expect(beforeText).toContain("@req REQ-BEFORE");
160
+ const insideText = (0, branch_annotation_helpers_1.gatherBranchCommentText)(sourceCode, ifNode, parent, "inside");
161
+ expect(insideText).toContain("@story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md");
162
+ expect(insideText).toContain("@req REQ-INSIDE");
163
+ expect(insideText).not.toContain("@req REQ-BEFORE");
164
+ });
165
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.17.1",
3
+ "version": "1.19.0",
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",
@@ -290,7 +290,7 @@ The rule accepts an optional configuration object:
290
290
  - `requireDescribeStory` (boolean, optional) – When `true` (default), requires that each top-level `describe` block include a story reference somewhere in its description text (for example, a path such as `docs/stories/010.0-PAYMENTS.story.md` or a shorter project-specific alias that your team uses consistently).
291
291
  - `requireTestReqPrefix` (boolean, optional) – When `true` (default), requires each `it`/`test` block name to begin with a requirement identifier in square brackets, such as `[REQ-PAYMENTS-REFUND]`. The exact `REQ-` pattern is shared with the `valid-annotation-format` rule’s requirement ID checks.
292
292
  - `describePattern` (string, optional) – A JavaScript regular expression **source** (without leading and trailing `/`) that the `describe` description text must match when `requireDescribeStory` is enabled. This lets you enforce a project-specific format such as requiring a canonical story path or a `STORY-` style identifier in the `describe` string. If omitted, the default is equivalent to `"Story [0-9]+\\.[0-9]+-"`, which expects the description to include a story label such as `"Story 021.0-DEV-TEST-TRACEABILITY"`. You can override this to instead require full story paths or whatever story-labeling convention your project prefers.
293
- - `autoFixTestTemplate` (boolean, optional) – When `true` (default), allows the rule’s `--fix` mode to insert a file-level `@supports` placeholder template at the top of test files that are missing it. The template is intentionally non-semantic and includes TODO-style guidance so humans can later replace it with a real story path and requirement IDs; disabling this option prevents the rule from inserting the template automatically.
293
+ - `autoFixTestTemplate` (boolean, optional) – When `true` (default), allows the rule’s `--fix` mode to insert a file-level `@supports` placeholder template at the top of test files that are missing it. The template is intentionally non-semantic and includes TODO-style guidance so humans can later replace it with a real story and requirement IDs; disabling this option prevents the rule from inserting the template automatically.
294
294
  - `autoFixTestPrefixFormat` (boolean, optional) – When `true` (default), enables safe normalization of malformed `[REQ-XXX]` prefixes in `it`/`test` names during `--fix`. The rule only rewrites prefixes that already contain a recognizable requirement identifier and limits changes to formatting concerns (spacing, square brackets vs. parentheses, underscore and dash usage, and letter casing) without fabricating new IDs or guessing requirement names.
295
295
  - `testSupportsTemplate` (string, optional) – Overrides the default file-level `@supports` placeholder template used when `autoFixTestTemplate` is enabled. This string should be a complete JSDoc-style block (for example, including `/**`, `*`, and `*/`) that encodes your project’s preferred TODO guidance or placeholder story path; it is inserted verbatim at the top of matching test files that lack a `@supports` annotation, and is never interpreted or expanded by the rule.
296
296
 
@@ -327,6 +327,8 @@ describe("Refunds flow docs/stories/010.0-PAYMENTS.story.md", () => {
327
327
 
328
328
  Description: Detects and optionally removes **redundant** traceability annotations on code that is already covered by an enclosing annotated scope. It focuses on simple, statement-level constructs—such as `return` statements, basic variable declarations, and other leaf statements—where repeating the same `@story` / `@req` / `@supports` information adds noise without improving coverage. When run with `--fix`, the rule offers safe auto-fixes that remove only the redundant comments while preserving all annotations that are required to maintain correct traceability.
329
329
 
330
+ Catch blocks are treated as separate execution paths for traceability purposes, and annotations inside a `catch` block that intentionally repeat the requirements of the corresponding `try` path are not considered redundant and are never auto-removed. This behavior was introduced as part of story `027.0-DEV-REDUNDANT-ANNOTATION-DETECTION (Detect and Remove Redundant Annotations)` to avoid false positives on error-handling paths that implement the same requirement as the success path.
331
+
330
332
  The rule is designed to complement the core presence and validation rules: it never treats removing a redundant annotation as valid if doing so would leave the underlying requirement or story **uncovered** according to the plugin’s normal rules. It only targets comments whose traceability content is already implied by a surrounding function, method, or branch annotation.
331
333
 
332
334
  Options:
@@ -344,6 +346,9 @@ The rule accepts an optional configuration object:
344
346
  Behavior notes:
345
347
 
346
348
  - The rule only inspects comments that contain recognized traceability annotations (`@story`, `@req`, `@supports`) and are attached to simple statements (returns, expression statements, variable declarations, and similar leaf nodes). It intentionally does **not** attempt to de-duplicate annotations on functions, classes, or major branches, which remain the responsibility of the core rules. When a statement has multiple redundant traceability comments (for example, a small comment block that repeats both @story and @req lines), the rule reports a **single** diagnostic for that statement and, in fix mode, removes all of the redundant annotation comments associated with it in a single grouped fix.
349
+ - **Catch blocks and error-handling paths**
350
+ - The rule never treats annotations inside a `catch` block as redundant, even when they repeat exactly the same `(story, requirement)` pairs that already cover the surrounding `try` statement or other enclosing branches.
351
+ - This preserves explicit traceability for error-handling logic, in line with the requirements captured in story `027.0-DEV-REDUNDANT-ANNOTATION-DETECTION`, and avoids collapsing success-path and failure-path coverage into a single, less precise annotation.
347
352
  - Auto-fix removes only the redundant traceability lines (and any now-empty comment delimiters when safe) while preserving surrounding non-traceability text in the same comment where possible.
348
353
  - When no enclosing scope with compatible coverage is found within `maxScopeDepth`, the annotation is not considered redundant and is left unchanged.
349
354
 
@@ -187,3 +187,35 @@ Depending on your Prettier version and configuration, the exact layout of the `e
187
187
  - For most branch types, `traceability/require-branch-annotation` associates comments immediately before the branch keyword (such as `if`, `else`, `switch`, `case`) with that branch. Branches can be annotated either with a single `@supports` line (preferred), or with the older `@story`/`@req` pair for backward compatibility. The rule treats a valid `@supports` annotation as satisfying both the story and requirement presence checks.
188
188
  - For `catch` clauses and `else if` branches, the rule is formatter-aware and also looks at comments between the condition and the block, as well as the first comment-only lines inside the block body, so you do not need to fight Prettier if it moves your annotations.
189
189
  - When annotations exist in more than one place around an `else if` branch, the rule prefers comments immediately before the `else if` line, then comments between the condition and the block, and finally comments inside the block body, matching the behavior described in the API reference and stories `025.0` and `026.0`.
190
+
191
+ ## 7. Redundant annotations and catch blocks
192
+
193
+ When `traceability/no-redundant-annotation` is enabled (for example, via the recommended preset), `catch` blocks are always treated as distinct execution paths. Repeating the same `(story, requirement)` pair in a `catch` block as in the corresponding `try` path is not considered redundant and will be preserved. This behavior is part of the improvements captured in story `027.0-DEV-REDUNDANT-ANNOTATION-DETECTION`.
194
+
195
+ In the following example, running ESLint with `--fix` will not remove the `@supports` annotation on the `catch` block, even though it repeats the requirement from the `try` path, because it represents separate error-handling coverage.
196
+
197
+ ```js
198
+ function filterSafeVersions(allVersions) {
199
+ try {
200
+ const stable = allVersions.filter((v) => !v.includes("-beta"));
201
+
202
+ // @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
203
+ if (stable.length > 0) {
204
+ return stable;
205
+ }
206
+ // @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
207
+ else if (allVersions.length > 0) {
208
+ // Fall back to all versions if we have no clearly stable ones
209
+ return allVersions;
210
+ }
211
+
212
+ // Fallback when the input list is empty
213
+ return [];
214
+ } catch (error) {
215
+ // @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
216
+ // traceability/no-redundant-annotation keeps this as separate error-handling coverage,
217
+ // even though it repeats the requirement from the try path.
218
+ return [];
219
+ }
220
+ }
221
+ ```
@@ -123,7 +123,7 @@ A typical migration path is:
123
123
  - Enable it as `"warn"` to get non-breaking guidance and auto-fixes for straightforward cases.
124
124
  - Optionally move to `"error"` once you want to strictly enforce `@supports` usage for all JSDoc blocks that are eligible for safe conversion.
125
125
 
126
- #### When to keep `@story` + `@req`
126
+ #### When to keep `@story` + `req`
127
127
 
128
128
  Keep your current annotations if:
129
129
 
@@ -229,6 +229,8 @@ In all cases, the rule is conservative:
229
229
 
230
230
  The rule operates over both `@supports` and legacy `@story`/`@req` style annotations, so it continues to work even in mixed codebases during a long-running migration.
231
231
 
232
+ In addition, `catch` blocks are treated as distinct execution paths: repeating the same `(story, requirement)` pair in a `catch` block is **not** considered redundant, because the error-handling path is typically validated and reasoned about separately from the main control flow.
233
+
232
234
  A simplified example, using an illustrative story path that represents a file in **your** documentation tree:
233
235
 
234
236
  Before (redundant duplication inside a branch):
@@ -271,6 +273,38 @@ if (cart.items.length === 0) {
271
273
  }
272
274
  ```
273
275
 
276
+ #### Example: try/if/else-if/catch with non-redundant catch annotation
277
+
278
+ The following example shows a `try` block with an `if` / `else if` chain that validates a safe operation, and a `catch` block that handles the error path for the **same** requirement. Both paths are annotated with the same `(story, requirement)` pair to make it clear that the requirement covers normal execution and error handling:
279
+
280
+ ```js
281
+ async function performSafeOperation(input) {
282
+ try {
283
+ // @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
284
+ if (input == null) {
285
+ throw new Error("Missing input");
286
+ }
287
+
288
+ // @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
289
+ if (typeof input === "string") {
290
+ return await doSafeStringOperation(input);
291
+ } else if (Array.isArray(input)) {
292
+ return await doSafeArrayOperation(input);
293
+ }
294
+
295
+ return await doSafeFallbackOperation(input);
296
+ } catch (error) {
297
+ // This catch represents the error-handling path for the same safe-operation requirement.
298
+ // Even though the coverage matches the try/if/else-if chain above, it is *not* redundant:
299
+ // it documents how failures are handled for the same requirement.
300
+ // @supports docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION
301
+ return handleSafeOperationFailure(error, input);
302
+ }
303
+ }
304
+ ```
305
+
306
+ Here, `traceability/no-redundant-annotation` recognizes the `catch` block as a separate execution path from the main `try` body. The annotation in the `catch` remains intact and is **not** treated as redundant, even though it repeats the same `docs/stories/010.0-EXAMPLE.story.md REQ-SAFE-OPERATION` coverage as the guarded `if` / `else if` chain in the `try`. This behavior was introduced and validated as part of story `027.0-DEV-REDUNDANT-ANNOTATION-DETECTION (Detect and Remove Redundant Annotations)` to prevent regressions in real-world `try/if/else-if/catch` scenarios like the one discussed there.
307
+
274
308
  #### Safe migration workflow
275
309
 
276
310
  To use `traceability/no-redundant-annotation` safely during your v1.x migration: