eslint-plugin-traceability 1.11.1 → 1.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +3 -3
  3. package/lib/src/index.d.ts +10 -5
  4. package/lib/src/index.js +71 -6
  5. package/lib/src/maintenance/commands.js +2 -3
  6. package/lib/src/maintenance/update.js +1 -14
  7. package/lib/src/rules/helpers/require-story-core.d.ts +12 -4
  8. package/lib/src/rules/helpers/require-story-core.js +59 -30
  9. package/lib/src/rules/helpers/require-story-helpers.d.ts +7 -41
  10. package/lib/src/rules/helpers/require-story-helpers.js +13 -70
  11. package/lib/src/rules/helpers/valid-annotation-format-internal.d.ts +12 -13
  12. package/lib/src/rules/helpers/valid-annotation-format-internal.js +21 -16
  13. package/lib/src/rules/helpers/valid-annotation-format-validators.d.ts +29 -3
  14. package/lib/src/rules/helpers/valid-annotation-format-validators.js +29 -3
  15. package/lib/src/rules/helpers/valid-annotation-utils.d.ts +3 -3
  16. package/lib/src/rules/helpers/valid-annotation-utils.js +10 -10
  17. package/lib/src/rules/helpers/valid-req-reference-helpers.d.ts +11 -0
  18. package/lib/src/rules/helpers/valid-req-reference-helpers.js +362 -0
  19. package/lib/src/rules/prefer-implements-annotation.js +7 -7
  20. package/lib/src/rules/require-story-annotation.d.ts +2 -0
  21. package/lib/src/rules/require-story-annotation.js +1 -1
  22. package/lib/src/rules/valid-req-reference.d.ts +4 -0
  23. package/lib/src/rules/valid-req-reference.js +5 -349
  24. package/lib/src/rules/valid-story-reference.d.ts +1 -1
  25. package/lib/src/rules/valid-story-reference.js +17 -10
  26. package/lib/src/utils/branch-annotation-helpers.d.ts +2 -2
  27. package/lib/src/utils/branch-annotation-helpers.js +96 -17
  28. package/lib/tests/cli-error-handling.test.js +1 -1
  29. package/lib/tests/config/eslint-config-validation.test.js +73 -0
  30. package/lib/tests/fixtures/stale/example.js +1 -1
  31. package/lib/tests/fixtures/update/example.js +1 -1
  32. package/lib/tests/integration/catch-annotation-prettier.integration.test.d.ts +1 -0
  33. package/lib/tests/integration/catch-annotation-prettier.integration.test.js +131 -0
  34. package/lib/tests/integration/dogfooding-validation.test.d.ts +1 -0
  35. package/lib/tests/integration/dogfooding-validation.test.js +94 -0
  36. package/lib/tests/maintenance/cli.test.js +37 -0
  37. package/lib/tests/maintenance/detect-isolated.test.js +5 -5
  38. package/lib/tests/perf/maintenance-cli-large-workspace.test.js +18 -0
  39. package/lib/tests/perf/require-branch-annotation-large-file.test.d.ts +1 -0
  40. package/lib/tests/perf/require-branch-annotation-large-file.test.js +67 -0
  41. package/lib/tests/perf/valid-annotation-format-large-file.test.d.ts +1 -0
  42. package/lib/tests/perf/valid-annotation-format-large-file.test.js +74 -0
  43. package/lib/tests/plugin-default-export-and-configs.test.js +1 -0
  44. package/lib/tests/plugin-setup.test.js +12 -1
  45. package/lib/tests/rules/prefer-implements-annotation.test.js +84 -70
  46. package/lib/tests/rules/require-branch-annotation.test.js +33 -1
  47. package/lib/tests/rules/valid-annotation-format-internal.test.d.ts +8 -0
  48. package/lib/tests/rules/valid-annotation-format-internal.test.js +47 -0
  49. package/lib/tests/utils/branch-annotation-catch-insert-position.test.d.ts +1 -0
  50. package/lib/tests/utils/branch-annotation-catch-insert-position.test.js +68 -0
  51. package/lib/tests/utils/branch-annotation-catch-position.test.d.ts +1 -0
  52. package/lib/tests/utils/branch-annotation-catch-position.test.js +115 -0
  53. package/lib/tests/utils/req-annotation-detection.test.d.ts +1 -0
  54. package/lib/tests/utils/req-annotation-detection.test.js +247 -0
  55. package/package.json +4 -4
  56. package/user-docs/api-reference.md +20 -12
  57. package/user-docs/examples.md +2 -1
  58. package/user-docs/migration-guide.md +11 -7
@@ -12,6 +12,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  };
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
14
  const valid_story_reference_1 = __importDefault(require("../../src/rules/valid-story-reference"));
15
+ const use_at_your_own_risk_1 = require("eslint/use-at-your-own-risk");
16
+ const index_1 = __importDefault(require("../../src/index"));
15
17
  /** @story docs/stories/002.0-DEV-ESLINT-CONFIG.story.md */
16
18
  describe("ESLint Configuration Setup (Story 002.0-DEV-ESLINT-CONFIG)", () => {
17
19
  it("[REQ-RULE-OPTIONS] rule meta.schema defines expected properties", () => {
@@ -24,4 +26,75 @@ describe("ESLint Configuration Setup (Story 002.0-DEV-ESLINT-CONFIG)", () => {
24
26
  const schema = valid_story_reference_1.default.meta.schema[0];
25
27
  expect(schema.additionalProperties).toBe(false);
26
28
  });
29
+ it("[REQ-CONFIG-VALIDATION] ESLint throws on unknown rule option", async () => {
30
+ const eslint = new use_at_your_own_risk_1.FlatESLint({
31
+ overrideConfig: [
32
+ {
33
+ plugins: {
34
+ traceability: index_1.default,
35
+ },
36
+ rules: {
37
+ "traceability/valid-story-reference": [
38
+ "error",
39
+ {
40
+ storyDirectories: ["stories"],
41
+ allowAbsolutePaths: false,
42
+ requireStoryExtension: true,
43
+ unknownOptionKey: true,
44
+ },
45
+ ],
46
+ },
47
+ },
48
+ ],
49
+ overrideConfigFile: true,
50
+ ignore: false,
51
+ });
52
+ let caughtError;
53
+ try {
54
+ await eslint.lintText("const x = 1;");
55
+ }
56
+ catch (err) {
57
+ caughtError = err;
58
+ }
59
+ expect(caughtError).toBeInstanceOf(Error);
60
+ const message = String(caughtError.message || caughtError);
61
+ expect(message).toContain("traceability/valid-story-reference");
62
+ expect(message.toLowerCase()).toContain("additional");
63
+ expect(message.toLowerCase()).toContain("unexpected property");
64
+ expect(message).toContain("unknownOptionKey");
65
+ });
66
+ it("[REQ-CONFIG-VALIDATION] ESLint throws on invalid option type", async () => {
67
+ const eslint = new use_at_your_own_risk_1.FlatESLint({
68
+ overrideConfig: [
69
+ {
70
+ plugins: {
71
+ traceability: index_1.default,
72
+ },
73
+ rules: {
74
+ "traceability/valid-story-reference": [
75
+ "error",
76
+ {
77
+ // storyDirectories must be an array, not a string
78
+ storyDirectories: "not-an-array",
79
+ },
80
+ ],
81
+ },
82
+ },
83
+ ],
84
+ overrideConfigFile: true,
85
+ ignore: false,
86
+ });
87
+ let caughtError;
88
+ try {
89
+ await eslint.lintText("const y = 2;");
90
+ }
91
+ catch (err) {
92
+ caughtError = err;
93
+ }
94
+ expect(caughtError).toBeInstanceOf(Error);
95
+ const message = String(caughtError.message || caughtError);
96
+ expect(message).toContain("traceability/valid-story-reference");
97
+ expect(message).toContain("not-an-array");
98
+ expect(message.toLowerCase()).toContain("array");
99
+ });
27
100
  });
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
2
  // Sample code with stale annotation
3
- // @story non-existent.md
3
+ // @story docs/stories/non-existent.story.md
@@ -1,3 +1,3 @@
1
1
  "use strict";
2
2
  // Sample code with annotation to update
3
- // @story old.md
3
+ // @story docs/stories/old.story.md
@@ -0,0 +1,131 @@
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
+ * Prettier integration tests for CatchClause annotation positions.
8
+ * @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
9
+ * @supports docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md REQ-PRETTIER-COMPATIBILITY
10
+ */
11
+ const path_1 = __importDefault(require("path"));
12
+ const child_process_1 = require("child_process");
13
+ describe("CatchClause annotations with Prettier (Story 025.0-DEV-CATCH-ANNOTATION-POSITION)", () => {
14
+ const eslintPkgDir = path_1.default.dirname(require.resolve("eslint/package.json"));
15
+ const eslintCliPath = path_1.default.join(eslintPkgDir, "bin", "eslint.js");
16
+ const configPath = path_1.default.resolve(__dirname, "../../eslint.config.js");
17
+ const prettierPackageJson = require.resolve("prettier/package.json");
18
+ const prettierCliPath = path_1.default.join(path_1.default.dirname(prettierPackageJson), "bin", "prettier.cjs");
19
+ function runEslintWithRequireBranchAnnotation(code) {
20
+ const args = [
21
+ "--no-config-lookup",
22
+ "--config",
23
+ configPath,
24
+ "--stdin",
25
+ "--stdin-filename",
26
+ "catch.js",
27
+ "--rule",
28
+ "no-unused-vars:off",
29
+ "--rule",
30
+ "no-magic-numbers:off",
31
+ "--rule",
32
+ "no-undef:off",
33
+ "--rule",
34
+ "no-console:off",
35
+ "--rule",
36
+ "traceability/require-branch-annotation:error",
37
+ ];
38
+ return (0, child_process_1.spawnSync)(process.execPath, [eslintCliPath, ...args], {
39
+ encoding: "utf-8",
40
+ input: code,
41
+ });
42
+ }
43
+ function formatWithPrettier(source) {
44
+ const result = (0, child_process_1.spawnSync)(process.execPath, [prettierCliPath, "--parser", "typescript"], {
45
+ encoding: "utf-8",
46
+ input: source,
47
+ });
48
+ if (result.status !== 0) {
49
+ throw new Error(`Prettier formatting failed: ${result.stderr || result.stdout}`);
50
+ }
51
+ return result.stdout;
52
+ }
53
+ it("[REQ-PRETTIER-COMPATIBILITY-BEFORE] accepts code where annotations start before catch but are moved inside by Prettier", () => {
54
+ const original = `
55
+ function doSomething() {
56
+ return 42;
57
+ }
58
+
59
+ function handleError(error) {
60
+ console.error(error);
61
+ }
62
+
63
+ // @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
64
+ // @req REQ-BRANCH-TRY
65
+ try {
66
+ doSomething();
67
+ }
68
+ // @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
69
+ // @req REQ-CATCH-PATH
70
+ catch (error) {
71
+ handleError(error);
72
+ }
73
+ `;
74
+ const formatted = formatWithPrettier(original);
75
+ // Sanity check: Prettier should move the branch annotations inside the catch body.
76
+ expect(formatted).toContain("catch (error) {");
77
+ const catchIndex = formatted.indexOf("catch (error) {");
78
+ const storyIndex = formatted.indexOf("@story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md");
79
+ expect(storyIndex).toBeGreaterThan(catchIndex);
80
+ const result = runEslintWithRequireBranchAnnotation(formatted);
81
+ expect(result.status).toBe(0);
82
+ });
83
+ it("[REQ-PRETTIER-COMPATIBILITY-INSIDE] accepts code where annotations start inside the catch body and are preserved by Prettier", () => {
84
+ const original = `
85
+ function doSomething() {
86
+ return 42;
87
+ }
88
+
89
+ function handleError(error) {
90
+ console.error(error);
91
+ }
92
+
93
+ // @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
94
+ // @req REQ-BRANCH-TRY
95
+ try {
96
+ doSomething();
97
+ } catch (error) {
98
+ // @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
99
+ // @req REQ-CATCH-INSIDE
100
+ handleError(error);
101
+ }
102
+ `;
103
+ const formatted = formatWithPrettier(original);
104
+ // Sanity: annotations should still be associated with the catch body after formatting.
105
+ expect(formatted).toContain("catch (error) {");
106
+ const catchIndex = formatted.indexOf("catch (error) {");
107
+ const storyIndex = formatted.indexOf("@story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md");
108
+ expect(storyIndex).toBeGreaterThan(catchIndex);
109
+ const result = runEslintWithRequireBranchAnnotation(formatted);
110
+ expect(result.status).toBe(0);
111
+ });
112
+ it("[REQ-PRETTIER-COMPATIBILITY-EMPTY] accepts empty catch blocks with inside-catch annotations after Prettier formatting", () => {
113
+ const original = `
114
+ function doSomething() {
115
+ return 42;
116
+ }
117
+
118
+ // @story docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md
119
+ // @req REQ-BRANCH-TRY
120
+ try {
121
+ doSomething();
122
+ } catch (error) {
123
+ // @story docs/stories/025.0-DEV-CATCH-ANNOTATION-POSITION.story.md
124
+ // @req REQ-CATCH-EMPTY
125
+ }
126
+ `;
127
+ const formatted = formatWithPrettier(original);
128
+ const result = runEslintWithRequireBranchAnnotation(formatted);
129
+ expect(result.status).toBe(0);
130
+ });
131
+ });
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ /**
37
+ * Dogfooding validation integration tests
38
+ * @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-TEST REQ-DOGFOODING-CI
39
+ */
40
+ const path = __importStar(require("path"));
41
+ const child_process_1 = require("child_process");
42
+ /**
43
+ * @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-TEST
44
+ */
45
+ function getTsConfigFromEslintConfig(eslintConfig) {
46
+ const configs = Array.isArray(eslintConfig) ? eslintConfig : [eslintConfig];
47
+ return configs.find((config) => {
48
+ if (!config || !config.files)
49
+ return false;
50
+ const files = config.files;
51
+ return files.includes("**/*.ts") && files.includes("**/*.tsx");
52
+ });
53
+ }
54
+ describe("Dogfooding Validation (Story 023.0-MAINT-DOGFOODING-VALIDATION)", () => {
55
+ it("[REQ-DOGFOODING-TEST] should have traceability/require-story-annotation enabled for TS sources", () => {
56
+ /**
57
+ * @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-TEST
58
+ */
59
+ // Require the project's eslint.config.js and find the TS-specific config
60
+ // that applies to *.ts and *.tsx files.
61
+ const eslintConfig = require("../../eslint.config.js");
62
+ const tsConfig = getTsConfigFromEslintConfig(eslintConfig);
63
+ expect(tsConfig).toBeDefined();
64
+ const rules = tsConfig.rules || {};
65
+ const ruleEntry = rules["traceability/require-story-annotation"];
66
+ expect(ruleEntry).toBeDefined();
67
+ const severity = Array.isArray(ruleEntry) && ruleEntry.length > 0
68
+ ? ruleEntry[0]
69
+ : ruleEntry;
70
+ expect(severity).toBe("error");
71
+ });
72
+ it("[REQ-DOGFOODING-CI] should run traceability/require-story-annotation via ESLint CLI on TS sources", () => {
73
+ /**
74
+ * @supports docs/stories/023.0-MAINT-DOGFOODING-VALIDATION.story.md REQ-DOGFOODING-CI
75
+ */
76
+ const eslintBin = path.resolve(__dirname, "../../node_modules/.bin/eslint");
77
+ const configPath = path.resolve(__dirname, "../../eslint.config.js");
78
+ const tsSnippet = `
79
+ const x: number = 42;
80
+ export function foo() {
81
+ return x;
82
+ }
83
+ `;
84
+ const result = (0, child_process_1.spawnSync)(process.platform === "win32" ? `${eslintBin}.cmd` : eslintBin, ["--config", configPath, "--stdin", "--stdin-filename", "src/dogfood.ts"], {
85
+ encoding: "utf8",
86
+ input: tsSnippet,
87
+ });
88
+ // The snippet intentionally lacks @story annotations, so the rule should
89
+ // report an error for the generated `src/dogfood.ts` virtual file.
90
+ expect(result.status).not.toBe(0);
91
+ expect(result.stdout).toContain("error");
92
+ expect(result.stdout).toContain("src/dogfood.ts");
93
+ });
94
+ });
@@ -58,6 +58,26 @@ describe("Maintenance CLI (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
58
58
  temp.cleanup();
59
59
  }
60
60
  });
61
+ it("[REQ-MAINT-VERIFY] verify exits with code 1 and prints guidance when annotations are stale or invalid", () => {
62
+ const temp = (0, temp_dir_helpers_1.createTempDir)("maint-cli-");
63
+ const dir = temp.dir;
64
+ process.chdir(dir);
65
+ const tsContent = `/**\n * @story missing.story.md\n */`;
66
+ fs_1.default.writeFileSync(path_1.default.join(dir, "file.ts"), tsContent, "utf8");
67
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
68
+ const code = (0, cli_1.runMaintenanceCli)(["node", "traceability-maint", "verify"]);
69
+ try {
70
+ expect(code).toBe(1);
71
+ expect(logSpy).toHaveBeenCalledTimes(1);
72
+ const message = String(logSpy.mock.calls[0][0]);
73
+ expect(message).toContain("Stale or invalid traceability annotations detected under");
74
+ expect(message).toContain("Run 'traceability-maint detect' or 'traceability-maint report' for details.");
75
+ }
76
+ finally {
77
+ logSpy.mockRestore();
78
+ temp.cleanup();
79
+ }
80
+ });
61
81
  it("[REQ-MAINT-REPORT] report prints human-readable summary and exits 0", () => {
62
82
  const temp = (0, temp_dir_helpers_1.createTempDir)("maint-cli-");
63
83
  const dir = temp.dir;
@@ -77,6 +97,23 @@ describe("Maintenance CLI (Story 009.0-DEV-MAINTENANCE-TOOLS)", () => {
77
97
  temp.cleanup();
78
98
  }
79
99
  });
100
+ it("[REQ-MAINT-REPORT] report prints 'nothing to report' when no stale annotations exist", () => {
101
+ const temp = (0, temp_dir_helpers_1.createTempDir)("maint-cli-");
102
+ const dir = temp.dir;
103
+ process.chdir(dir);
104
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
105
+ const code = (0, cli_1.runMaintenanceCli)(["node", "traceability-maint", "report"]);
106
+ try {
107
+ expect(code).toBe(0);
108
+ expect(logSpy).toHaveBeenCalled();
109
+ const allMessages = logSpy.mock.calls.flat().join("\n");
110
+ expect(allMessages).toContain("No stale @story annotations found. Nothing to report.");
111
+ }
112
+ finally {
113
+ logSpy.mockRestore();
114
+ temp.cleanup();
115
+ }
116
+ });
80
117
  it("[REQ-MAINT-UPDATE] update performs replacements and exits 0", () => {
81
118
  const temp = (0, temp_dir_helpers_1.createTempDir)("maint-cli-");
82
119
  const dir = temp.dir;
@@ -57,26 +57,26 @@ describe("detectStaleAnnotations isolated (Story 009.0-DEV-MAINTENANCE-TOOLS)",
57
57
  const filePath2 = path.join(nestedDir, "file2.ts");
58
58
  const content1 = `
59
59
  /**
60
- * @story stale1.story.md
60
+ * @story docs/stories/non-existent-story.story.md
61
61
  */
62
62
  `;
63
63
  fs.writeFileSync(filePath1, content1, "utf8");
64
64
  const content2 = `
65
65
  /**
66
- * @story stale2.story.md
66
+ * @story docs/stories/another-non-existent.story.md
67
67
  */
68
68
  `;
69
69
  fs.writeFileSync(filePath2, content2, "utf8");
70
70
  const result = (0, detect_1.detectStaleAnnotations)(tmpDir);
71
71
  expect(result).toHaveLength(2);
72
- expect(result).toContain("stale1.story.md");
73
- expect(result).toContain("stale2.story.md");
72
+ expect(result).toContain("docs/stories/non-existent-story.story.md");
73
+ expect(result).toContain("docs/stories/another-non-existent.story.md");
74
74
  }
75
75
  finally {
76
76
  fs.rmSync(tmpDir, { recursive: true, force: true });
77
77
  }
78
78
  });
79
- it("[REQ-MAINT-DETECT] throws error on permission denied", () => {
79
+ it("[REQ-MAINT-DETECT] handles permission denied errors by returning an empty result", () => {
80
80
  const tmpDir2 = fs.mkdtempSync(path.join(os.tmpdir(), "tmp-perm-"));
81
81
  const dir = path.join(tmpDir2, "subdir");
82
82
  fs.mkdirSync(dir);
@@ -127,4 +127,22 @@ describe("Maintenance CLI on large workspaces (Story 009.0-DEV-MAINTENANCE-TOOLS
127
127
  expect(typeof payload.report).toBe("string");
128
128
  logSpy.mockRestore();
129
129
  });
130
+ it("[REQ-MAINT-VERIFY] verify completes within a generous time budget and reports stale annotations", () => {
131
+ const logSpy = jest.spyOn(console, "log").mockImplementation(() => { });
132
+ const start = perf_hooks_1.performance.now();
133
+ const exitCode = (0, cli_1.runMaintenanceCli)([
134
+ "node",
135
+ "traceability-maint",
136
+ "verify",
137
+ "--root",
138
+ workspace.root,
139
+ ]);
140
+ const durationMs = perf_hooks_1.performance.now() - start;
141
+ expect(exitCode).toBe(1);
142
+ expect(durationMs).toBeLessThan(5000);
143
+ expect(logSpy).toHaveBeenCalledTimes(1);
144
+ const message = String(logSpy.mock.calls[0][0]);
145
+ expect(message).toContain("Stale or invalid traceability annotations detected under");
146
+ logSpy.mockRestore();
147
+ });
130
148
  });
@@ -0,0 +1,67 @@
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
+ * Performance tests for require-branch-annotation on large nested-branch files.
8
+ *
9
+ * @supports docs/stories/004.0-DEV-BRANCH-ANNOTATIONS.story.md REQ-PERFORMANCE-OPTIMIZATION REQ-NESTED-HANDLING
10
+ */
11
+ const eslint_1 = require("eslint");
12
+ const perf_hooks_1 = require("perf_hooks");
13
+ const require_branch_annotation_1 = __importDefault(require("../../src/rules/require-branch-annotation"));
14
+ /**
15
+ * Build a large source file containing many nested branch structures
16
+ * (if-statements within if-statements) to exercise the rule at scale.
17
+ *
18
+ * The generated code intentionally omits annotations so that the rule
19
+ * produces diagnostics for both outer and inner branches.
20
+ */
21
+ function buildLargeNestedBranchSource(functionCount, nestingDepth) {
22
+ const lines = [];
23
+ for (let i = 0; i < functionCount; i += 1) {
24
+ lines.push(`function fn_${i}() {`);
25
+ lines.push(" let x = 0;");
26
+ // Create a staircase of nested if-statements.
27
+ for (let depth = 0; depth < nestingDepth; depth += 1) {
28
+ const indent = " ".repeat(depth + 1);
29
+ lines.push(`${indent}if (x > ${depth}) {`);
30
+ }
31
+ const innerIndent = " ".repeat(nestingDepth + 1);
32
+ lines.push(`${innerIndent}if (x % 2 === 0) {`);
33
+ lines.push(`${innerIndent} x++;`);
34
+ lines.push(`${innerIndent}} else {`);
35
+ lines.push(`${innerIndent} x--;`);
36
+ lines.push(`${innerIndent}}`);
37
+ // Close all nested if blocks.
38
+ for (let depth = nestingDepth - 1; depth >= 0; depth -= 1) {
39
+ const indent = " ".repeat(depth + 1);
40
+ lines.push(`${indent}}`);
41
+ }
42
+ lines.push("}");
43
+ }
44
+ return lines.join("\n");
45
+ }
46
+ describe("require-branch-annotation performance on large nested-branch files (Story 004.0-DEV-BRANCH-ANNOTATIONS)", () => {
47
+ const ruleName = "traceability/require-branch-annotation";
48
+ it("[REQ-PERFORMANCE-OPTIMIZATION] analyzes a large nested-branch file within a generous time budget", () => {
49
+ const linter = new eslint_1.Linter({ configType: "eslintrc" });
50
+ linter.defineRule(ruleName, require_branch_annotation_1.default);
51
+ // 200 functions each with several nested branches gives us
52
+ // a substantial number of branch nodes without being extreme.
53
+ const source = buildLargeNestedBranchSource(200, 3);
54
+ const start = perf_hooks_1.performance.now();
55
+ const messages = linter.verify(source, {
56
+ parserOptions: { ecmaVersion: 2020, sourceType: "module" },
57
+ rules: {
58
+ [ruleName]: "error",
59
+ },
60
+ });
61
+ const durationMs = perf_hooks_1.performance.now() - start;
62
+ // Sanity check: we expect diagnostics for many branches.
63
+ expect(messages.length).toBeGreaterThan(0);
64
+ // Guardrail: keep analysis comfortably under ~5 seconds on CI hardware.
65
+ expect(durationMs).toBeLessThan(5000);
66
+ });
67
+ });
@@ -0,0 +1,74 @@
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
+ * Performance tests for valid-annotation-format on large annotated files.
8
+ *
9
+ * @supports docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md REQ-MULTILINE-SUPPORT REQ-FLEXIBLE-PARSING REQ-SYNTAX-VALIDATION
10
+ */
11
+ const eslint_1 = require("eslint");
12
+ const perf_hooks_1 = require("perf_hooks");
13
+ const valid_annotation_format_1 = __importDefault(require("../../src/rules/valid-annotation-format"));
14
+ /**
15
+ * Build a large source file containing many functions with traceability
16
+ * annotations in both line and block comments.
17
+ *
18
+ * The generated code mixes valid and invalid annotation formats to exercise
19
+ * parsing, multi-line handling, and error-reporting paths at scale without
20
+ * relying on auto-fix.
21
+ */
22
+ function buildLargeAnnotatedSource(functionCount, annotationsPerFunction) {
23
+ const lines = [];
24
+ for (let i = 0; i < functionCount; i += 1) {
25
+ // JSDoc-style block comment with multi-line @story/@req values.
26
+ lines.push("/**");
27
+ lines.push(" * @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story.md");
28
+ lines.push(" * @req REQ-FORMAT-SPECIFICATION");
29
+ lines.push(" */");
30
+ // Additional line comments with a mix of valid and intentionally
31
+ // invalid formats (missing extensions, traversal, malformed IDs).
32
+ for (let j = 0; j < annotationsPerFunction; j += 1) {
33
+ const selector = (i + j) % 4;
34
+ if (selector === 0) {
35
+ lines.push("// @story docs/stories/005.0-DEV-ANNOTATION-VALIDATION.story");
36
+ }
37
+ else if (selector === 1) {
38
+ lines.push("// @req REQ-EXAMPLE-" + i.toString(10));
39
+ }
40
+ else if (selector === 2) {
41
+ lines.push("// @story ../outside-project.story.md");
42
+ }
43
+ else {
44
+ lines.push("// @req invalid-format-id");
45
+ }
46
+ }
47
+ lines.push(`function annotated_fn_${i}() {`);
48
+ lines.push(' return "ok";\n}');
49
+ }
50
+ return lines.join("\n");
51
+ }
52
+ describe("valid-annotation-format performance on large annotated files (Story 005.0-DEV-ANNOTATION-VALIDATION)", () => {
53
+ const ruleName = "traceability/valid-annotation-format";
54
+ it("[REQ-MULTILINE-SUPPORT][REQ-FLEXIBLE-PARSING] analyzes a large annotated file within a generous time budget", () => {
55
+ const linter = new eslint_1.Linter({ configType: "eslintrc" });
56
+ linter.defineRule(ruleName, valid_annotation_format_1.default);
57
+ // 150 functions each with several annotations provides a substantial
58
+ // volume of comments and annotation patterns without being extreme.
59
+ const source = buildLargeAnnotatedSource(150, 3);
60
+ const start = perf_hooks_1.performance.now();
61
+ const messages = linter.verify(source, {
62
+ parserOptions: { ecmaVersion: 2020, sourceType: "module" },
63
+ rules: {
64
+ [ruleName]: "error",
65
+ },
66
+ });
67
+ const durationMs = perf_hooks_1.performance.now() - start;
68
+ // Sanity check: we expect diagnostics for some invalid annotations so the
69
+ // rule is definitely executing its validation logic.
70
+ expect(messages.length).toBeGreaterThan(0);
71
+ // Guardrail: keep analysis comfortably under ~5 seconds on CI hardware.
72
+ expect(durationMs).toBeLessThan(5000);
73
+ });
74
+ });
@@ -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
+ "prefer-supports-annotation",
62
63
  ];
63
64
  // Act: get actual rule names from plugin
64
65
  const actual = Object.keys(index_1.rules);
@@ -36,7 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  /**
37
37
  * Tests for: docs/stories/001.0-DEV-PLUGIN-SETUP.story.md
38
38
  * @story docs/stories/001.0-DEV-PLUGIN-SETUP.story.md
39
- * @supports docs/stories/001.0-DEV-PLUGIN-SETUP.story.md REQ-PLUGIN-STRUCTURE
39
+ * @supports docs/stories/001.0-DEV-PLUGIN-SETUP.story.md REQ-PLUGIN-STRUCTURE REQ-NPM-PACKAGE
40
40
  */
41
41
  const index_1 = __importStar(require("../src/index"));
42
42
  describe("Traceability ESLint Plugin (Story 001.0-DEV-PLUGIN-SETUP)", () => {
@@ -48,4 +48,15 @@ describe("Traceability ESLint Plugin (Story 001.0-DEV-PLUGIN-SETUP)", () => {
48
48
  expect(index_1.default.rules).toBe(index_1.rules);
49
49
  expect(index_1.default.configs).toBe(index_1.configs);
50
50
  });
51
+ it("[REQ-PLUGIN-STRUCTURE][REQ-NPM-PACKAGE] plugin exposes meta with name, namespace, and version", () => {
52
+ // Arrange
53
+ const pkg = require("../package.json");
54
+ // Act
55
+ const meta = index_1.default.meta;
56
+ // Assert
57
+ expect(meta).toBeDefined();
58
+ expect(meta.name).toBe(pkg.name);
59
+ expect(meta.version).toBe(pkg.version);
60
+ expect(meta.namespace).toBe("traceability");
61
+ });
51
62
  });