eslint-plugin-traceability 1.12.0 → 1.13.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.
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EXPECTED_RANGE_LENGTH = void 0;
4
+ exports.toStoryReqKey = toStoryReqKey;
5
+ exports.extractStoryReqPairsFromText = extractStoryReqPairsFromText;
6
+ exports.extractStoryReqPairsFromComments = extractStoryReqPairsFromComments;
7
+ exports.arePairsFullyCovered = arePairsFullyCovered;
8
+ exports.isStatementEligibleForRedundancy = isStatementEligibleForRedundancy;
9
+ exports.getCommentRemovalRange = getCommentRemovalRange;
10
+ /**
11
+ * Shared types and helpers for redundant-annotation detection.
12
+ *
13
+ * These utilities focus on parsing traceability annotations from comment
14
+ * text and computing relationships between "scope" coverage and
15
+ * statement-level annotations. They are intentionally small, pure
16
+ * functions so that the ESLint rule can delegate most of its logic
17
+ * here while keeping its own create/visitor code shallow.
18
+ *
19
+ * @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SCOPE-ANALYSIS REQ-DUPLICATION-DETECTION
20
+ */
21
+ exports.EXPECTED_RANGE_LENGTH = 2; // [start, end]
22
+ /**
23
+ * Build a canonical key for a story/requirement pair.
24
+ *
25
+ * Empty story or requirement components are normalized to the empty
26
+ * string so that comparisons remain stable even when some annotations
27
+ * omit one side (for example, malformed or story-less @req lines).
28
+ *
29
+ * @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-DUPLICATION-DETECTION REQ-DIFFERENT-REQUIREMENTS
30
+ */
31
+ function toStoryReqKey(storyPath, reqId) {
32
+ const story = storyPath ?? "";
33
+ const req = reqId ?? "";
34
+ return `${story}|${req}`;
35
+ }
36
+ /**
37
+ * Extract story/requirement pairs from a snippet of comment text.
38
+ *
39
+ * Supported patterns:
40
+ * - `@story <path>` followed by one or more `@req <ID>` lines.
41
+ * - `@supports <path> <REQ-ID-1> <REQ-ID-2> ...` where each `REQ-*`
42
+ * token is treated as a separate pair bound to the same story path.
43
+ *
44
+ * The parser is intentionally conservative: it only creates pairs when
45
+ * it can confidently associate a requirement identifier with a story
46
+ * path. This avoids false positives in REQ-DIFFERENT-REQUIREMENTS by
47
+ * ensuring we never conflate different requirement IDs.
48
+ *
49
+ * @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SCOPE-ANALYSIS REQ-DUPLICATION-DETECTION REQ-DIFFERENT-REQUIREMENTS
50
+ */
51
+ function extractStoryReqPairsFromText(text) {
52
+ const pairs = new Set();
53
+ if (!text)
54
+ return pairs;
55
+ const lines = text.split(/\r?\n/);
56
+ let currentStory = null;
57
+ for (const rawLine of lines) {
58
+ const line = rawLine.trim();
59
+ if (!line)
60
+ continue;
61
+ // @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SCOPE-ANALYSIS
62
+ const storyMatch = line.match(/@story\s+(\S+)/);
63
+ if (storyMatch) {
64
+ currentStory = storyMatch[1];
65
+ }
66
+ // Handle explicit @req lines that follow the most recent @story.
67
+ const reqMatch = line.match(/@req\s+(\S+)/);
68
+ if (reqMatch && currentStory) {
69
+ pairs.add(toStoryReqKey(currentStory, reqMatch[1]));
70
+ }
71
+ // Handle consolidated @supports lines that encode both story and
72
+ // requirement identifiers on a single line.
73
+ const supportsMatch = line.match(/@supports\s+(\S+)\s+(.+)/);
74
+ if (supportsMatch) {
75
+ const storyPath = supportsMatch[1];
76
+ const tail = supportsMatch[2];
77
+ const tokens = tail
78
+ .split(/\s+/)
79
+ .filter((t) => /^REQ-[A-Z0-9-]+$/.test(t));
80
+ for (const reqId of tokens) {
81
+ pairs.add(toStoryReqKey(storyPath, reqId));
82
+ }
83
+ }
84
+ }
85
+ return pairs;
86
+ }
87
+ /**
88
+ * Extract story/requirement pairs from a list of ESLint comment nodes.
89
+ *
90
+ * @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SCOPE-ANALYSIS REQ-DUPLICATION-DETECTION
91
+ */
92
+ function extractStoryReqPairsFromComments(comments) {
93
+ const pairs = new Set();
94
+ if (!Array.isArray(comments) || comments.length === 0) {
95
+ return pairs;
96
+ }
97
+ const combinedText = comments
98
+ .filter((comment) => comment && typeof comment.value === "string")
99
+ .map((comment) => comment.value)
100
+ .join("\n");
101
+ const fromComments = extractStoryReqPairsFromText(combinedText);
102
+ for (const key of fromComments) {
103
+ pairs.add(key);
104
+ }
105
+ return pairs;
106
+ }
107
+ /**
108
+ * Determine whether all story/requirement pairs in `child` are already
109
+ * covered by `parent`.
110
+ *
111
+ * This implements the core notion of redundancy: if a statement-level
112
+ * annotation only repeats the exact same story+requirement pairs that
113
+ * are already declared on its containing scope, it does not add any
114
+ * new traceability information.
115
+ *
116
+ * @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-DUPLICATION-DETECTION REQ-DIFFERENT-REQUIREMENTS REQ-SCOPE-INHERITANCE
117
+ */
118
+ function arePairsFullyCovered(child, parent) {
119
+ if (child.size === 0)
120
+ return false;
121
+ if (parent.size === 0)
122
+ return false;
123
+ for (const key of child) {
124
+ if (!parent.has(key)) {
125
+ return false;
126
+ }
127
+ }
128
+ return true;
129
+ }
130
+ /**
131
+ * Decide whether a given statement node type should be considered
132
+ * "simple" or "significant" for redundancy detection, based on the
133
+ * configured strictness and alwaysCovered lists.
134
+ *
135
+ * - In `strict` mode, all non-branch statements are eligible.
136
+ * - In `moderate` mode (default), only statement types listed in
137
+ * `alwaysCovered` plus bare expression statements are treated as
138
+ * candidates for redundancy.
139
+ * - In `permissive` mode, only `alwaysCovered` types are considered.
140
+ *
141
+ * This keeps REQ-STATEMENT-SIGNIFICANCE and REQ-CONFIGURABLE-STRICTNESS
142
+ * aligned with the story's configuration model.
143
+ *
144
+ * @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-STATEMENT-SIGNIFICANCE REQ-CONFIGURABLE-STRICTNESS
145
+ */
146
+ function isStatementEligibleForRedundancy(node, options, branchTypes) {
147
+ if (!node || typeof node.type !== "string") {
148
+ return false;
149
+ }
150
+ // Never treat branch nodes themselves as "simple" statements; their
151
+ // annotations are typically intentional and should be preserved.
152
+ if (branchTypes.includes(node.type)) {
153
+ return false;
154
+ }
155
+ const alwaysCoveredSet = new Set(options.alwaysCovered);
156
+ if (alwaysCoveredSet.has(node.type)) {
157
+ return true;
158
+ }
159
+ if (options.strictness === "permissive") {
160
+ return false;
161
+ }
162
+ if (options.strictness === "moderate") {
163
+ // Treat side-effecting expression statements (e.g. assignments or
164
+ // simple calls) as eligible while still excluding more complex
165
+ // control-flow constructs.
166
+ return node.type === "ExpressionStatement";
167
+ }
168
+ // strict: any non-branch statement may be considered.
169
+ return true;
170
+ }
171
+ /**
172
+ * Compute the character range that should be removed when auto-fixing a
173
+ * redundant annotation comment.
174
+ *
175
+ * The implementation is conservative to satisfy REQ-SAFE-REMOVAL:
176
+ *
177
+ * - When the comment occupies its own line (only whitespace before the
178
+ * comment token), the removal range is expanded to include that
179
+ * leading whitespace and the trailing newline, so the entire line is
180
+ * removed.
181
+ * - When there is other code before the comment on the same line, only
182
+ * the comment text itself is removed, leaving surrounding code and
183
+ * whitespace intact.
184
+ *
185
+ * @supports docs/stories/027.0-DEV-REDUNDANT-ANNOTATION-DETECTION.story.md REQ-SAFE-REMOVAL
186
+ */
187
+ function getCommentRemovalRange(comment, sourceCode) {
188
+ const fullText = sourceCode.getText();
189
+ const range = comment && comment.range;
190
+ if (!Array.isArray(range) || range.length !== exports.EXPECTED_RANGE_LENGTH) {
191
+ return [0, 0];
192
+ }
193
+ let [start, end] = range;
194
+ // Find the start of the current line.
195
+ let lineStart = start;
196
+ while (lineStart > 0) {
197
+ const ch = fullText.charAt(lineStart - 1);
198
+ if (ch === "\n" || ch === "\r")
199
+ break;
200
+ lineStart -= 1;
201
+ }
202
+ const leadingText = fullText.slice(lineStart, start);
203
+ const onlyWhitespaceBeforeComment = leadingText.trim().length === 0;
204
+ let removalStart = start;
205
+ let removalEnd = end;
206
+ if (onlyWhitespaceBeforeComment) {
207
+ removalStart = lineStart;
208
+ }
209
+ // Expand to consume trailing whitespace after the comment.
210
+ while (removalEnd < fullText.length) {
211
+ const ch = fullText.charAt(removalEnd);
212
+ if (ch === " " || ch === " ") {
213
+ removalEnd += 1;
214
+ }
215
+ else {
216
+ break;
217
+ }
218
+ }
219
+ // Optionally include the newline when the comment owns the line.
220
+ if (onlyWhitespaceBeforeComment && removalEnd < fullText.length) {
221
+ const ch = fullText.charAt(removalEnd);
222
+ if (ch === "\r") {
223
+ removalEnd += 1;
224
+ if (fullText.charAt(removalEnd) === "\n") {
225
+ removalEnd += 1;
226
+ }
227
+ }
228
+ else if (ch === "\n") {
229
+ removalEnd += 1;
230
+ }
231
+ }
232
+ return [removalStart, removalEnd];
233
+ }
@@ -87,24 +87,69 @@ function extractCommentValue(_c) {
87
87
  return _c.value;
88
88
  }
89
89
  /**
90
- * Collect a single contiguous comment line at the given index, appending its
91
- * trimmed text to the accumulator. Returns true when a valid comment was
92
- * collected and false when scanning should stop (blank or non-comment line).
90
+ * Extract trimmed comment text for a given source line index or return null
91
+ * when the line is blank or not a comment. This helper centralizes the
92
+ * formatter-aware rules used by branch helpers when scanning for contiguous
93
+ * comment lines around branches.
93
94
  * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-COMMENT-ASSOCIATION
94
95
  * @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION REQ-FALLBACK-LOGIC
95
96
  * @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION-ELSE-IF REQ-FALLBACK-LOGIC-ELSE-IF
96
97
  */
97
- function collectCommentLine(lines, index, comments) {
98
+ function getCommentTextAtLine(lines, index) {
98
99
  const line = lines[index];
99
100
  if (!line || !line.trim()) {
100
- return false;
101
+ return null;
101
102
  }
102
103
  if (!/^\s*(\/\/|\/\*)/.test(line)) {
104
+ return null;
105
+ }
106
+ return line.trim();
107
+ }
108
+ /**
109
+ * Collect a single contiguous comment line at the given index, appending its
110
+ * trimmed text to the accumulator. Returns true when a valid comment was
111
+ * collected and false when scanning should stop (blank or non-comment line).
112
+ * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-COMMENT-ASSOCIATION
113
+ * @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION REQ-FALLBACK-LOGIC
114
+ * @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-DUAL-POSITION-DETECTION-ELSE-IF REQ-FALLBACK-LOGIC-ELSE-IF
115
+ */
116
+ function collectCommentLine(lines, index, comments) {
117
+ const commentText = getCommentTextAtLine(lines, index);
118
+ if (!commentText) {
103
119
  return false;
104
120
  }
105
- comments.push(line.trim());
121
+ comments.push(commentText);
106
122
  return true;
107
123
  }
124
+ /**
125
+ * Scan contiguous formatter-aware comment lines between the provided 0-based
126
+ * start and end indices (inclusive), stopping when a non-comment or blank line
127
+ * is encountered. This helper is used as a line-based fallback when
128
+ * structured comment APIs are not available for branch bodies.
129
+ * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-COMMENT-ASSOCIATION
130
+ * @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-FALLBACK-LOGIC
131
+ * @supports docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md REQ-FALLBACK-LOGIC-ELSE-IF
132
+ */
133
+ function scanCommentLinesInRange(lines, startIndex, endIndexInclusive) {
134
+ if (!Array.isArray(lines) || lines.length === 0) {
135
+ return "";
136
+ }
137
+ if (startIndex < 0 ||
138
+ startIndex >= lines.length ||
139
+ startIndex > endIndexInclusive) {
140
+ return "";
141
+ }
142
+ const comments = [];
143
+ const lastIndex = Math.min(endIndexInclusive, lines.length - 1);
144
+ let i = startIndex;
145
+ while (i <= lastIndex) {
146
+ if (!collectCommentLine(lines, i, comments)) {
147
+ break;
148
+ }
149
+ i++;
150
+ }
151
+ return comments.join(" ");
152
+ }
108
153
  function isElseIfBranch(node, parent) {
109
154
  return (node &&
110
155
  node.type === "IfStatement" &&
@@ -139,15 +184,7 @@ function gatherCatchClauseCommentText(sourceCode, node, beforeText) {
139
184
  const lines = sourceCode.lines;
140
185
  const startIndex = node.body.loc.start.line - 1;
141
186
  const endIndex = node.body.loc.end.line - 1;
142
- const comments = [];
143
- let i = startIndex + 1;
144
- while (i <= endIndex) {
145
- if (!collectCommentLine(lines, i, comments)) {
146
- break;
147
- }
148
- i++;
149
- }
150
- const insideText = comments.join(" ");
187
+ const insideText = scanCommentLinesInRange(lines, startIndex + 1, endIndex);
151
188
  if (insideText) {
152
189
  return insideText;
153
190
  }
@@ -165,14 +202,11 @@ function scanElseIfPrecedingComments(sourceCode, node) {
165
202
  let i = startLine - 1;
166
203
  let scanned = 0;
167
204
  while (i >= 0 && scanned < PRE_COMMENT_OFFSET) {
168
- const line = lines[i];
169
- if (!line || !line.trim()) {
205
+ const commentText = getCommentTextAtLine(lines, i);
206
+ if (!commentText) {
170
207
  break;
171
208
  }
172
- if (!/^\s*(\/\/|\/\*)/.test(line)) {
173
- break;
174
- }
175
- comments.unshift(line.trim());
209
+ comments.unshift(commentText);
176
210
  i--;
177
211
  scanned++;
178
212
  }
@@ -194,13 +228,16 @@ function scanElseIfBetweenConditionAndBody(sourceCode, node) {
194
228
  const lines = sourceCode.lines;
195
229
  const conditionEndLine = node.test.loc.end.line;
196
230
  const consequentStartLine = node.consequent.loc.start.line;
197
- const comments = [];
198
- for (let lineIndex = conditionEndLine; lineIndex < consequentStartLine - 1; lineIndex++) {
199
- if (!collectCommentLine(lines, lineIndex, comments)) {
200
- break;
201
- }
231
+ // Lines in sourceCode are 0-based indexes, but loc.line values are 1-based.
232
+ // We want to scan comments strictly between the condition and the
233
+ // consequent body, so we start at the line after the condition's end and
234
+ // stop at the line immediately before the consequent's starting line.
235
+ const startIndex = conditionEndLine; // already the next logical line index when 0-based
236
+ const endIndexExclusive = consequentStartLine - 1;
237
+ if (endIndexExclusive <= startIndex) {
238
+ return "";
202
239
  }
203
- return comments.join(" ");
240
+ return scanCommentLinesInRange(lines, startIndex, endIndexExclusive - 1);
204
241
  }
205
242
  /** @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md */
206
243
  function scanElseIfInsideBlockComments(sourceCode, node) {
@@ -228,7 +265,10 @@ function scanElseIfInsideBlockComments(sourceCode, node) {
228
265
  * @supports REQ-FALLBACK-LOGIC
229
266
  */
230
267
  function gatherElseIfCommentText(sourceCode, node, parent, beforeText) {
231
- if (/@story\b/.test(beforeText) || /@req\b/.test(beforeText)) {
268
+ if (beforeText &&
269
+ (/@story\b/.test(beforeText) ||
270
+ /@req\b/.test(beforeText) ||
271
+ /@supports\b/.test(beforeText))) {
232
272
  return beforeText;
233
273
  }
234
274
  if (!isElseIfBranch(node, parent)) {
@@ -236,7 +276,9 @@ function gatherElseIfCommentText(sourceCode, node, parent, beforeText) {
236
276
  }
237
277
  const beforeElseText = scanElseIfPrecedingComments(sourceCode, node);
238
278
  if (beforeElseText &&
239
- (/@story\b/.test(beforeElseText) || /@req\b/.test(beforeElseText))) {
279
+ (/@story\b/.test(beforeElseText) ||
280
+ /@req\b/.test(beforeElseText) ||
281
+ /@supports\b/.test(beforeElseText))) {
240
282
  return beforeElseText;
241
283
  }
242
284
  if (!hasValidElseIfBlockLoc(node)) {
@@ -375,12 +417,28 @@ function reportMissingReq(context, node, options) {
375
417
  * @supports REQ-ANNOTATION-PARSING
376
418
  * @supports REQ-DUAL-POSITION-DETECTION
377
419
  */
378
- function getBaseBranchIndentAndInsertPos(sourceCode, node) {
379
- let indent = sourceCode.lines[node.loc.start.line - 1].match(/^(\s*)/)?.[1] || "";
380
- let insertPos = sourceCode.getIndexFromLoc({
381
- line: node.loc.start.line,
420
+ /**
421
+ * Compute indentation and insert position for the start of a given 1-based line
422
+ * number. This keeps indentation and fixer insert positions consistent across
423
+ * branch helpers that need to align auto-inserted comments with existing
424
+ * source formatting.
425
+ * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-ANNOTATION-PARSING
426
+ */
427
+ function getIndentAndInsertPosForLine(sourceCode, line, fallbackIndent) {
428
+ const lines = sourceCode.lines;
429
+ let indent = fallbackIndent;
430
+ if (line >= 1 && line <= lines.length) {
431
+ const rawLine = lines[line - 1];
432
+ indent = rawLine.match(/^(\s*)/)?.[1] || fallbackIndent;
433
+ }
434
+ const insertPos = sourceCode.getIndexFromLoc({
435
+ line,
382
436
  column: 0,
383
437
  });
438
+ return { indent, insertPos };
439
+ }
440
+ function getBaseBranchIndentAndInsertPos(sourceCode, node) {
441
+ let { indent, insertPos } = getIndentAndInsertPosForLine(sourceCode, node.loc.start.line, "");
384
442
  if (node.type === "CatchClause" && node.body) {
385
443
  const bodyNode = node.body;
386
444
  const bodyStatements = Array.isArray(bodyNode.body)
@@ -391,22 +449,16 @@ function getBaseBranchIndentAndInsertPos(sourceCode, node) {
391
449
  : undefined;
392
450
  if (firstStatement && firstStatement.loc && firstStatement.loc.start) {
393
451
  const firstLine = firstStatement.loc.start.line;
394
- const innerIndent = sourceCode.lines[firstLine - 1].match(/^(\s*)/)?.[1] || "";
395
- indent = innerIndent;
396
- insertPos = sourceCode.getIndexFromLoc({
397
- line: firstLine,
398
- column: 0,
399
- });
452
+ const firstLineInfo = getIndentAndInsertPosForLine(sourceCode, firstLine, "");
453
+ indent = firstLineInfo.indent;
454
+ insertPos = firstLineInfo.insertPos;
400
455
  }
401
456
  else if (bodyNode.loc && bodyNode.loc.start) {
402
457
  const blockLine = bodyNode.loc.start.line;
403
- const blockIndent = sourceCode.lines[blockLine - 1].match(/^(\s*)/)?.[1] || "";
404
- const innerIndent = `${blockIndent} `;
458
+ const blockLineInfo = getIndentAndInsertPosForLine(sourceCode, blockLine, "");
459
+ const innerIndent = `${blockLineInfo.indent} `;
405
460
  indent = innerIndent;
406
- insertPos = sourceCode.getIndexFromLoc({
407
- line: blockLine,
408
- column: 0,
409
- });
461
+ insertPos = blockLineInfo.insertPos;
410
462
  }
411
463
  }
412
464
  return { indent, insertPos };
@@ -433,12 +485,9 @@ function getBranchAnnotationInfo(sourceCode, node, parent) {
433
485
  // For else-if blocks, align auto-fix comments with Prettier's tendency to place comments
434
486
  // inside the wrapped block body; non-block consequents intentionally keep the default behavior.
435
487
  const commentLine = node.consequent.loc.start.line + 1;
436
- const commentIndent = sourceCode.lines[commentLine - 1]?.match(/^(\s*)/)?.[1] || indent;
437
- indent = commentIndent;
438
- insertPos = sourceCode.getIndexFromLoc({
439
- line: commentLine,
440
- column: 0,
441
- });
488
+ const commentLineInfo = getIndentAndInsertPosForLine(sourceCode, commentLine, indent);
489
+ indent = commentLineInfo.indent;
490
+ insertPos = commentLineInfo.insertPos;
442
491
  }
443
492
  return { missingStory, missingReq, indent, insertPos };
444
493
  }
@@ -35,10 +35,12 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  /**
37
37
  * Dogfooding validation integration tests
38
- * @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-TEST REQ-DOGFOODING-CI
38
+ * @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-TEST REQ-DOGFOODING-CI REQ-DOGFOODING-VERIFY REQ-DOGFOODING-PRESET
39
39
  */
40
40
  const path = __importStar(require("path"));
41
41
  const child_process_1 = require("child_process");
42
+ const use_at_your_own_risk_1 = require("eslint/use-at-your-own-risk");
43
+ const index_1 = __importStar(require("../../src/index"));
42
44
  /**
43
45
  * @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-TEST
44
46
  */
@@ -91,4 +93,34 @@ describe("Dogfooding Validation (Story 023.0-MAINT-DOGFOODING-VALIDATION)", () =
91
93
  expect(result.stdout).toContain("error");
92
94
  expect(result.stdout).toContain("src/dogfood.ts");
93
95
  });
96
+ it("[REQ-DOGFOODING-VERIFY] should report at least one traceability rule active for TS sources", () => {
97
+ /**
98
+ * @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-VERIFY
99
+ */
100
+ const eslintConfig = require("../../eslint.config.js");
101
+ const tsConfig = getTsConfigFromEslintConfig(eslintConfig);
102
+ expect(tsConfig).toBeDefined();
103
+ const rules = tsConfig.rules || {};
104
+ const hasTraceabilityRule = Object.keys(rules).some((key) => key.startsWith("traceability/"));
105
+ expect(hasTraceabilityRule).toBe(true);
106
+ });
107
+ it("[REQ-DOGFOODING-PRESET] should be compatible with recommended preset usage without throwing", async () => {
108
+ /**
109
+ * @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-PRESET
110
+ */
111
+ const config = [
112
+ { plugins: { traceability: index_1.default }, rules: {} },
113
+ ...index_1.configs.recommended,
114
+ ];
115
+ const eslint = new use_at_your_own_risk_1.FlatESLint({
116
+ overrideConfig: config,
117
+ overrideConfigFile: true,
118
+ ignore: false,
119
+ });
120
+ const results = await eslint.lintText("function foo() {}", {
121
+ filePath: "example.ts",
122
+ });
123
+ expect(results.length).toBeGreaterThanOrEqual(1);
124
+ expect(Array.isArray(results[0].messages)).toBe(true);
125
+ });
94
126
  });
@@ -50,9 +50,8 @@ describe("Else-if annotations with Prettier (Story 026.0-DEV-ELSE-IF-ANNOTATION-
50
50
  }
51
51
  return result.stdout;
52
52
  }
53
- if (process.env.TRACEABILITY_EXPERIMENTAL_ELSE_IF === "1") {
54
- it("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-BEFORE] accepts code where annotations start before else-if but are moved between condition and body by Prettier", () => {
55
- const original = `
53
+ it("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-BEFORE] accepts code where annotations start before else-if but are moved between condition and body by Prettier", () => {
54
+ const original = `
56
55
  function doA() {
57
56
  return 1;
58
57
  }
@@ -72,16 +71,16 @@ else if (anotherVeryLongConditionThatForcesWrapping && someOtherCondition) {
72
71
  doB();
73
72
  }
74
73
  `;
75
- const formatted = formatWithPrettier(original);
76
- // Sanity checks: Prettier should keep both the else-if branch and the associated story annotation,
77
- // but the exact layout and comment movement may vary between versions.
78
- expect(formatted).toContain("else if");
79
- expect(formatted).toContain("@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md");
80
- const result = runEslintWithRequireBranchAnnotation(formatted);
81
- expect(result.status).toBe(0);
82
- });
83
- it("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-INSIDE] accepts code where annotations start between condition and body and are preserved by Prettier", () => {
84
- const original = `
74
+ const formatted = formatWithPrettier(original);
75
+ // Sanity checks: Prettier should keep both the else-if branch and the associated story annotation,
76
+ // but the exact layout and comment movement may vary between versions.
77
+ expect(formatted).toContain("else if");
78
+ expect(formatted).toContain("@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md");
79
+ const result = runEslintWithRequireBranchAnnotation(formatted);
80
+ expect(result.status).toBe(0);
81
+ });
82
+ it("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-INSIDE] accepts code where annotations start between condition and body and are preserved by Prettier", () => {
83
+ const original = `
85
84
  function doA() {
86
85
  return 1;
87
86
  }
@@ -102,70 +101,10 @@ if (aVeryLongConditionThatForcesPrettierToWrapTheElseIfBranch && anotherConditio
102
101
  doB();
103
102
  }
104
103
  `;
105
- const formatted = formatWithPrettier(original);
106
- // Note: Prettier's exact layout of the else-if and its comments may differ between versions;
107
- // the rule should accept any of the supported annotation positions regardless of formatting.
108
- const result = runEslintWithRequireBranchAnnotation(formatted);
109
- expect(result.status).toBe(0);
110
- });
111
- }
112
- else {
113
- it.skip("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-BEFORE] accepts code where annotations start before else-if but are moved between condition and body by Prettier", () => {
114
- const original = `
115
- function doA() {
116
- return 1;
117
- }
118
-
119
- function doB() {
120
- return 2;
121
- }
122
-
123
- // @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
124
- // @req REQ-BRANCH-DETECTION
125
- if (aVeryLongConditionThatForcesPrettierToWrapTheElseIfBranch && anotherCondition) {
126
- doA();
127
- }
128
- // @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
129
- // @req REQ-DUAL-POSITION-DETECTION-ELSE-IF
130
- else if (anotherVeryLongConditionThatForcesWrapping && someOtherCondition) {
131
- doB();
132
- }
133
- `;
134
- const formatted = formatWithPrettier(original);
135
- // Sanity checks: Prettier should keep both the else-if branch and the associated story annotation,
136
- // but the exact layout and comment movement may vary between versions.
137
- expect(formatted).toContain("else if");
138
- expect(formatted).toContain("@story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md");
139
- const result = runEslintWithRequireBranchAnnotation(formatted);
140
- expect(result.status).toBe(0);
141
- });
142
- it.skip("[REQ-PRETTIER-COMPATIBILITY-ELSE-IF-INSIDE] accepts code where annotations start between condition and body and are preserved by Prettier", () => {
143
- const original = `
144
- function doA() {
145
- return 1;
146
- }
147
-
148
- function doB() {
149
- return 2;
150
- }
151
-
152
- // @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
153
- // @req REQ-BRANCH-DETECTION
154
- if (aVeryLongConditionThatForcesPrettierToWrapTheElseIfBranch && anotherCondition) {
155
- doA();
156
- } else if (
157
- anotherVeryLongConditionThatForcesWrapping && someOtherCondition
158
- ) {
159
- // @story docs/stories/026.0-DEV-ELSE-IF-ANNOTATION-POSITION.story.md
160
- // @req REQ-DUAL-POSITION-DETECTION-ELSE-IF
161
- doB();
162
- }
163
- `;
164
- const formatted = formatWithPrettier(original);
165
- // Note: Prettier's exact layout of the else-if and its comments may differ between versions;
166
- // the rule should accept any of the supported annotation positions regardless of formatting.
167
- const result = runEslintWithRequireBranchAnnotation(formatted);
168
- expect(result.status).toBe(0);
169
- });
170
- }
104
+ const formatted = formatWithPrettier(original);
105
+ // Note: Prettier's exact layout of the else-if and its comments may differ between versions;
106
+ // the rule should accept any of the supported annotation positions regardless of formatting.
107
+ const result = runEslintWithRequireBranchAnnotation(formatted);
108
+ expect(result.status).toBe(0);
109
+ });
171
110
  });
@@ -59,6 +59,7 @@ describe("Plugin Default Export and Configs (Story 001.0-DEV-PLUGIN-SETUP)", ()
59
59
  "valid-req-reference",
60
60
  "prefer-implements-annotation",
61
61
  "require-test-traceability",
62
+ "no-redundant-annotation",
62
63
  "prefer-supports-annotation",
63
64
  ];
64
65
  // Act: get actual rule names from plugin