eslint-plugin-traceability 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -54,6 +54,7 @@ module.exports = [
54
54
  - `traceability/valid-annotation-format` Enforces correct format of traceability annotations. ([Documentation](docs/rules/valid-annotation-format.md))
55
55
  - `traceability/valid-story-reference` Validates that `@story` references point to existing story files. ([Documentation](docs/rules/valid-story-reference.md))
56
56
  - `traceability/valid-req-reference` Validates that `@req` references point to existing requirement IDs. ([Documentation](docs/rules/valid-req-reference.md))
57
+ - `traceability/prefer-implements-annotation` Recommends migration from legacy `@story`/`@req` annotations to `@implements` (disabled by default). ([Documentation](docs/rules/prefer-implements-annotation.md))
57
58
 
58
59
  Configuration options: For detailed per-rule options (such as scopes, branch types, and story directory settings), see the individual rule docs in `docs/rules/` and the consolidated [API Reference](user-docs/api-reference.md).
59
60
 
@@ -14,7 +14,7 @@ import { detectStaleAnnotations, updateAnnotationReferences, batchUpdateAnnotati
14
14
  * @story docs/stories/002.0-DYNAMIC-RULE-LOADING.story.md
15
15
  * @req REQ-RULE-LIST - Enumerate supported rule file names for plugin discovery
16
16
  */
17
- declare const RULE_NAMES: readonly ["require-story-annotation", "require-req-annotation", "require-branch-annotation", "valid-annotation-format", "valid-story-reference", "valid-req-reference"];
17
+ declare const RULE_NAMES: readonly ["require-story-annotation", "require-req-annotation", "require-branch-annotation", "valid-annotation-format", "valid-story-reference", "valid-req-reference", "prefer-implements-annotation"];
18
18
  type RuleName = (typeof RULE_NAMES)[number];
19
19
  declare const rules: Record<RuleName, Rule.RuleModule>;
20
20
  /**
@@ -54,7 +54,7 @@ declare const maintenance: {
54
54
  };
55
55
  export { rules, configs, maintenance };
56
56
  declare const _default: {
57
- rules: Record<"require-story-annotation" | "require-req-annotation" | "require-branch-annotation" | "valid-annotation-format" | "valid-story-reference" | "valid-req-reference", Rule.RuleModule>;
57
+ rules: Record<"require-story-annotation" | "require-req-annotation" | "require-branch-annotation" | "valid-annotation-format" | "valid-story-reference" | "valid-req-reference" | "prefer-implements-annotation", Rule.RuleModule>;
58
58
  configs: {
59
59
  recommended: {
60
60
  plugins: {
package/lib/src/index.js CHANGED
@@ -17,6 +17,7 @@ const RULE_NAMES = [
17
17
  "valid-annotation-format",
18
18
  "valid-story-reference",
19
19
  "valid-req-reference",
20
+ "prefer-implements-annotation",
20
21
  ];
21
22
  const rules = {};
22
23
  exports.rules = rules;
@@ -85,6 +86,7 @@ const TRACEABILITY_RULE_SEVERITIES = {
85
86
  "traceability/valid-annotation-format": "warn",
86
87
  "traceability/valid-story-reference": "error",
87
88
  "traceability/valid-req-reference": "error",
89
+ "traceability/prefer-implements-annotation": "warn",
88
90
  };
89
91
  /**
90
92
  * @story docs/stories/007.0-DEV-ERROR-REPORTING.story.md
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Internal helpers and types for the valid-annotation-format rule.
3
+ *
4
+ * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
5
+ * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
6
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
7
+ * @req REQ-MULTILINE-SUPPORT - Handle annotations split across multiple lines
8
+ * @req REQ-FLEXIBLE-PARSING - Support reasonable variations in whitespace and formatting
9
+ * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
10
+ * @req REQ-IMPLEMENTS-PARSE - Parse @implements annotations without affecting @story/@req
11
+ * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
12
+ */
13
+ /**
14
+ * Pending annotation state tracked while iterating through comment lines.
15
+ */
16
+ export interface PendingAnnotation {
17
+ type: "story" | "req";
18
+ value: string;
19
+ hasValue: boolean;
20
+ }
21
+ /**
22
+ * Normalize a raw comment line to make annotation parsing more robust.
23
+ *
24
+ * This function trims whitespace, keeps any annotation tags that appear
25
+ * later in the line, and supports common JSDoc styles such as leading "*".
26
+ *
27
+ * It detects @story, @req, and @implements tags while preserving the rest
28
+ * of the line for downstream logic.
29
+ */
30
+ export declare function normalizeCommentLine(rawLine: string): string;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ /**
3
+ * Internal helpers and types for the valid-annotation-format rule.
4
+ *
5
+ * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
6
+ * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
7
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
8
+ * @req REQ-MULTILINE-SUPPORT - Handle annotations split across multiple lines
9
+ * @req REQ-FLEXIBLE-PARSING - Support reasonable variations in whitespace and formatting
10
+ * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
11
+ * @req REQ-IMPLEMENTS-PARSE - Parse @implements annotations without affecting @story/@req
12
+ * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.normalizeCommentLine = normalizeCommentLine;
16
+ /**
17
+ * Normalize a raw comment line to make annotation parsing more robust.
18
+ *
19
+ * This function trims whitespace, keeps any annotation tags that appear
20
+ * later in the line, and supports common JSDoc styles such as leading "*".
21
+ *
22
+ * It detects @story, @req, and @implements tags while preserving the rest
23
+ * of the line for downstream logic.
24
+ */
25
+ function normalizeCommentLine(rawLine) {
26
+ const trimmed = rawLine.trim();
27
+ if (!trimmed) {
28
+ return "";
29
+ }
30
+ const annotationMatch = trimmed.match(/@story\b|@req\b|@implements\b/);
31
+ if (!annotationMatch || annotationMatch.index === undefined) {
32
+ const withoutLeadingStar = trimmed.replace(/^\*\s?/, "");
33
+ return withoutLeadingStar;
34
+ }
35
+ return trimmed.slice(annotationMatch.index);
36
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Helpers for @implements annotation validation used by valid-annotation-format.
3
+ *
4
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
5
+ * @req REQ-IMPLEMENTS-PARSE - Parse @implements annotations without affecting @story/@req
6
+ * @req REQ-FORMAT-VALIDATION - Validate @implements story path and requirement IDs
7
+ * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
8
+ */
9
+ import type { ResolvedAnnotationOptions } from "./valid-annotation-options";
10
+ /**
11
+ * Minimum number of tokens required for a valid @implements value:
12
+ * - one story path
13
+ * - at least one requirement ID
14
+ *
15
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
16
+ * @req REQ-IMPLEMENTS-PARSE
17
+ */
18
+ export declare const MIN_IMPLEMENTS_TOKENS = 2;
19
+ /**
20
+ * Report a completely missing @implements value (no story path or req IDs).
21
+ *
22
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
23
+ * @req REQ-FORMAT-VALIDATION
24
+ */
25
+ export declare function reportMissingImplementsValue(context: any, comment: any, options: ResolvedAnnotationOptions): void;
26
+ /**
27
+ * Report a value that has only a story path and no requirement IDs.
28
+ *
29
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
30
+ * @req REQ-FORMAT-VALIDATION
31
+ */
32
+ export declare function reportMissingImplementsReqIds(context: any, comment: any, options: ResolvedAnnotationOptions): void;
33
+ /**
34
+ * Report an invalid story path inside @implements.
35
+ *
36
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
37
+ * @req REQ-FORMAT-VALIDATION
38
+ */
39
+ export declare function reportInvalidImplementsStoryPath(context: any, comment: any, storyPath: string, options: ResolvedAnnotationOptions): void;
40
+ /**
41
+ * Report an invalid requirement ID token inside @implements.
42
+ *
43
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
44
+ * @req REQ-FORMAT-VALIDATION
45
+ * @req REQ-MIXED-SUPPORT
46
+ */
47
+ export declare function reportInvalidImplementsReqId(context: any, comment: any, reqId: string, options: ResolvedAnnotationOptions): void;
48
+ type ImplementsDeps = {
49
+ MIN_IMPLEMENTS_TOKENS: number;
50
+ reportMissingImplementsValue: typeof reportMissingImplementsValue;
51
+ reportMissingImplementsReqIds: typeof reportMissingImplementsReqIds;
52
+ reportInvalidImplementsStoryPath: typeof reportInvalidImplementsStoryPath;
53
+ reportInvalidImplementsReqId: typeof reportInvalidImplementsReqId;
54
+ };
55
+ /**
56
+ * Validate an @implements annotation value.
57
+ *
58
+ * This helper encapsulates the logic previously in valid-annotation-format.ts:
59
+ * - trims the raw value
60
+ * - splits into tokens
61
+ * - enforces MIN_IMPLEMENTS_TOKENS
62
+ * - validates the story path using options.storyPattern
63
+ * - validates each requirement ID using options.reqPattern
64
+ * - delegates reporting to the provided helpers
65
+ *
66
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
67
+ * @req REQ-IMPLEMENTS-PARSE
68
+ * @req REQ-FORMAT-VALIDATION
69
+ * @req REQ-MIXED-SUPPORT
70
+ */
71
+ export declare function validateImplementsAnnotationHelper(deps: ImplementsDeps, context: any, comment: any, args: {
72
+ rawValue: string | null | undefined;
73
+ options: ResolvedAnnotationOptions;
74
+ }): void;
75
+ export {};
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MIN_IMPLEMENTS_TOKENS = void 0;
4
+ exports.reportMissingImplementsValue = reportMissingImplementsValue;
5
+ exports.reportMissingImplementsReqIds = reportMissingImplementsReqIds;
6
+ exports.reportInvalidImplementsStoryPath = reportInvalidImplementsStoryPath;
7
+ exports.reportInvalidImplementsReqId = reportInvalidImplementsReqId;
8
+ exports.validateImplementsAnnotationHelper = validateImplementsAnnotationHelper;
9
+ const valid_annotation_utils_1 = require("./valid-annotation-utils");
10
+ /**
11
+ * Minimum number of tokens required for a valid @implements value:
12
+ * - one story path
13
+ * - at least one requirement ID
14
+ *
15
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
16
+ * @req REQ-IMPLEMENTS-PARSE
17
+ */
18
+ exports.MIN_IMPLEMENTS_TOKENS = 2;
19
+ /**
20
+ * Report a completely missing @implements value (no story path or req IDs).
21
+ *
22
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
23
+ * @req REQ-FORMAT-VALIDATION
24
+ */
25
+ function reportMissingImplementsValue(context, comment, options) {
26
+ const { storyExample, reqExample } = options;
27
+ context.report({
28
+ node: comment,
29
+ messageId: "invalidImplementsFormat",
30
+ data: {
31
+ details: `Missing story path and requirement IDs for @implements annotation. Expected a value like "${storyExample} ${reqExample}".`,
32
+ },
33
+ });
34
+ }
35
+ /**
36
+ * Report a value that has only a story path and no requirement IDs.
37
+ *
38
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
39
+ * @req REQ-FORMAT-VALIDATION
40
+ */
41
+ function reportMissingImplementsReqIds(context, comment, options) {
42
+ const { storyExample, reqExample } = options;
43
+ context.report({
44
+ node: comment,
45
+ messageId: "invalidImplementsFormat",
46
+ data: {
47
+ details: `Missing requirement IDs for @implements annotation. Expected a value like "${storyExample} ${reqExample}".`,
48
+ },
49
+ });
50
+ }
51
+ /**
52
+ * Report an invalid story path inside @implements.
53
+ *
54
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
55
+ * @req REQ-FORMAT-VALIDATION
56
+ */
57
+ function reportInvalidImplementsStoryPath(context, comment, storyPath, options) {
58
+ const { storyExample } = options;
59
+ context.report({
60
+ node: comment,
61
+ messageId: "invalidImplementsFormat",
62
+ data: {
63
+ details: `Invalid story path "${storyPath}" for @implements annotation. Expected a path like "${storyExample}".`,
64
+ },
65
+ });
66
+ }
67
+ /**
68
+ * Report an invalid requirement ID token inside @implements.
69
+ *
70
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
71
+ * @req REQ-FORMAT-VALIDATION
72
+ * @req REQ-MIXED-SUPPORT
73
+ */
74
+ function reportInvalidImplementsReqId(context, comment, reqId, options) {
75
+ context.report({
76
+ node: comment,
77
+ messageId: "invalidReqFormat",
78
+ data: {
79
+ details: (0, valid_annotation_utils_1.buildReqErrorMessage)("invalid", reqId, options),
80
+ },
81
+ });
82
+ }
83
+ /**
84
+ * Prepare and validate the token array for an @implements value.
85
+ *
86
+ * Returns { storyPath, reqIds } when tokens are present and structurally valid,
87
+ * or null when a missing-value condition has been reported.
88
+ */
89
+ function parseImplementsTokens(deps, context, comment, rest) {
90
+ const { MIN_IMPLEMENTS_TOKENS, reportMissingImplementsValue, reportMissingImplementsReqIds, } = deps;
91
+ const { rawValue, options } = rest;
92
+ const value = rawValue?.trim() ?? "";
93
+ if (!value) {
94
+ reportMissingImplementsValue(context, comment, options);
95
+ return null;
96
+ }
97
+ const tokens = value.split(/\s+/);
98
+ if (tokens.length < MIN_IMPLEMENTS_TOKENS) {
99
+ reportMissingImplementsReqIds(context, comment, options);
100
+ return null;
101
+ }
102
+ const [storyPath, ...reqIds] = tokens;
103
+ return { storyPath, reqIds };
104
+ }
105
+ /**
106
+ * Validate the parsed storyPath and reqIds against the provided patterns and
107
+ * delegate reporting of any invalid tokens.
108
+ */
109
+ function validateImplementsTokens(deps, context, comment, rest) {
110
+ const { reportInvalidImplementsStoryPath, reportInvalidImplementsReqId } = deps;
111
+ const { parsed, options } = rest;
112
+ const { storyPath, reqIds } = parsed;
113
+ if (!options.storyPattern.test(storyPath)) {
114
+ reportInvalidImplementsStoryPath(context, comment, storyPath, options);
115
+ return;
116
+ }
117
+ for (const reqId of reqIds) {
118
+ if (!options.reqPattern.test(reqId)) {
119
+ reportInvalidImplementsReqId(context, comment, reqId, options);
120
+ }
121
+ }
122
+ }
123
+ /**
124
+ * Validate an @implements annotation value.
125
+ *
126
+ * This helper encapsulates the logic previously in valid-annotation-format.ts:
127
+ * - trims the raw value
128
+ * - splits into tokens
129
+ * - enforces MIN_IMPLEMENTS_TOKENS
130
+ * - validates the story path using options.storyPattern
131
+ * - validates each requirement ID using options.reqPattern
132
+ * - delegates reporting to the provided helpers
133
+ *
134
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
135
+ * @req REQ-IMPLEMENTS-PARSE
136
+ * @req REQ-FORMAT-VALIDATION
137
+ * @req REQ-MIXED-SUPPORT
138
+ */
139
+ function validateImplementsAnnotationHelper(deps, context, comment, args) {
140
+ const { rawValue, options } = args;
141
+ const parsed = parseImplementsTokens(deps, context, comment, {
142
+ rawValue,
143
+ options,
144
+ });
145
+ if (!parsed) {
146
+ return;
147
+ }
148
+ validateImplementsTokens(deps, context, comment, { parsed, options });
149
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ESLint rule implementation for preferring the consolidated `@implements`
3
+ * annotation over legacy combinations of `@story` and `@req` within JSDoc
4
+ * block comments. This module provides:
5
+ *
6
+ * - Detection of legacy `@story` + `@req` patterns.
7
+ * - Identification of multi-story comment blocks that are not safely
8
+ * auto-fixable.
9
+ * - A conservative auto-fix that rewrites simple, single-story patterns into
10
+ * a single `@implements` annotation while preserving formatting.
11
+ *
12
+ * The rule is intended as an **optional migration aid** to help projects
13
+ * gradually move to the newer `@implements` format without breaking existing
14
+ * traceability links.
15
+ *
16
+ * @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
17
+ * @req REQ-OPTIONAL-WARNING - Emit configurable recommendation diagnostics for legacy @story/@req usage
18
+ * @req REQ-MULTI-STORY-DETECT - Detect multi-story patterns that cannot be auto-fixed
19
+ * @req REQ-SINGLE-STORY-FIX - Restrict auto-fix to single-story, single-path cases
20
+ * @req REQ-PRESERVE-FORMAT - Preserve original JSDoc indentation and prefix formatting
21
+ * @req REQ-VALID-OUTPUT - Avoid emitting auto-fixes for complex or ambiguous patterns
22
+ * @req REQ-BACKWARD-COMP-VALIDATION - Keep legacy @story/@req annotations valid when the rule is disabled
23
+ * @req REQ-AUTO-FIX - Provide safe, opt-in auto-fix for simple legacy patterns
24
+ */
25
+ import type { Rule } from "eslint";
26
+ /**
27
+ * ESLint rule: prefer-implements-annotation
28
+ *
29
+ * Recommend migrating from legacy `@story` + `@req` annotations to the
30
+ * newer `@implements` format. This rule is **disabled by default** and
31
+ * is intended as an optional, opt-in migration aid.
32
+ *
33
+ * @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
34
+ * @req REQ-OPTIONAL-WARNING - Emit configurable recommendation diagnostics for legacy @story/@req usage
35
+ * @req REQ-MULTI-STORY-DETECT - Detect multi-story patterns that cannot be auto-fixed
36
+ * @req REQ-BACKWARD-COMP-VALIDATION - Keep legacy @story/@req annotations valid when the rule is disabled
37
+ */
38
+ declare const preferImplementsAnnotationRule: Rule.RuleModule;
39
+ export default preferImplementsAnnotationRule;
@@ -0,0 +1,276 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const valid_annotation_format_internal_1 = require("./helpers/valid-annotation-format-internal");
4
+ // Maximum number of distinct @story paths allowed before treating as "multi-story".
5
+ // @req REQ-MULTI-STORY-DETECT - Centralized threshold constant for detecting multi-story patterns
6
+ const MULTI_STORY_THRESHOLD = 1;
7
+ // Minimum number of tokens required for a valid @story annotation line.
8
+ // @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
9
+ // @req REQ-MULTI-STORY-DETECT
10
+ const MIN_STORY_TOKENS = 2;
11
+ // Minimum number of tokens required for a valid @req annotation line, aligned with story tokens.
12
+ const MIN_REQ_TOKENS = MIN_STORY_TOKENS;
13
+ // Length of the opening "/*" portion of a block comment prefix.
14
+ const COMMENT_PREFIX_LENGTH = 2;
15
+ function collectStoryAndReqMetadata(comment) {
16
+ const rawValue = comment.value || "";
17
+ const rawLines = rawValue.split(/\r?\n/);
18
+ const storyLineIndices = [];
19
+ const reqLineIndices = [];
20
+ const reqIds = [];
21
+ let storyPath = null;
22
+ rawLines.forEach((rawLine, index) => {
23
+ const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(rawLine);
24
+ if (!normalized)
25
+ return;
26
+ if (/^@implements\b/.test(normalized)) {
27
+ // Mixed @implements usage should have been filtered out earlier
28
+ return;
29
+ }
30
+ if (/^@story\b/.test(normalized)) {
31
+ const parts = normalized.split(/\s+/);
32
+ if (parts.length === MIN_STORY_TOKENS) {
33
+ storyLineIndices.push(index);
34
+ storyPath = parts[1];
35
+ }
36
+ else {
37
+ storyPath = null;
38
+ }
39
+ return;
40
+ }
41
+ if (/^@req\b/.test(normalized)) {
42
+ const parts = normalized.split(/\s+/);
43
+ if (parts.length === MIN_REQ_TOKENS) {
44
+ reqLineIndices.push(index);
45
+ reqIds.push(parts[1]);
46
+ }
47
+ else {
48
+ // Complex @req form; bail out entirely.
49
+ storyPath = null;
50
+ }
51
+ }
52
+ });
53
+ return { storyLineIndices, reqLineIndices, reqIds, storyPath };
54
+ }
55
+ function applyImplementsReplacement(context, comment, details) {
56
+ const { storyIdx, allIndicesToRemove, storyPath, reqIds } = details;
57
+ const rawValue = comment.value || "";
58
+ const rawLines = rawValue.split(/\r?\n/);
59
+ const implAnnotation = `@implements ${storyPath} ${reqIds.join(" ")}`;
60
+ // Determine the leading prefix (indentation and `*`) from the original @story line
61
+ const storyRawLine = rawLines[storyIdx];
62
+ const prefixMatch = storyRawLine.match(/^(\s*\*?\s*)/);
63
+ const linePrefix = prefixMatch ? prefixMatch[1] : "";
64
+ const implementsLine = `${linePrefix}${implAnnotation}`;
65
+ const fixedLines = [];
66
+ rawLines.forEach((line, index) => {
67
+ if (index === storyIdx) {
68
+ fixedLines.push(implementsLine);
69
+ return;
70
+ }
71
+ if (allIndicesToRemove.has(index)) {
72
+ return;
73
+ }
74
+ fixedLines.push(line);
75
+ });
76
+ const fixedValue = fixedLines.join("\n");
77
+ const sourceCode = context.getSourceCode();
78
+ return (fixer) => fixer.replaceTextRange([comment.range[0], comment.range[1]], sourceCode.text.slice(comment.range[0], comment.range[0] + COMMENT_PREFIX_LENGTH) +
79
+ fixedValue +
80
+ "*/");
81
+ }
82
+ /**
83
+ * Build an ESLint auto-fix for simple single-story `@story` + `@req` JSDoc
84
+ * blocks, converting them to a single `@implements` annotation while
85
+ * preserving the original comment formatting.
86
+ *
87
+ * The fixer is intentionally conservative and only activates when:
88
+ * - There is exactly one distinct `@story` path.
89
+ * - Exactly one `@story` line is present.
90
+ * - At least one `@req` line is present.
91
+ * - Each `@req` line has the simple form `@req <REQ-ID>` (no extra tokens).
92
+ *
93
+ * When applicable, the fix:
94
+ * - Removes the original `@story` and `@req` lines.
95
+ * - Inserts a single `@implements` line in their place, preserving the
96
+ * original leading comment prefix (indentation and `*` markers).
97
+ *
98
+ * More complex patterns remain diagnostics-only with no fix to avoid
99
+ * producing invalid or ambiguous output.
100
+ *
101
+ * @implements docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
102
+ * @req REQ-AUTO-FIX - Provide safe, opt-in auto-fix for simple legacy patterns
103
+ * @req REQ-SINGLE-STORY-FIX - Restrict auto-fix to single-story, single-path cases
104
+ * @req REQ-PRESERVE-FORMAT - Preserve original JSDoc indentation and prefix formatting
105
+ * @req REQ-VALID-OUTPUT - Avoid emitting auto-fixes for complex or ambiguous patterns
106
+ */
107
+ function buildImplementsAutoFix(context, comment, storyPaths) {
108
+ if (storyPaths.size !== 1)
109
+ return null;
110
+ const { storyLineIndices, reqLineIndices, reqIds, storyPath } = collectStoryAndReqMetadata(comment);
111
+ if (storyPaths.size !== 1 ||
112
+ storyLineIndices.length !== 1 ||
113
+ reqLineIndices.length < 1 ||
114
+ storyPath === null) {
115
+ return null;
116
+ }
117
+ const storyIdx = storyLineIndices[0];
118
+ const allIndicesToRemove = new Set([
119
+ ...storyLineIndices,
120
+ ...reqLineIndices,
121
+ ]);
122
+ return applyImplementsReplacement(context, comment, {
123
+ storyIdx,
124
+ allIndicesToRemove,
125
+ storyPath,
126
+ reqIds,
127
+ });
128
+ }
129
+ function analyzeComment(comment) {
130
+ const rawLines = (comment.value || "").split(/\r?\n/);
131
+ let hasStory = false;
132
+ let hasReq = false;
133
+ let hasImplements = false;
134
+ const storyPaths = new Set();
135
+ rawLines.forEach((rawLine) => {
136
+ const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(rawLine);
137
+ if (!normalized)
138
+ return;
139
+ if (/^@implements\b/.test(normalized)) {
140
+ hasImplements = true;
141
+ return;
142
+ }
143
+ if (/^@story\b/.test(normalized)) {
144
+ hasStory = true;
145
+ const parts = normalized.split(/\s+/);
146
+ if (parts.length >= MIN_STORY_TOKENS) {
147
+ storyPaths.add(parts[1]);
148
+ }
149
+ return;
150
+ }
151
+ if (/^@req\b/.test(normalized)) {
152
+ hasReq = true;
153
+ }
154
+ });
155
+ return { hasStory, hasReq, hasImplements, storyPaths };
156
+ }
157
+ function hasMultipleStories(storyPaths) {
158
+ // @req REQ-MULTI-STORY-DETECT - Use named threshold constant instead of a magic number
159
+ return storyPaths.size > MULTI_STORY_THRESHOLD;
160
+ }
161
+ function processComment(comment, context) {
162
+ const { hasStory, hasReq, hasImplements, storyPaths } = analyzeComment(comment);
163
+ if (!hasStory || !hasReq) {
164
+ return;
165
+ }
166
+ if (hasImplements) {
167
+ context.report({
168
+ node: comment,
169
+ messageId: "cannotAutoFix",
170
+ data: {
171
+ reason: "comment mixes @story/@req with existing @implements annotations",
172
+ },
173
+ });
174
+ return;
175
+ }
176
+ if (hasMultipleStories(storyPaths)) {
177
+ context.report({
178
+ node: comment,
179
+ messageId: "multiStoryDetected",
180
+ });
181
+ return;
182
+ }
183
+ const fix = buildImplementsAutoFix(context, comment, storyPaths);
184
+ context.report({
185
+ node: comment,
186
+ messageId: "preferImplements",
187
+ fix: fix ?? undefined,
188
+ });
189
+ }
190
+ /**
191
+ * ESLint rule: prefer-implements-annotation
192
+ *
193
+ * Recommend migrating from legacy `@story` + `@req` annotations to the
194
+ * newer `@implements` format. This rule is **disabled by default** and
195
+ * is intended as an optional, opt-in migration aid.
196
+ *
197
+ * @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
198
+ * @req REQ-OPTIONAL-WARNING - Emit configurable recommendation diagnostics for legacy @story/@req usage
199
+ * @req REQ-MULTI-STORY-DETECT - Detect multi-story patterns that cannot be auto-fixed
200
+ * @req REQ-BACKWARD-COMP-VALIDATION - Keep legacy @story/@req annotations valid when the rule is disabled
201
+ */
202
+ const preferImplementsAnnotationRule = {
203
+ meta: {
204
+ type: "suggestion",
205
+ docs: {
206
+ description: "Recommend using @implements instead of legacy @story + @req annotations (optional migration rule)",
207
+ recommended: false,
208
+ },
209
+ // Auto-fix support will be wired in a later iteration; the rule starts as
210
+ // a recommendation-only warning with no code modifications.
211
+ fixable: "code",
212
+ messages: {
213
+ /**
214
+ * Recommend migrating simple, single-story @story + @req blocks to a
215
+ * single @implements line. Auto-fix is provided where safe in a
216
+ * follow-up iteration.
217
+ *
218
+ * @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
219
+ * @req REQ-OPTIONAL-WARNING
220
+ */
221
+ preferImplements: "Consider using @implements instead of @story + @req for clearer traceability. Run ESLint with --fix to auto-convert.",
222
+ /**
223
+ * Report situations where the rule detects a legacy annotation pattern
224
+ * but cannot safely provide an automatic fix. The `reason` field gives
225
+ * a short, human-readable explanation to guide manual migration.
226
+ *
227
+ * @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
228
+ * @req REQ-MULTI-STORY-DETECT
229
+ */
230
+ cannotAutoFix: "Cannot auto-fix: {{reason}}. Manual migration to @implements required.",
231
+ /**
232
+ * Specialized message for the most common non-fixable case where more
233
+ * than one @story annotation appears in the same block, indicating a
234
+ * likely multi-story integration that must be converted manually.
235
+ *
236
+ * @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
237
+ * @req REQ-MULTI-STORY-DETECT
238
+ */
239
+ multiStoryDetected: "Multiple @story annotations detected in the same comment block. Manually convert to separate @implements lines.",
240
+ },
241
+ schema: [],
242
+ },
243
+ /**
244
+ * Rule entrypoint.
245
+ *
246
+ * This initial implementation focuses on **detection and messaging only**:
247
+ * it surfaces recommendations when legacy `@story` + `@req` combinations are
248
+ * present but does not yet perform automatic code modifications.
249
+ *
250
+ * @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
251
+ * @req REQ-OPTIONAL-WARNING
252
+ * @req REQ-MULTI-STORY-DETECT
253
+ */
254
+ create(context) {
255
+ const sourceCode = context.getSourceCode();
256
+ return {
257
+ /**
258
+ * Program-level visitor that scans all comments for legacy
259
+ * `@story` + `@req` usage and emits recommendation diagnostics.
260
+ *
261
+ * @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
262
+ * @req REQ-OPTIONAL-WARNING - Emit recommendations when legacy annotations are detected
263
+ * @req REQ-MULTI-STORY-DETECT - Detect multi-story and mixed annotation patterns
264
+ */
265
+ Program() {
266
+ const comments = sourceCode.getAllComments() || [];
267
+ comments
268
+ .filter((comment) => comment.type === "Block")
269
+ .forEach((comment) => {
270
+ processComment(comment, context);
271
+ });
272
+ },
273
+ };
274
+ },
275
+ };
276
+ exports.default = preferImplementsAnnotationRule;
@@ -2,34 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const valid_annotation_options_1 = require("./helpers/valid-annotation-options");
4
4
  const valid_annotation_utils_1 = require("./helpers/valid-annotation-utils");
5
- /**
6
- * Normalize a raw comment line to make annotation parsing more robust.
7
- *
8
- * This function trims whitespace, keeps any annotation tags that appear
9
- * later in the line, and supports common JSDoc styles such as leading "*".
10
- *
11
- * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
12
- * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
13
- * @req REQ-FLEXIBLE-PARSING - Support reasonable variations in whitespace and formatting
14
- * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
15
- */
16
- function normalizeCommentLine(rawLine) {
17
- const trimmed = rawLine.trim();
18
- if (!trimmed) {
19
- return "";
20
- }
21
- const annotationMatch = trimmed.match(/@story\b|@req\b/);
22
- if (!annotationMatch || annotationMatch.index === undefined) {
23
- const withoutLeadingStar = trimmed.replace(/^\*\s?/, "");
24
- return withoutLeadingStar;
25
- }
26
- return trimmed.slice(annotationMatch.index);
27
- }
5
+ const valid_implements_utils_1 = require("./helpers/valid-implements-utils");
6
+ const valid_annotation_format_internal_1 = require("./helpers/valid-annotation-format-internal");
28
7
  /**
29
8
  * Report an invalid @story annotation without applying a fix.
30
9
  *
31
10
  * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
32
11
  * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
12
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
33
13
  * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
34
14
  */
35
15
  function reportInvalidStoryFormat(context, comment, collapsed, options) {
@@ -91,6 +71,7 @@ function createStoryFix(context, comment, fixed) {
91
71
  *
92
72
  * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
93
73
  * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
74
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
94
75
  * @req REQ-PATH-FORMAT - Validate @story paths follow expected patterns
95
76
  * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
96
77
  * @req REQ-AUTOFIX-SAFE - Auto-fix must be conservative and avoid changing semantics
@@ -117,11 +98,13 @@ function reportInvalidStoryFormatWithFix(context, comment, collapsed, fixed) {
117
98
  *
118
99
  * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
119
100
  * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
101
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
120
102
  * @req REQ-PATH-FORMAT - Validate @story paths follow expected patterns
121
103
  * @req REQ-ERROR-SPECIFICITY - Provide specific error messages for different format violations
122
104
  * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
123
105
  * @req REQ-REGEX-VALIDATION - Validate configurable story regex patterns and fall back safely
124
106
  * @req REQ-BACKWARD-COMP - Preserve behavior when invalid regex config is supplied
107
+ * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
125
108
  */
126
109
  function validateStoryAnnotation(context, comment, rawValue, options) {
127
110
  const trimmed = rawValue.trim();
@@ -154,10 +137,12 @@ function validateStoryAnnotation(context, comment, rawValue, options) {
154
137
  *
155
138
  * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
156
139
  * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
140
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
157
141
  * @req REQ-REQ-FORMAT - Validate @req identifiers follow expected patterns
158
142
  * @req REQ-ERROR-SPECIFICITY - Provide specific error messages for different format violations
159
143
  * @req REQ-REGEX-VALIDATION - Validate configurable requirement regex patterns and fall back safely
160
144
  * @req REQ-BACKWARD-COMP - Preserve behavior when invalid regex config is supplied
145
+ * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
161
146
  */
162
147
  function validateReqAnnotation(context, comment, rawValue, options) {
163
148
  const trimmed = rawValue.trim();
@@ -179,13 +164,47 @@ function validateReqAnnotation(context, comment, rawValue, options) {
179
164
  });
180
165
  }
181
166
  }
167
+ /**
168
+ * Validate an @implements annotation value and report detailed errors when needed.
169
+ *
170
+ * Expected format:
171
+ * @implements <storyPath> <REQ-ID> [<REQ-ID> ...]
172
+ *
173
+ * Validation rules:
174
+ * - Value must include at least a story path and one requirement ID.
175
+ * - Story path must match the same storyPattern used for @story (no auto-fix).
176
+ * - Each subsequent token must match reqPattern and is validated individually.
177
+ *
178
+ * Story path issues are reported with "invalidImplementsFormat" and
179
+ * requirement ID issues reuse the existing "invalidReqFormat" message.
180
+ *
181
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
182
+ * @req REQ-IMPLEMENTS-PARSE - Parse @implements annotations without affecting @story/@req
183
+ * @req REQ-FORMAT-VALIDATION - Validate @implements story path and requirement IDs
184
+ * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
185
+ */
186
+ function validateImplementsAnnotation(context, comment, rawValue, options) {
187
+ const deps = {
188
+ MIN_IMPLEMENTS_TOKENS: valid_implements_utils_1.MIN_IMPLEMENTS_TOKENS,
189
+ reportMissingImplementsReqIds: valid_implements_utils_1.reportMissingImplementsReqIds,
190
+ reportMissingImplementsValue: valid_implements_utils_1.reportMissingImplementsValue,
191
+ reportInvalidImplementsReqId: valid_implements_utils_1.reportInvalidImplementsReqId,
192
+ reportInvalidImplementsStoryPath: valid_implements_utils_1.reportInvalidImplementsStoryPath,
193
+ };
194
+ (0, valid_implements_utils_1.validateImplementsAnnotationHelper)(deps, context, comment, {
195
+ rawValue,
196
+ options,
197
+ });
198
+ }
182
199
  /**
183
200
  * Finalize and validate the currently pending annotation, if any.
184
201
  *
185
202
  * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
186
203
  * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
204
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
187
205
  * @req REQ-SYNTAX-VALIDATION - Validate annotation syntax matches specification
188
206
  * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
207
+ * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
189
208
  */
190
209
  function finalizePendingAnnotation(context, comment, options, pending) {
191
210
  if (!pending) {
@@ -193,8 +212,10 @@ function finalizePendingAnnotation(context, comment, options, pending) {
193
212
  }
194
213
  // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
195
214
  // @story docs/stories/008.0-DEV-AUTO-FIX.story.md
215
+ // @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
196
216
  // @req REQ-SYNTAX-VALIDATION - Dispatch validation based on annotation type
197
217
  // @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
218
+ // @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
198
219
  if (pending.type === "story") {
199
220
  validateStoryAnnotation(context, comment, pending.value, options);
200
221
  }
@@ -208,9 +229,13 @@ function finalizePendingAnnotation(context, comment, options, pending) {
208
229
  *
209
230
  * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
210
231
  * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
232
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
211
233
  * @req REQ-SYNTAX-VALIDATION - Start new pending annotation when a tag is found
212
234
  * @req REQ-MULTILINE-SUPPORT - Treat subsequent lines as continuation for pending annotation
213
235
  * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
236
+ * @req REQ-IMPLEMENTS-PARSE - Parse @implements annotations without affecting @story/@req
237
+ * @req REQ-FORMAT-VALIDATION - Validate @implements story path and requirement IDs
238
+ * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
214
239
  */
215
240
  function processCommentLine({ normalized, pending, context, comment, options, }) {
216
241
  if (!normalized) {
@@ -218,10 +243,19 @@ function processCommentLine({ normalized, pending, context, comment, options, })
218
243
  }
219
244
  const isStory = /@story\b/.test(normalized);
220
245
  const isReq = /@req\b/.test(normalized);
246
+ const isImplements = /@implements\b/.test(normalized);
247
+ // Handle @implements as an immediate, single-line annotation
248
+ if (isImplements) {
249
+ const implementsValue = normalized.replace(/^@implements\b/, "").trim();
250
+ validateImplementsAnnotation(context, comment, implementsValue, options);
251
+ return pending;
252
+ }
221
253
  // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
222
254
  // @story docs/stories/008.0-DEV-AUTO-FIX.story.md
255
+ // @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
223
256
  // @req REQ-SYNTAX-VALIDATION - Start new pending annotation when a tag is found
224
257
  // @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
258
+ // @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
225
259
  if (isStory || isReq) {
226
260
  finalizePendingAnnotation(context, comment, options, pending);
227
261
  const value = normalized.replace(/^@story\b|^@req\b/, "").trim();
@@ -233,8 +267,10 @@ function processCommentLine({ normalized, pending, context, comment, options, })
233
267
  }
234
268
  // @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
235
269
  // @story docs/stories/008.0-DEV-AUTO-FIX.story.md
270
+ // @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
236
271
  // @req REQ-MULTILINE-SUPPORT - Treat subsequent lines as continuation for pending annotation
237
272
  // @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
273
+ // @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
238
274
  if (pending) {
239
275
  const continuation = normalized.trim();
240
276
  if (!continuation) {
@@ -252,23 +288,30 @@ function processCommentLine({ normalized, pending, context, comment, options, })
252
288
  return pending;
253
289
  }
254
290
  /**
255
- * Process a single comment node and validate any @story/@req annotations it contains.
291
+ * Process a single comment node and validate any @story/@req/@implements annotations it contains.
256
292
  *
257
- * Supports annotations whose values span multiple lines within the same
293
+ * Supports @story and @req annotations whose values span multiple lines within the same
258
294
  * comment block, collapsing whitespace so that the logical value can be
259
295
  * validated against the configured patterns.
260
296
  *
297
+ * @implements annotations are validated immediately per-line and are not
298
+ * accumulated into pending multi-line state.
299
+ *
261
300
  * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
262
301
  * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
302
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
263
303
  * @req REQ-MULTILINE-SUPPORT - Handle annotations split across multiple lines
264
304
  * @req REQ-FLEXIBLE-PARSING - Support reasonable variations in whitespace and formatting
265
305
  * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
306
+ * @req REQ-IMPLEMENTS-PARSE - Parse @implements annotations without affecting @story/@req
307
+ * @req REQ-FORMAT-VALIDATION - Validate @implements story path and requirement IDs
308
+ * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
266
309
  */
267
310
  function processComment(context, comment, options) {
268
311
  const rawLines = (comment.value || "").split(/\r?\n/);
269
312
  let pending = null;
270
313
  rawLines.forEach((rawLine) => {
271
- const normalized = normalizeCommentLine(rawLine);
314
+ const normalized = (0, valid_annotation_format_internal_1.normalizeCommentLine)(rawLine);
272
315
  pending = processCommentLine({
273
316
  normalized,
274
317
  pending,
@@ -283,7 +326,7 @@ exports.default = {
283
326
  meta: {
284
327
  type: "problem",
285
328
  docs: {
286
- description: "Validate format and syntax of @story and @req annotations",
329
+ description: "Validate format and syntax of @story, @req, and @implements annotations",
287
330
  recommended: "error",
288
331
  },
289
332
  messages: {
@@ -301,6 +344,14 @@ exports.default = {
301
344
  * @req REQ-ERROR-CONSISTENCY - Use shared "Invalid annotation format: {{details}}." message pattern across rules
302
345
  */
303
346
  invalidReqFormat: "Invalid annotation format: {{details}}.",
347
+ /**
348
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
349
+ * @req REQ-ERROR-SPECIFIC - Provide specific details about invalid @implements annotation format
350
+ * @req REQ-ERROR-CONTEXT - Include human-readable details about the expected @implements annotation format
351
+ * @req REQ-ERROR-CONSISTENCY - Use shared "Invalid annotation format: {{details}}." message pattern across rules
352
+ * @req REQ-FORMAT-VALIDATION - Validate @implements story path and requirement IDs
353
+ */
354
+ invalidImplementsFormat: "Invalid annotation format: {{details}}.",
304
355
  /**
305
356
  * @story docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md
306
357
  * @req REQ-REGEX-VALIDATION - Surface configuration errors for invalid regex patterns
@@ -326,11 +377,15 @@ exports.default = {
326
377
  * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
327
378
  * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
328
379
  * @story docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md
380
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
329
381
  * @req REQ-SYNTAX-VALIDATION - Ensure rule create function validates annotations syntax
330
382
  * @req REQ-FORMAT-SPECIFICATION - Implement formatting checks per specification
331
383
  * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
332
384
  * @req REQ-REGEX-VALIDATION - Derive validation regexes from shared options helper
333
385
  * @req REQ-BACKWARD-COMP - Fall back to default patterns and continue validation on config errors
386
+ * @req REQ-IMPLEMENTS-PARSE - Parse @implements annotations without affecting @story/@req
387
+ * @req REQ-FORMAT-VALIDATION - Validate @implements story path and requirement IDs
388
+ * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
334
389
  */
335
390
  create(context) {
336
391
  const sourceCode = context.getSourceCode();
@@ -338,16 +393,20 @@ exports.default = {
338
393
  const optionErrors = (0, valid_annotation_options_1.getOptionErrors)();
339
394
  return {
340
395
  /**
341
- * Program-level handler that inspects all comments for @story and @req tags
396
+ * Program-level handler that inspects all comments for @story, @req, and @implements tags
342
397
  *
343
398
  * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md
344
399
  * @story docs/stories/008.0-DEV-AUTO-FIX.story.md
345
400
  * @story docs/stories/010.1-DEV-CONFIGURABLE-PATTERNS.story.md
401
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
346
402
  * @req REQ-PATH-FORMAT - Validate @story paths follow expected patterns
347
403
  * @req REQ-REQ-FORMAT - Validate @req identifiers follow expected patterns
348
404
  * @req REQ-AUTOFIX-FORMAT - Provide safe, minimal automatic fixes for common format issues
349
405
  * @req REQ-REGEX-VALIDATION - Surface regex configuration errors without blocking validation
350
406
  * @req REQ-BACKWARD-COMP - Continue validating comments using default patterns on error
407
+ * @req REQ-IMPLEMENTS-PARSE - Parse @implements annotations without affecting @story/@req
408
+ * @req REQ-FORMAT-VALIDATION - Validate @implements story path and requirement IDs
409
+ * @req REQ-MIXED-SUPPORT - Support mixed @story/@req/@implements usage in comments
351
410
  */
352
411
  Program(node) {
353
412
  if (optionErrors && optionErrors.length > 0) {
@@ -14,6 +14,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
14
14
  */
15
15
  const fs_1 = __importDefault(require("fs"));
16
16
  const path_1 = __importDefault(require("path"));
17
+ /**
18
+ * Token index configuration for @implements annotations.
19
+ * This clarifies the expected positions of the story path and first requirement ID
20
+ * and avoids hard-coded "magic number" indices in parsing logic.
21
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
22
+ */
23
+ const IMPLEMENTS_TOKENS = {
24
+ STORY_INDEX: 1,
25
+ FIRST_REQ_INDEX: 2,
26
+ };
17
27
  /**
18
28
  * Extract the story path from a JSDoc comment.
19
29
  * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
@@ -164,11 +174,68 @@ function validateReqLine(opts) {
164
174
  reqSet,
165
175
  });
166
176
  }
177
+ /**
178
+ * Parse an @implements annotation line into its story path and requirement IDs.
179
+ * Expects the format: "@implements <storyPath> <REQ-ID-1> <REQ-ID-2> ..."
180
+ * Invalid formats (missing storyPath or reqIds) are ignored by this deep rule.
181
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
182
+ * @req REQ-IMPLEMENTS-VALIDATE - Support validation of @implements annotations
183
+ * @req REQ-MIXED-SUPPORT - Allow mixed @story/@req/@implements usage in the same comment
184
+ * @req REQ-SCOPED-IDS - Treat requirement IDs as scoped to the referenced story file
185
+ */
186
+ function parseImplementsLine(line) {
187
+ const parts = line.split(/\s+/);
188
+ const storyPath = parts[IMPLEMENTS_TOKENS.STORY_INDEX];
189
+ const reqIds = parts.slice(IMPLEMENTS_TOKENS.FIRST_REQ_INDEX);
190
+ if (!storyPath || reqIds.length === 0) {
191
+ return null;
192
+ }
193
+ return { storyPath, reqIds };
194
+ }
195
+ /**
196
+ * Validate an @implements annotation line against the referenced story content.
197
+ * Performs path validation, file reading, caching, and requirement existence checks
198
+ * for each requirement ID listed on the line.
199
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
200
+ * @req REQ-IMPLEMENTS-VALIDATE - Validate that all @implements requirement IDs exist
201
+ * @req REQ-MIXED-SUPPORT - Ensure @implements can coexist with @story/@req annotations
202
+ * @req REQ-SCOPED-IDS - Validate requirement IDs in the scope of their explicit story
203
+ */
204
+ function validateImplementsLine(opts) {
205
+ const { comment, context, line, cwd, reqCache } = opts;
206
+ const parsed = parseImplementsLine(line);
207
+ if (!parsed) {
208
+ return;
209
+ }
210
+ const { storyPath, reqIds } = parsed;
211
+ const { reqSet } = resolveStoryAndRequirements({
212
+ comment,
213
+ context,
214
+ storyPath,
215
+ cwd,
216
+ reqCache,
217
+ });
218
+ if (!reqSet) {
219
+ return;
220
+ }
221
+ for (const reqId of reqIds) {
222
+ checkRequirementExists({
223
+ comment,
224
+ context,
225
+ reqId,
226
+ storyPath,
227
+ reqSet,
228
+ });
229
+ }
230
+ }
167
231
  /**
168
232
  * Handle a single annotation line for story or requirement metadata.
169
233
  * @story docs/stories/010.0-DEV-DEEP-VALIDATION.story.md
170
234
  * @req REQ-DEEP-PARSE - Parse annotation lines for @story and @req tags
171
235
  * @req REQ-DEEP-MATCH - Dispatch @req lines for validation against story requirements
236
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
237
+ * @req REQ-IMPLEMENTS-VALIDATE - Dispatch @implements lines for validation
238
+ * @req REQ-MIXED-SUPPORT - Support mixed annotation types without interfering with each other
172
239
  */
173
240
  function handleAnnotationLine(opts) {
174
241
  const { line, comment, context, cwd, reqCache, storyPath } = opts;
@@ -180,6 +247,10 @@ function handleAnnotationLine(opts) {
180
247
  validateReqLine({ comment, context, line, storyPath, cwd, reqCache });
181
248
  return storyPath;
182
249
  }
250
+ else if (line.startsWith("@implements")) {
251
+ validateImplementsLine({ comment, context, line, cwd, reqCache });
252
+ return storyPath;
253
+ }
183
254
  return storyPath;
184
255
  }
185
256
  /**
@@ -55,6 +55,7 @@ describe("Plugin Default Export and Configs (Story 001.0-DEV-PLUGIN-SETUP)", ()
55
55
  "valid-annotation-format",
56
56
  "valid-story-reference",
57
57
  "valid-req-reference",
58
+ "prefer-implements-annotation",
58
59
  ];
59
60
  // Act: get actual rule names from plugin
60
61
  const actual = Object.keys(index_1.rules);
@@ -79,10 +80,12 @@ describe("Plugin Default Export and Configs (Story 001.0-DEV-PLUGIN-SETUP)", ()
79
80
  expect(recommendedRules).toHaveProperty("traceability/require-branch-annotation", "error");
80
81
  expect(recommendedRules).toHaveProperty("traceability/valid-story-reference", "error");
81
82
  expect(recommendedRules).toHaveProperty("traceability/valid-req-reference", "error");
83
+ expect(recommendedRules).toHaveProperty("traceability/prefer-implements-annotation", "warn");
82
84
  });
83
85
  it("[REQ-ERROR-SEVERITY] configs.strict uses same severity mapping as recommended", () => {
84
86
  const strictRules = index_1.configs.strict[0].rules;
85
87
  const recommendedRules = index_1.configs.recommended[0].rules;
86
88
  expect(strictRules).toEqual(recommendedRules);
89
+ expect(strictRules).toHaveProperty("traceability/prefer-implements-annotation", "warn");
87
90
  });
88
91
  });
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ /**
7
+ * Tests for: docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
8
+ * @story docs/stories/010.3-DEV-MIGRATE-TO-IMPLEMENTS.story.md
9
+ * @req REQ-OPTIONAL-WARNING - Verify rule emits recommendations for legacy @story/@req usage
10
+ * @req REQ-MULTI-STORY-DETECT - Verify rule detects multi-story and mixed-annotation patterns
11
+ * @req REQ-CONFIG-SEVERITY - Verify rule is disabled by default and can be enabled as warn/error
12
+ */
13
+ const eslint_1 = require("eslint");
14
+ const prefer_implements_annotation_1 = __importDefault(require("../../src/rules/prefer-implements-annotation"));
15
+ const ruleTester = new eslint_1.RuleTester({
16
+ languageOptions: {
17
+ parserOptions: { ecmaVersion: 2020, sourceType: "module" },
18
+ },
19
+ });
20
+ describe("prefer-implements-annotation rule (Story 010.3-DEV-MIGRATE-TO-IMPLEMENTS)", () => {
21
+ ruleTester.run("prefer-implements-annotation", prefer_implements_annotation_1.default, {
22
+ valid: [
23
+ {
24
+ name: "[REQ-BACKWARD-COMP-VALIDATION] comment with only @story is ignored",
25
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n */\nfunction onlyStory() {}`,
26
+ },
27
+ {
28
+ name: "[REQ-BACKWARD-COMP-VALIDATION] comment with only @req is ignored",
29
+ code: `/**\n * @req REQ-ONLY\n */\nfunction onlyReq() {}`,
30
+ },
31
+ {
32
+ name: "[REQ-BACKWARD-COMP-VALIDATION] comment with @implements only is ignored",
33
+ code: `/**\n * @implements docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction alreadyImplements() {}`,
34
+ },
35
+ ],
36
+ invalid: [
37
+ {
38
+ name: "[REQ-OPTIONAL-WARNING] single-story @story + @req block triggers preferImplements message",
39
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction legacy() {}`,
40
+ output: `/**\n * @implements docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction legacy() {}`,
41
+ errors: [{ messageId: "preferImplements" }],
42
+ },
43
+ {
44
+ name: "[REQ-MULTI-STORY-DETECT] mixed @story/@req and @implements triggers cannotAutoFix",
45
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n * @implements docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction mixed() {}`,
46
+ errors: [
47
+ {
48
+ messageId: "cannotAutoFix",
49
+ data: {
50
+ reason: "comment mixes @story/@req with existing @implements annotations",
51
+ },
52
+ },
53
+ ],
54
+ },
55
+ {
56
+ name: "[REQ-MULTI-STORY-DETECT] multiple @story paths in same block trigger multiStoryDetected",
57
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n * @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md\n * @req REQ-BRANCH-DETECTION\n */\nfunction multiStory() {}`,
58
+ errors: [{ messageId: "multiStoryDetected" }],
59
+ },
60
+ {
61
+ name: "[REQ-AUTO-FIX] single @story + single @req auto-fixes to single @implements line",
62
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction autoFixSingleReq() {}`,
63
+ output: `/**\n * @implements docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ANNOTATION-REQUIRED\n */\nfunction autoFixSingleReq() {}`,
64
+ errors: [{ messageId: "preferImplements" }],
65
+ },
66
+ {
67
+ name: "[REQ-SINGLE-STORY-FIX] single @story with multiple @req lines auto-fixes to single @implements line containing all REQ IDs",
68
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ONE\n * @req REQ-TWO\n * @req REQ-THREE\n */\nfunction autoFixMultiReq() {}`,
69
+ output: `/**\n * @implements docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md REQ-ONE REQ-TWO REQ-THREE\n */\nfunction autoFixMultiReq() {}`,
70
+ errors: [{ messageId: "preferImplements" }],
71
+ },
72
+ {
73
+ name: "[REQ-AUTO-FIX] complex @req content (extra description) does not auto-fix but still warns",
74
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md\n * @req REQ-ANNOTATION-REQUIRED must handle extra description\n */\nfunction complexReqNoAutoFix() {}`,
75
+ errors: [{ messageId: "preferImplements" }],
76
+ },
77
+ {
78
+ name: "[REQ-AUTO-FIX] complex @story content (extra description) does not auto-fix but still warns",
79
+ code: `/**\n * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md additional descriptive text\n * @req REQ-ANNOTATION-REQUIRED\n */\nfunction complexStoryNoAutoFix() {}`,
80
+ errors: [{ messageId: "preferImplements" }],
81
+ },
82
+ ],
83
+ });
84
+ });
@@ -18,6 +18,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
18
18
  * @req REQ-CONFIGURABLE-PATTERNS-REQ - Rule supports configurable requirement ID regex patterns
19
19
  * @req REQ-CONFIGURABLE-PATTERNS-EXAMPLES - Rule supports configurable example strings in error messages
20
20
  * @req REQ-CONFIGURABLE-PATTERNS-FALLBACK - Invalid regex patterns fall back to default behavior without crashing
21
+ * Tests for: docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
22
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
23
+ * @req REQ-IMPLEMENTS-PARSE - Rule parses @implements annotations with story and requirement references
24
+ * @req REQ-FORMAT-VALIDATION - Rule validates story and requirement formats inside @implements annotations
25
+ * @req REQ-MIXED-SUPPORT - Rule supports mixed @story/@req/@implements usage in the same comment
21
26
  */
22
27
  const eslint_1 = require("eslint");
23
28
  const valid_annotation_format_1 = __importDefault(require("../../src/rules/valid-annotation-format"));
@@ -168,6 +173,27 @@ describe("Valid Annotation Format Rule (Story 005.0-DEV-ANNOTATION-VALIDATION)",
168
173
  },
169
174
  ],
170
175
  },
176
+ {
177
+ name: "[REQ-IMPLEMENTS-PARSE] valid single @implements with one story and one requirement (default patterns)",
178
+ code: `/**
179
+ * @implements docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md REQ-IMPLEMENTS-PARSE
180
+ */`,
181
+ },
182
+ {
183
+ name: "[REQ-IMPLEMENTS-PARSE] valid multiple @implements lines with different stories and requirements",
184
+ code: `/**
185
+ * @implements docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md REQ-IMPLEMENTS-PARSE REQ-FORMAT-VALIDATION
186
+ * @implements docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-FORMAT-SPECIFICATION
187
+ */`,
188
+ },
189
+ {
190
+ name: "[REQ-MIXED-SUPPORT] valid mixed @story/@req/@implements usage in same block comment",
191
+ code: `/**
192
+ * @story docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
193
+ * @req REQ-MIXED-SUPPORT
194
+ * @implements docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md REQ-IMPLEMENTS-PARSE REQ-FORMAT-VALIDATION REQ-MIXED-SUPPORT
195
+ */`,
196
+ },
171
197
  ],
172
198
  invalid: [
173
199
  makeInvalidStory({
@@ -480,6 +506,58 @@ describe("Valid Annotation Format Rule (Story 005.0-DEV-ANNOTATION-VALIDATION)",
480
506
  },
481
507
  ],
482
508
  },
509
+ makeInvalid({
510
+ name: "[REQ-IMPLEMENTS-PARSE] @implements with no value is invalid",
511
+ code: `/**
512
+ * @implements
513
+ */`,
514
+ messageId: "invalidImplementsFormat",
515
+ details: 'Missing story path and requirement IDs for @implements annotation. Expected a value like "docs/stories/005.0-DEV-EXAMPLE.story.md REQ-EXAMPLE".',
516
+ }),
517
+ makeInvalid({
518
+ name: "[REQ-IMPLEMENTS-PARSE] @implements with only story path and no requirement IDs is invalid",
519
+ code: `/**
520
+ * @implements docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md
521
+ */`,
522
+ messageId: "invalidImplementsFormat",
523
+ details: 'Missing requirement IDs for @implements annotation. Expected a value like "docs/stories/005.0-DEV-EXAMPLE.story.md REQ-EXAMPLE".',
524
+ }),
525
+ makeInvalid({
526
+ name: "[REQ-FORMAT-VALIDATION] @implements with invalid story path format",
527
+ code: `/**
528
+ * @implements invalid/path.txt REQ-IMPLEMENTS-PARSE
529
+ */`,
530
+ messageId: "invalidImplementsFormat",
531
+ details: 'Invalid story path "invalid/path.txt" for @implements annotation. Expected a path like "docs/stories/005.0-DEV-EXAMPLE.story.md".',
532
+ }),
533
+ {
534
+ name: "[REQ-FORMAT-VALIDATION] @implements with invalid requirement ID format",
535
+ code: `/**
536
+ * @implements docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md REQ-VALID invalid-format
537
+ */`,
538
+ errors: [
539
+ {
540
+ messageId: "invalidReqFormat",
541
+ data: {
542
+ details: 'Invalid requirement ID "invalid-format" for @req annotation. Expected an identifier like "REQ-EXAMPLE" (uppercase letters, numbers, and dashes only).',
543
+ },
544
+ },
545
+ ],
546
+ },
547
+ {
548
+ name: "[REQ-FORMAT-VALIDATION] @implements with multiple requirement IDs where one is invalid",
549
+ code: `/**
550
+ * @implements docs/stories/010.2-DEV-MULTI-STORY-SUPPORT.story.md REQ-VALID-1 REQ-VALID-2 bad-id
551
+ */`,
552
+ errors: [
553
+ {
554
+ messageId: "invalidReqFormat",
555
+ data: {
556
+ details: 'Invalid requirement ID "bad-id" for @req annotation. Expected an identifier like "REQ-EXAMPLE" (uppercase letters, numbers, and dashes only).',
557
+ },
558
+ },
559
+ ],
560
+ },
483
561
  ],
484
562
  });
485
563
  });
@@ -32,6 +32,15 @@ describe("Valid Req Reference Rule (Story 010.0-DEV-DEEP-VALIDATION)", () => {
32
32
  code: `// @story tests/fixtures/story_bullet.md
33
33
  // @req REQ-BULLET-LIST`,
34
34
  },
35
+ {
36
+ name: "[REQ-DEEP-IMPLEMENTS] single implements line with multiple requirements in multi-story fixture (see 010.2-DEV-MULTI-STORY-SUPPORT)",
37
+ code: `// @implements tests/fixtures/story_multi_a.md REQ-SHARED-ID REQ-ONLY-A`,
38
+ },
39
+ {
40
+ name: "[REQ-DEEP-IMPLEMENTS] multi-story implements with shared requirement IDs (see 010.2-DEV-MULTI-STORY-SUPPORT)",
41
+ code: `// @implements tests/fixtures/story_multi_a.md REQ-SHARED-ID REQ-ONLY-A
42
+ // @implements tests/fixtures/story_multi_b.md REQ-SHARED-ID REQ-ONLY-B`,
43
+ },
35
44
  ],
36
45
  invalid: [
37
46
  {
@@ -88,6 +97,31 @@ describe("Valid Req Reference Rule (Story 010.0-DEV-DEEP-VALIDATION)", () => {
88
97
  },
89
98
  ],
90
99
  },
100
+ {
101
+ name: "[REQ-DEEP-IMPLEMENTS] missing implements requirement in multi-story fixture (see 010.2-DEV-MULTI-STORY-SUPPORT)",
102
+ code: `// @implements tests/fixtures/story_multi_a.md REQ-NOT-IN-A`,
103
+ errors: [
104
+ {
105
+ messageId: "reqMissing",
106
+ data: {
107
+ reqId: "REQ-NOT-IN-A",
108
+ storyPath: "tests/fixtures/story_multi_a.md",
109
+ },
110
+ },
111
+ ],
112
+ },
113
+ {
114
+ name: "[REQ-DEEP-IMPLEMENTS] disallow path traversal in implements story path (see 010.2-DEV-MULTI-STORY-SUPPORT)",
115
+ code: `// @implements ../tests/fixtures/story_multi_a.md REQ-SHARED-ID`,
116
+ errors: [
117
+ {
118
+ messageId: "invalidPath",
119
+ data: {
120
+ storyPath: "../tests/fixtures/story_multi_a.md",
121
+ },
122
+ },
123
+ ],
124
+ },
91
125
  ],
92
126
  });
93
127
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
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",