eslint-plugin-traceability 1.19.3 → 1.19.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,9 +1,9 @@
1
- ## [1.19.3](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.19.2...v1.19.3) (2025-12-18)
1
+ ## [1.19.4](https://github.com/voder-ai/eslint-plugin-traceability/compare/v1.19.3...v1.19.4) (2025-12-18)
2
2
 
3
3
 
4
4
  ### Bug Fixes
5
5
 
6
- * support inside placement for switch cases in branch helpers ([3fd08d1](https://github.com/voder-ai/eslint-plugin-traceability/commit/3fd08d1314085a7aa9894f6248dbb7f42a07bda0))
6
+ * migrate before-brace annotations into inside-brace placement ([c8c381d](https://github.com/voder-ai/eslint-plugin-traceability/commit/c8c381d5f5fbd39fdb626e9dd6fe2dd837bffb53))
7
7
 
8
8
  # Changelog
9
9
 
package/README.md CHANGED
@@ -89,6 +89,47 @@ export default [
89
89
  ];
90
90
  ```
91
91
 
92
+ ### Annotation Placement
93
+
94
+ Traceability annotations are typically placed immediately adjacent to the code they index. The plugin exposes explicit placement options for branch-level rules and a stable, conventional placement for function-level rules.
95
+
96
+ - **Branch-level (`traceability/require-branch-annotation`)**
97
+
98
+ `require-branch-annotation` supports an `annotationPlacement` option:
99
+ - `"before"` – Annotation appears **immediately before** the branch statement (default).
100
+ - `"inside"` – Annotation appears as the **first comment-only lines inside** the branch block.
101
+
102
+ In `"inside"` mode, the rule expects the annotation to be the first meaningful content inside blocks for `if` / `else` / loops / `try` / `catch` / `finally` / `switch` cases.
103
+
104
+ Example (`if` statement):
105
+
106
+ ```js
107
+ // annotationPlacement: "before"
108
+ // @supports docs/stories/auth.md REQ-AUTH-VALIDATION
109
+ if (isValidUser(user)) {
110
+ performLogin(user);
111
+ }
112
+
113
+ // annotationPlacement: "inside"
114
+ if (isValidUser(user)) {
115
+ // @supports docs/stories/auth.md REQ-AUTH-VALIDATION
116
+ performLogin(user);
117
+ }
118
+ ```
119
+
120
+ - **Function-level (`traceability/require-story-annotation`, `traceability/require-req-annotation`)**
121
+
122
+ Function-level rules continue to accept annotations:
123
+ - As JSDoc blocks immediately preceding the function, or
124
+ - As line comments placed directly before the function declaration or expression.
125
+
126
+ This placement is stable and supported for all current versions. Future versions may introduce an **inside-brace** placement mode for function bodies (similar to branch blocks) to align function annotations with the branch-level `"inside"` standard.
127
+
128
+ For full configuration details and migration guidance between placement styles, see:
129
+
130
+ - `traceability/require-branch-annotation` rule docs: [docs/rules/require-branch-annotation.md](docs/rules/require-branch-annotation.md)
131
+ - Migration guide: [user-docs/migration-guide.md](user-docs/migration-guide.md)
132
+
92
133
  ### Available Rules
93
134
 
94
135
  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.
@@ -53,6 +53,8 @@ export declare function reportMissingStory(context: Rule.RuleContext, node: any,
53
53
  storyFixCountRef: {
54
54
  count: number;
55
55
  };
56
+ annotationPlacement: AnnotationPlacement;
57
+ sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>;
56
58
  }): void;
57
59
  /**
58
60
  * Report missing @req annotation tag on a branch node when that branch has no linked requirement identifier in its associated comments.
@@ -11,6 +11,7 @@ Object.defineProperty(exports, "reportMissingAnnotations", { enumerable: true, g
11
11
  const branch_annotation_loop_helpers_1 = require("./branch-annotation-loop-helpers");
12
12
  const branch_annotation_if_helpers_1 = require("./branch-annotation-if-helpers");
13
13
  const branch_annotation_switch_helpers_1 = require("./branch-annotation-switch-helpers");
14
+ const branch_annotation_story_fix_helpers_1 = require("./branch-annotation-story-fix-helpers");
14
15
  /**
15
16
  * Valid branch types for require-branch-annotation rule.
16
17
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
@@ -402,21 +403,20 @@ function gatherBranchCommentText(sourceCode, node, parent, annotationPlacement =
402
403
  * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
403
404
  */
404
405
  function reportMissingStory(context, node, options) {
405
- const { indent, insertPos, storyFixCountRef } = options;
406
+ const { indent, insertPos, storyFixCountRef, annotationPlacement, sourceCode, } = options;
406
407
  /**
407
408
  * Conditional branch deciding whether to offer an auto-fix for the missing story.
408
409
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
409
410
  * @req REQ-TRACEABILITY-FIX-DECISION - Trace decision to provide fixer for missing @story
410
411
  */
411
412
  if (storyFixCountRef.count === 0) {
412
- /**
413
- * Fixer that inserts a default @story tag above the branch.
414
- * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
415
- * @req REQ-TRACEABILITY-FIX-ARROW - Trace fixer function used to insert missing @story
416
- */
417
- function insertStoryFixer(fixer) {
418
- return fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @story <story-file>.story.md\n`);
419
- }
413
+ const insertStoryFixer = (0, branch_annotation_story_fix_helpers_1.createStoryFixer)({
414
+ annotationPlacement,
415
+ sourceCode,
416
+ node,
417
+ insertPos,
418
+ indent,
419
+ });
420
420
  context.report({
421
421
  node,
422
422
  messageId: "missingAnnotation",
@@ -0,0 +1,52 @@
1
+ import type { Rule } from "eslint";
2
+ import type { AnnotationPlacement } from "./branch-annotation-helpers";
3
+ /**
4
+ * Shared helpers for computing inside-brace indentation and insert positions
5
+ * for branch nodes used by require-branch-annotation. This module isolates
6
+ * the inside-placement logic so that the main report helpers stay small and
7
+ * within ESLint's max-lines-per-function limits.
8
+ *
9
+ * @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
10
+ * @supports REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG REQ-INDENTATION-CORRECT
11
+ */
12
+ type SourceCode = ReturnType<Rule.RuleContext["getSourceCode"]>;
13
+ type IndentHelperContext = {
14
+ getInsideBlockIndentAndInsertPos: (_sourceCode: SourceCode, _blockNode: any, _baseFallbackIndent: string) => {
15
+ indent: string;
16
+ insertPos: number;
17
+ };
18
+ getIndentAndInsertPosForLine: (_sourceCode: SourceCode, _line: number, _fallbackIndent: string) => {
19
+ indent: string;
20
+ insertPos: number;
21
+ };
22
+ };
23
+ type BranchIndentOptions = {
24
+ sourceCode: SourceCode;
25
+ node: any;
26
+ indent: string;
27
+ };
28
+ /**
29
+ * Inside-placement helper used by getBaseBranchIndentAndInsertPos to select the
30
+ * correct inside-placement strategy for the base branch.
31
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG
32
+ */
33
+ export declare function computeInsideBaseIndentAndInsertPos(options: {
34
+ sourceCode: SourceCode;
35
+ node: any;
36
+ annotationPlacement: AnnotationPlacement;
37
+ currentIndent: string;
38
+ }, context: IndentHelperContext): {
39
+ indent: string;
40
+ insertPos: number;
41
+ } | null;
42
+ /**
43
+ * Apply inside-placement overrides for non-if branches (switch, try, loops, catch).
44
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG
45
+ */
46
+ export declare function applyInsidePlacementOverridesForBranch(options: BranchIndentOptions & {
47
+ annotationPlacement: AnnotationPlacement;
48
+ }, context: IndentHelperContext): {
49
+ indent: string;
50
+ insertPos: number;
51
+ } | null;
52
+ export {};
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeInsideBaseIndentAndInsertPos = computeInsideBaseIndentAndInsertPos;
4
+ exports.applyInsidePlacementOverridesForBranch = applyInsidePlacementOverridesForBranch;
5
+ function isLoopNode(node) {
6
+ return (node.type === "ForStatement" ||
7
+ node.type === "ForInStatement" ||
8
+ node.type === "ForOfStatement" ||
9
+ node.type === "WhileStatement" ||
10
+ node.type === "DoWhileStatement");
11
+ }
12
+ function computeInsideCatchIndentAndInsertPos(sourceCode, node, currentIndent, context) {
13
+ if (!(node.type === "CatchClause" && node.body)) {
14
+ return null;
15
+ }
16
+ const bodyNode = node.body;
17
+ if (!bodyNode.loc || !bodyNode.loc.start) {
18
+ return null;
19
+ }
20
+ return context.getInsideBlockIndentAndInsertPos(sourceCode, bodyNode, currentIndent);
21
+ }
22
+ function computeInsideLoopIndentAndInsertPos(options, context) {
23
+ const { sourceCode, node, indent } = options;
24
+ if (!isLoopNode(node) ||
25
+ !node.body ||
26
+ node.body.type !== "BlockStatement" ||
27
+ !node.body.loc ||
28
+ !node.body.loc.start) {
29
+ return null;
30
+ }
31
+ return context.getInsideBlockIndentAndInsertPos(sourceCode, node.body, indent);
32
+ }
33
+ function computeInsideTryOrSwitchIndentAndInsertPos(sourceCode, node, currentIndent, context) {
34
+ if (!((node.type === "TryStatement" || node.type === "SwitchCase") &&
35
+ node.consequent &&
36
+ Array.isArray(node.consequent) &&
37
+ node.consequent.length > 0)) {
38
+ return null;
39
+ }
40
+ const firstStatement = node.consequent[0];
41
+ if (!firstStatement || !firstStatement.loc || !firstStatement.loc.start) {
42
+ return null;
43
+ }
44
+ const commentLineInfo = context.getIndentAndInsertPosForLine(sourceCode, firstStatement.loc.start.line, currentIndent);
45
+ return {
46
+ indent: commentLineInfo.indent,
47
+ insertPos: commentLineInfo.insertPos,
48
+ };
49
+ }
50
+ function computeInsideTryBlockIndentAndInsertPos(options, context) {
51
+ const { sourceCode, node, indent } = options;
52
+ if (!(node.type === "TryStatement" &&
53
+ node.block &&
54
+ node.block.type === "BlockStatement" &&
55
+ node.block.loc &&
56
+ node.block.loc.start)) {
57
+ return null;
58
+ }
59
+ return context.getInsideBlockIndentAndInsertPos(sourceCode, node.block, indent);
60
+ }
61
+ function computeInsideSwitchCaseIndentAndInsertPos(options, context) {
62
+ const { sourceCode, node, indent } = options;
63
+ if (!(node.type === "SwitchCase" &&
64
+ node.consequent &&
65
+ Array.isArray(node.consequent) &&
66
+ node.consequent.length > 0)) {
67
+ return null;
68
+ }
69
+ const firstStatement = node.consequent[0];
70
+ if (!firstStatement || !firstStatement.loc || !firstStatement.loc.start) {
71
+ return null;
72
+ }
73
+ // Prefer line-based helper for consistency with other callers.
74
+ const commentLineInfo = context.getIndentAndInsertPosForLine(sourceCode, firstStatement.loc.start.line, indent);
75
+ return {
76
+ indent: commentLineInfo.indent,
77
+ insertPos: commentLineInfo.insertPos,
78
+ };
79
+ }
80
+ function computeInsideCatchBlockIndentAndInsertPos(options, context) {
81
+ const { sourceCode, node, indent } = options;
82
+ if (!(node.type === "CatchClause" &&
83
+ node.body &&
84
+ node.body.type === "BlockStatement" &&
85
+ node.body.loc &&
86
+ node.body.loc.start)) {
87
+ return null;
88
+ }
89
+ return context.getInsideBlockIndentAndInsertPos(sourceCode, node.body, indent);
90
+ }
91
+ /**
92
+ * Inside-placement helper used by getBaseBranchIndentAndInsertPos to select the
93
+ * correct inside-placement strategy for the base branch.
94
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG
95
+ */
96
+ function computeInsideBaseIndentAndInsertPos(options, context) {
97
+ const { sourceCode, node, annotationPlacement, currentIndent } = options;
98
+ if (annotationPlacement !== "inside") {
99
+ return null;
100
+ }
101
+ const catchInside = computeInsideCatchIndentAndInsertPos(sourceCode, node, currentIndent, context);
102
+ if (catchInside) {
103
+ return catchInside;
104
+ }
105
+ const loopInside = computeInsideLoopIndentAndInsertPos({ sourceCode, node, indent: currentIndent }, context);
106
+ if (loopInside) {
107
+ return loopInside;
108
+ }
109
+ const tryOrSwitchInside = computeInsideTryOrSwitchIndentAndInsertPos(sourceCode, node, currentIndent, context);
110
+ if (tryOrSwitchInside) {
111
+ return tryOrSwitchInside;
112
+ }
113
+ return null;
114
+ }
115
+ /**
116
+ * Apply inside-placement overrides for non-if branches (switch, try, loops, catch).
117
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-INSIDE-BRACE-PLACEMENT REQ-PLACEMENT-CONFIG
118
+ */
119
+ function applyInsidePlacementOverridesForBranch(options, context) {
120
+ const { annotationPlacement } = options;
121
+ if (annotationPlacement !== "inside") {
122
+ return null;
123
+ }
124
+ const calculators = [
125
+ computeInsideSwitchCaseIndentAndInsertPos,
126
+ computeInsideTryBlockIndentAndInsertPos,
127
+ computeInsideLoopIndentAndInsertPos,
128
+ computeInsideCatchBlockIndentAndInsertPos,
129
+ ];
130
+ for (const calculator of calculators) {
131
+ const result = calculator(options, context);
132
+ if (result) {
133
+ return result;
134
+ }
135
+ }
136
+ return null;
137
+ }
@@ -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/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
8
9
  * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
9
10
  */
10
11
  export declare function reportMissingAnnotations(context: Rule.RuleContext, node: any, storyFixCountRef: {
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.reportMissingAnnotations = reportMissingAnnotations;
4
4
  const branch_annotation_helpers_1 = require("./branch-annotation-helpers");
5
+ const branch_annotation_indent_helpers_1 = require("./branch-annotation-indent-helpers");
5
6
  /**
6
7
  * Compute indentation and insert position for the start of a given 1-based line
7
8
  * number. This keeps indentation and fixer insert positions consistent across
@@ -23,11 +24,43 @@ function getIndentAndInsertPosForLine(sourceCode, line, fallbackIndent) {
23
24
  return { indent, insertPos };
24
25
  }
25
26
  /**
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
27
+ * Compute indentation and insert position for the first "inner" line of a
28
+ * BlockStatement, used for inside-brace insertion.
29
+ * Falls back to the block's own line with one extra indent step if it has no
30
+ * body statements.
28
31
  */
29
- function getBaseBranchIndentAndInsertPos(sourceCode, node, _annotationPlacement) {
30
- let { indent, insertPos } = getIndentAndInsertPosForLine(sourceCode, node.loc.start.line, "");
32
+ function getInsideBlockIndentAndInsertPos(sourceCode, blockNode, baseFallbackIndent) {
33
+ let indent = baseFallbackIndent;
34
+ let insertPos = sourceCode.getIndexFromLoc({
35
+ line: blockNode.loc.start.line,
36
+ column: 0,
37
+ });
38
+ const bodyStatements = Array.isArray(blockNode.body)
39
+ ? blockNode.body
40
+ : undefined;
41
+ const firstStatement = bodyStatements && bodyStatements.length > 0 ? bodyStatements[0] : undefined;
42
+ if (firstStatement && firstStatement.loc && firstStatement.loc.start) {
43
+ const firstLine = firstStatement.loc.start.line;
44
+ const firstLineInfo = getIndentAndInsertPosForLine(sourceCode, firstLine, baseFallbackIndent);
45
+ indent = firstLineInfo.indent;
46
+ insertPos = firstLineInfo.insertPos;
47
+ }
48
+ else if (blockNode.loc && blockNode.loc.start) {
49
+ const blockLine = blockNode.loc.start.line;
50
+ const blockLineInfo = getIndentAndInsertPosForLine(sourceCode, blockLine, baseFallbackIndent);
51
+ const innerIndent = `${blockLineInfo.indent} `;
52
+ indent = innerIndent;
53
+ insertPos = blockLineInfo.insertPos;
54
+ }
55
+ return { indent, insertPos };
56
+ }
57
+ /**
58
+ * Apply the base catch-clause indentation/insert-position fallback used by
59
+ * getBaseBranchIndentAndInsertPos when no inside-placement override is applied.
60
+ */
61
+ function applyCatchClauseBaseIndentFallback(sourceCode, node, currentIndent, currentInsertPos) {
62
+ let indent = currentIndent;
63
+ let insertPos = currentInsertPos;
31
64
  if (node.type === "CatchClause" && node.body) {
32
65
  const bodyNode = node.body;
33
66
  const bodyStatements = Array.isArray(bodyNode.body)
@@ -52,6 +85,27 @@ function getBaseBranchIndentAndInsertPos(sourceCode, node, _annotationPlacement)
52
85
  }
53
86
  return { indent, insertPos };
54
87
  }
88
+ /**
89
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
90
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
91
+ */
92
+ function getBaseBranchIndentAndInsertPos(sourceCode, node, annotationPlacement) {
93
+ let { indent, insertPos } = getIndentAndInsertPosForLine(sourceCode, node.loc.start.line, "");
94
+ const indentHelpers = {
95
+ getInsideBlockIndentAndInsertPos,
96
+ getIndentAndInsertPosForLine,
97
+ };
98
+ const insideBase = (0, branch_annotation_indent_helpers_1.computeInsideBaseIndentAndInsertPos)({
99
+ sourceCode,
100
+ node,
101
+ annotationPlacement,
102
+ currentIndent: indent,
103
+ }, indentHelpers);
104
+ if (insideBase) {
105
+ return insideBase;
106
+ }
107
+ return applyCatchClauseBaseIndentFallback(sourceCode, node, indent, insertPos);
108
+ }
55
109
  /**
56
110
  * Determine whether a node represents an else-if branch that should be used for
57
111
  * determining comment insertion position.
@@ -112,7 +166,8 @@ function getBranchMissingFlags(sourceCode, node, parent, annotationPlacement) {
112
166
  * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
113
167
  */
114
168
  function getBranchIndentAndInsertPos(sourceCode, node, parent, annotationPlacement) {
115
- const { indent, insertPos } = getBaseBranchIndentAndInsertPos(sourceCode, node, annotationPlacement);
169
+ const base = getBaseBranchIndentAndInsertPos(sourceCode, node, annotationPlacement);
170
+ let { indent, insertPos } = base;
116
171
  if (node.type === "IfStatement") {
117
172
  const context = { indent, insertPos };
118
173
  const updatedContext = getIfStatementIndentAndInsertPos(sourceCode, node, { parent, annotationPlacement }, context);
@@ -121,6 +176,19 @@ function getBranchIndentAndInsertPos(sourceCode, node, parent, annotationPlaceme
121
176
  insertPos: updatedContext.insertPos,
122
177
  };
123
178
  }
179
+ const indentHelpers = {
180
+ getInsideBlockIndentAndInsertPos,
181
+ getIndentAndInsertPosForLine,
182
+ };
183
+ const insideOverride = (0, branch_annotation_indent_helpers_1.applyInsidePlacementOverridesForBranch)({
184
+ sourceCode,
185
+ node,
186
+ indent,
187
+ annotationPlacement,
188
+ }, indentHelpers);
189
+ if (insideOverride) {
190
+ return insideOverride;
191
+ }
124
192
  return { indent, insertPos };
125
193
  }
126
194
  /**
@@ -137,12 +205,22 @@ function getBranchAnnotationInfo(sourceCode, node, parent, annotationPlacement)
137
205
  const { indent, insertPos } = getBranchIndentAndInsertPos(sourceCode, node, parent, annotationPlacement);
138
206
  return { missingStory, missingReq, indent, insertPos };
139
207
  }
208
+ function processMissingAnnotationActions(context, node, actions) {
209
+ /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
210
+ function processAction(item) {
211
+ if (item.missing) {
212
+ item.fn(...item.args);
213
+ }
214
+ }
215
+ actions.forEach(processAction);
216
+ }
140
217
  /**
141
218
  * Report missing annotations on a branch node.
142
219
  * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
143
220
  * @req REQ-ANNOTATION-PARSING - Parse @story and @req annotations from branch comments
144
221
  * @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
145
222
  * @supports REQ-DUAL-POSITION-DETECTION
223
+ * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-SUPPORTS-ALTERNATIVE
146
224
  * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG
147
225
  */
148
226
  function reportMissingAnnotations(context, node, storyFixCountRef) {
@@ -159,7 +237,17 @@ function reportMissingAnnotations(context, node, storyFixCountRef) {
159
237
  {
160
238
  missing: missingStory,
161
239
  fn: branch_annotation_helpers_1.reportMissingStory,
162
- args: [context, node, { indent, insertPos, storyFixCountRef }],
240
+ args: [
241
+ context,
242
+ node,
243
+ {
244
+ indent,
245
+ insertPos,
246
+ storyFixCountRef,
247
+ annotationPlacement,
248
+ sourceCode,
249
+ },
250
+ ],
163
251
  },
164
252
  {
165
253
  missing: missingReq,
@@ -167,11 +255,5 @@ function reportMissingAnnotations(context, node, storyFixCountRef) {
167
255
  args: [context, node, { indent, insertPos, missingStory }],
168
256
  },
169
257
  ];
170
- /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
171
- function processAction(item) {
172
- if (item.missing) {
173
- item.fn(...item.args);
174
- }
175
- }
176
- actions.forEach(processAction);
258
+ processMissingAnnotationActions(context, node, actions);
177
259
  }
@@ -0,0 +1,27 @@
1
+ import type { Rule } from "eslint";
2
+ import type { AnnotationPlacement } from "./branch-annotation-helpers";
3
+ /**
4
+ * Context object for building story-fixers used by require-branch-annotation.
5
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
6
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-PLACEMENT-CONFIG REQ-INSIDE-BRACE-PLACEMENT
7
+ */
8
+ export interface StoryFixContext {
9
+ annotationPlacement: AnnotationPlacement;
10
+ sourceCode: ReturnType<Rule.RuleContext["getSourceCode"]>;
11
+ node: any;
12
+ insertPos: number;
13
+ indent: string;
14
+ }
15
+ /**
16
+ * Create a fixer function that inserts or migrates a @story comment for a
17
+ * missing branch annotation, honoring the configured placement mode.
18
+ * When annotationPlacement is "inside", this helper uses
19
+ * buildInsidePlacementStoryFixes to migrate existing before-branch
20
+ * annotations into the standardized inside-brace location. Otherwise, it
21
+ * preserves the original "before" behavior of inserting directly above
22
+ * the branch.
23
+ *
24
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
25
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-AUTO-FIX-MIGRATION REQ-INDENTATION-CORRECT
26
+ */
27
+ export declare function createStoryFixer(ctx: StoryFixContext): (fixer: any) => any;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createStoryFixer = createStoryFixer;
4
+ /**
5
+ * Build the individual fixes needed when migrating existing before-branch
6
+ * annotations into inside-brace placement. This helper is responsible for
7
+ * removing redundant before-branch comments that already contain
8
+ * traceability tags and inserting the canonical placeholder inside the
9
+ * branch body at the computed insertion position.
10
+ *
11
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
12
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-AUTO-FIX-MIGRATION REQ-INSIDE-BRACE-PLACEMENT
13
+ */
14
+ function buildInsidePlacementStoryFixes(ctx, fixer) {
15
+ const { sourceCode, node, insertPos, indent } = ctx;
16
+ const fixes = [];
17
+ const beforeComments = sourceCode.getCommentsBefore(node) || [];
18
+ const removableComments = beforeComments.filter((c) => /@story\b/.test(c.value) ||
19
+ /@req\b/.test(c.value) ||
20
+ /@supports\b/.test(c.value));
21
+ removableComments.forEach((comment) => {
22
+ fixes.push(fixer.remove(comment));
23
+ });
24
+ fixes.push(fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @story <story-file>.story.md\n`));
25
+ return fixes;
26
+ }
27
+ /**
28
+ * Create a fixer function that inserts or migrates a @story comment for a
29
+ * missing branch annotation, honoring the configured placement mode.
30
+ * When annotationPlacement is "inside", this helper uses
31
+ * buildInsidePlacementStoryFixes to migrate existing before-branch
32
+ * annotations into the standardized inside-brace location. Otherwise, it
33
+ * preserves the original "before" behavior of inserting directly above
34
+ * the branch.
35
+ *
36
+ * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
37
+ * @supports docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md REQ-AUTO-FIX-MIGRATION REQ-INDENTATION-CORRECT
38
+ */
39
+ function createStoryFixer(ctx) {
40
+ const { annotationPlacement, insertPos, indent } = ctx;
41
+ function insertStoryFixer(fixer) {
42
+ if (annotationPlacement === "inside") {
43
+ return buildInsidePlacementStoryFixes(ctx, fixer);
44
+ }
45
+ return fixer.insertTextBeforeRange([insertPos, insertPos], `${indent}// @story <story-file>.story.md\n`);
46
+ }
47
+ return insertStoryFixer;
48
+ }
@@ -473,16 +473,7 @@ try {
473
473
  handleError(error);
474
474
  }`,
475
475
  options: [{ annotationPlacement: "inside" }],
476
- output: `// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
477
- // @req REQ-BRANCH-TRY
478
- // @story <story-file>.story.md
479
- try {
480
- doSomething();
481
- } catch (error) {
482
- // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
483
- // @req REQ-INSIDE-CATCH
484
- handleError(error);
485
- }`,
476
+ output: "\n\ntry {\n // @story <story-file>.story.md\n doSomething();\n} catch (error) {\n // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md\n // @req REQ-INSIDE-CATCH\n handleError(error);\n}",
486
477
  errors: makeMissingAnnotationErrors("@story", "@req"),
487
478
  },
488
479
  {
@@ -493,12 +484,7 @@ if (condition) {
493
484
  doSomething();
494
485
  }`,
495
486
  options: [{ annotationPlacement: "inside" }],
496
- output: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
497
- // @req REQ-BEFORE-BRACE-ERROR
498
- if (condition) {
499
- // @story <story-file>.story.md
500
- doSomething();
501
- }`,
487
+ output: "\n\nif (condition) {\n // @story <story-file>.story.md\n doSomething();\n}",
502
488
  errors: makeMissingAnnotationErrors("@story", "@req"),
503
489
  },
504
490
  {
@@ -509,12 +495,7 @@ for (const item of items) {
509
495
  process(item);
510
496
  }`,
511
497
  options: [{ annotationPlacement: "inside" }],
512
- output: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
513
- // @req REQ-LOOP-BEFORE
514
- // @story <story-file>.story.md
515
- for (const item of items) {
516
- process(item);
517
- }`,
498
+ output: "\n\nfor (const item of items) {\n // @story <story-file>.story.md\n process(item);\n}",
518
499
  errors: makeMissingAnnotationErrors("@story", "@req"),
519
500
  },
520
501
  {
@@ -530,17 +511,7 @@ catch (error) {
530
511
  handleError(error);
531
512
  }`,
532
513
  options: [{ annotationPlacement: "inside" }],
533
- output: `// @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
534
- // @req REQ-BRANCH-TRY
535
- // @story <story-file>.story.md
536
- try {
537
- doSomething();
538
- }
539
- // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
540
- // @req REQ-CATCH-BEFORE
541
- catch (error) {
542
- handleError(error);
543
- }`,
514
+ output: "\n\ntry {\n // @story <story-file>.story.md\n doSomething();\n}\n// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md\n// @req REQ-CATCH-BEFORE\ncatch (error) {\n handleError(error);\n}",
544
515
  errors: makeMissingAnnotationErrors("@story", "@req", "@story", "@req"),
545
516
  },
546
517
  {
@@ -553,14 +524,7 @@ try {
553
524
  cleanup();
554
525
  }`,
555
526
  options: [{ annotationPlacement: "inside" }],
556
- output: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
557
- // @req REQ-TRY-BEFORE
558
- // @story <story-file>.story.md
559
- try {
560
- doWork();
561
- } finally {
562
- cleanup();
563
- }`,
527
+ output: "\n\ntry {\n // @story <story-file>.story.md\n doWork();\n} finally {\n cleanup();\n}",
564
528
  errors: makeMissingAnnotationErrors("@story", "@req"),
565
529
  },
566
530
  {
@@ -576,17 +540,7 @@ else if (b) {
576
540
  doB();
577
541
  }`,
578
542
  options: [{ annotationPlacement: "inside" }],
579
- output: `if (a) {
580
- // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
581
- // @req REQ-OUTER-IF-INSIDE
582
- doA();
583
- }
584
- // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
585
- // @req REQ-ELSE-IF-BEFORE
586
- else if (b) {
587
- // @story <story-file>.story.md
588
- doB();
589
- }`,
543
+ output: "if (a) {\n // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md\n // @req REQ-OUTER-IF-INSIDE\n doA();\n}\n// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md\n// @req REQ-ELSE-IF-BEFORE\nelse if (b) {\n // @story <story-file>.story.md\n doB();\n}",
590
544
  errors: makeMissingAnnotationErrors("@story", "@req"),
591
545
  },
592
546
  {
@@ -603,18 +557,7 @@ if (a) {
603
557
  doC();
604
558
  }`,
605
559
  options: [{ annotationPlacement: "inside" }],
606
- output: `// @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
607
- // @req REQ-INSIDE-OUTER-IF
608
- if (a) {
609
- // @story <story-file>.story.md
610
- doA();
611
- } else if (b) {
612
- // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
613
- // @req REQ-INSIDE-ELSE-IF
614
- doB();
615
- } else {
616
- doC();
617
- }`,
560
+ output: "\n\nif (a) {\n // @story <story-file>.story.md\n doA();\n} else if (b) {\n // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md\n // @req REQ-INSIDE-ELSE-IF\n doB();\n} else {\n doC();\n}",
618
561
  errors: makeMissingAnnotationErrors("@story", "@req"),
619
562
  },
620
563
  {
@@ -627,14 +570,7 @@ if (a) {
627
570
  }
628
571
  }`,
629
572
  options: [{ annotationPlacement: "inside" }],
630
- output: `switch (value) {
631
- // @story docs/stories/028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION.story.md
632
- // @req REQ-SWITCH-BEFORE
633
- // @story <story-file>.story.md
634
- case 'a': {
635
- doSomething();
636
- }
637
- }`,
573
+ output: "switch (value) {\n \n \n // @story <story-file>.story.md\n case 'a': {\n doSomething();\n }\n}",
638
574
  errors: makeMissingAnnotationErrors("@story", "@req"),
639
575
  },
640
576
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.19.3",
3
+ "version": "1.19.4",
4
4
  "description": "A customizable ESLint plugin that enforces traceability annotations in your code, ensuring each implementation is linked to its requirement or test case.",
5
5
  "main": "lib/src/index.js",
6
6
  "types": "lib/src/index.d.ts",
@@ -811,4 +811,5 @@ If `--from` or `--to` is missing, the CLI prints an error, shows the help text,
811
811
  In CI:
812
812
 
813
813
  ```bash
814
- npm run traceability:verify
814
+ npm run traceability:verify
815
+ ```
@@ -354,6 +354,46 @@ The full API reference documents all options, but the most important knobs for m
354
354
 
355
355
  For most teams, the defaults in the recommended preset are a good starting point; you can then tune these options incrementally as your traceability style and `@supports` usage stabilize.
356
356
 
357
+ ### 3.4 Inside-brace branch annotation placement (optional)
358
+
359
+ Story 028.0-DEV-ANNOTATION-PLACEMENT-STANDARDIZATION introduces an **inside-brace** placement standard for branch annotations. Instead of placing annotations directly above a branch, you can configure `traceability/require-branch-annotation` to look for annotations as the first comment-only lines **inside** each block body.
360
+
361
+ The feature is controlled by the `annotationPlacement` option on `require-branch-annotation`:
362
+
363
+ ```js
364
+ // eslint.config.js (flat config example)
365
+ import traceability from "eslint-plugin-traceability";
366
+
367
+ export default [
368
+ traceability.configs.recommended,
369
+ {
370
+ rules: {
371
+ "traceability/require-branch-annotation": [
372
+ "error",
373
+ {
374
+ annotationPlacement: "inside", // "before" (default) or "inside"
375
+ },
376
+ ],
377
+ },
378
+ },
379
+ ];
380
+ ```
381
+
382
+ With `annotationPlacement: "inside"`, the rule expects annotations in these locations:
383
+
384
+ - `if` / `else if` / `else`: first comment-only lines inside the `{ ... }` block.
385
+ - Loops: first comment-only lines inside the loop body.
386
+ - `try` / `catch` / `finally`: first comment-only lines inside the corresponding block body.
387
+ - `switch` cases: first comment-only lines inside the `case` body when it is a block (`case 'a': { ... }`).
388
+
389
+ Before-brace annotations are still honored when you leave `annotationPlacement` at the default value (`"before"`), so you can migrate gradually:
390
+
391
+ 1. **Start in default mode** — keep `annotationPlacement` unspecified (or set to `"before"`) and continue using your existing `// @story` / `// @req` comments above branches.
392
+ 2. **Introduce inside-brace style for new code** — when adding or refactoring branches, place annotations on the first comment-only line inside the block body. This layout plays nicely with Prettier and is what the rule’s auto-fix uses for `if`/`else if` and similar branches.
393
+ 3. **Opt-in to `annotationPlacement: "inside"`** — once your codebase is mostly using inside-brace annotations, enable the option. Branches that still rely only on before-brace comments will be reported as missing annotations in inside mode, and the rule’s autofix can insert placeholders at the correct inside location to help you complete the migration.
394
+
395
+ The default configuration in 1.x keeps `annotationPlacement` at `"before"` for backward compatibility, so existing projects do not need to change anything unless they want the new inside-brace behavior.
396
+
357
397
  ## 4. Test and Validate
358
398
 
359
399
  Run your test suite to confirm everything passes: