@sarj/eslint-plugin 12.0.0 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,7 +7,193 @@ var REPO_BLOB = "https://github.com/sarj-ai/standards/blob/main";
7
7
  var TESTS_DIR = "packages/typescript/tests/rules";
8
8
  var examplesPath = (name) => `${TESTS_DIR}/${name}.test.ts`;
9
9
  var examplesUrl = (name) => `${REPO_BLOB}/${examplesPath(name)}`;
10
- var createRule = ESLintUtils.RuleCreator(examplesUrl);
10
+ var KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
11
+ var MAX_SUMMARY_LENGTH = 160;
12
+ var eslintCreateRule = ESLintUtils.RuleCreator(examplesUrl);
13
+ function createRule(config) {
14
+ const { documentation, ...eslintConfig } = config;
15
+ const rule = eslintCreateRule(eslintConfig);
16
+ if (documentation !== void 0) {
17
+ Object.defineProperty(rule, "documentation", {
18
+ configurable: false,
19
+ enumerable: false,
20
+ value: nativeSpec(eslintConfig, documentation),
21
+ writable: false
22
+ });
23
+ }
24
+ return rule;
25
+ }
26
+ function documentationWarnings(rules2) {
27
+ return Object.entries(rules2).filter(([, rule]) => rule.documentation === void 0).map(([name]) => `${name}: source-owned documentation has not been migrated`).sort();
28
+ }
29
+ function publicDocumentation(rules2) {
30
+ const missing = documentationWarnings(rules2);
31
+ if (missing.length > 0) {
32
+ throw new TypeError(`cannot publish an incomplete rule catalog:
33
+ ${missing.join("\n")}`);
34
+ }
35
+ return Object.entries(rules2).sort(([left], [right]) => left.localeCompare(right)).map(([ruleId, rule]) => {
36
+ const spec = rule.documentation;
37
+ if (spec === void 0 || spec.ruleId !== ruleId) {
38
+ throw new TypeError(`${ruleId}: native documentation identity mismatch`);
39
+ }
40
+ return deepFreeze({
41
+ engine: spec.engine,
42
+ ruleId: spec.ruleId,
43
+ code: spec.code,
44
+ summary: spec.summary,
45
+ rationale: spec.rationale,
46
+ remediation: spec.remediation,
47
+ category: spec.category,
48
+ languages: spec.languages,
49
+ autofix: spec.autofix,
50
+ aliases: spec.aliases,
51
+ limitations: spec.limitations,
52
+ filePatterns: spec.filePatterns,
53
+ references: spec.references,
54
+ since: spec.since,
55
+ messageIds: spec.messageIds,
56
+ optionsSchema: spec.optionsSchema,
57
+ examples: spec.publicExamples.map(publicExample)
58
+ });
59
+ });
60
+ }
61
+ function publicExample(example) {
62
+ return {
63
+ id: example.id,
64
+ title: example.title,
65
+ outcome: example.outcome,
66
+ files: example.files.map(publicFile),
67
+ focusPath: example.focusPath,
68
+ expectedCount: example.expectedCount,
69
+ fixedFiles: (example.fixedFiles ?? []).map(publicFile)
70
+ };
71
+ }
72
+ function publicFile(file) {
73
+ return { path: file.path, source: file.source };
74
+ }
75
+ function nativeSpec(config, documentation) {
76
+ const { name, meta: meta2 } = config;
77
+ if (!KEBAB_CASE.test(name)) throw new TypeError("rule ID must be lowercase kebab-case");
78
+ for (const [label, value] of [
79
+ ["summary", documentation.summary],
80
+ ["rationale", documentation.rationale],
81
+ ["remediation", documentation.remediation]
82
+ ]) {
83
+ if (value.trim().length === 0) throw new TypeError(`rule ${label} must not be empty`);
84
+ }
85
+ if (documentation.summary.includes("\n") || documentation.summary.length > MAX_SUMMARY_LENGTH) {
86
+ throw new TypeError(`rule summary must be one line of at most ${MAX_SUMMARY_LENGTH} characters`);
87
+ }
88
+ if (documentation.summary !== meta2.docs?.description) {
89
+ throw new TypeError(`${name}: ESLint description must equal the authored documentation summary`);
90
+ }
91
+ const aliases = [...documentation.aliases ?? []];
92
+ assertUnique(aliases, "rule aliases");
93
+ if (aliases.some((alias) => !KEBAB_CASE.test(alias) || alias === name)) {
94
+ throw new TypeError("rule aliases must be historical lowercase kebab-case IDs");
95
+ }
96
+ const limitations = [...documentation.limitations ?? []];
97
+ const filePatterns = [...documentation.filePatterns ?? []];
98
+ if ([...limitations, ...filePatterns].some((value) => value.trim().length === 0)) {
99
+ throw new TypeError("rule limitations and file patterns must not be empty");
100
+ }
101
+ const references = [...documentation.references ?? []];
102
+ if (references.some((reference) => !reference.startsWith("https://"))) {
103
+ throw new TypeError("rule references must use https");
104
+ }
105
+ const examples = [...documentation.examples ?? []];
106
+ examples.forEach(validateExample);
107
+ assertUnique(examples.map((example) => example.id), "rule example IDs");
108
+ const publicOutcomes = new Set(examples.filter((example) => example.public === true).map((example) => example.outcome));
109
+ if (publicOutcomes.size > 0 && !(publicOutcomes.has("match") && publicOutcomes.has("no-match"))) {
110
+ throw new TypeError("published rule examples must include matching and non-matching cases");
111
+ }
112
+ const messageIds = Object.keys(meta2.messages).sort();
113
+ const schema = optionsSchema(meta2.schema);
114
+ const spec = {
115
+ engine: "eslint",
116
+ ruleId: name,
117
+ code: null,
118
+ key: `eslint:${name}`,
119
+ summary: documentation.summary,
120
+ rationale: documentation.rationale,
121
+ remediation: documentation.remediation,
122
+ category: documentation.category,
123
+ languages: [...documentation.languages ?? ["typescript"]],
124
+ autofix: documentation.autofix ?? "none",
125
+ aliases,
126
+ limitations,
127
+ filePatterns,
128
+ references,
129
+ since: documentation.since ?? null,
130
+ examples,
131
+ publicExamples: examples.filter((example) => example.public === true),
132
+ messageIds,
133
+ optionsSchema: schema
134
+ };
135
+ return deepFreeze(spec);
136
+ }
137
+ function optionsSchema(value) {
138
+ if (!Array.isArray(value)) return isObject(value) ? value : null;
139
+ const items = value;
140
+ if (items.length === 0) return null;
141
+ const [only] = items;
142
+ return items.length === 1 && isObject(only) ? only : { type: "array", items };
143
+ }
144
+ function isObject(value) {
145
+ return value !== null && typeof value === "object";
146
+ }
147
+ function validateExample(example) {
148
+ if (!KEBAB_CASE.test(example.id)) {
149
+ throw new TypeError("example ID must be lowercase kebab-case");
150
+ }
151
+ if (example.title.trim().length === 0) {
152
+ throw new TypeError("example title must not be empty");
153
+ }
154
+ if (!Number.isSafeInteger(example.expectedCount) || example.expectedCount < 0) {
155
+ throw new TypeError("example expected count must be a non-negative integer");
156
+ }
157
+ if (example.outcome === "match" && example.expectedCount < 1) {
158
+ throw new TypeError("matching examples must expect at least one diagnostic");
159
+ }
160
+ if (example.outcome === "no-match" && example.expectedCount !== 0) {
161
+ throw new TypeError("non-matching examples must expect zero diagnostics");
162
+ }
163
+ if (example.files.length === 0) {
164
+ throw new TypeError("example files must not be empty");
165
+ }
166
+ const paths = example.files.map((file) => file.path);
167
+ assertUnique(paths, "example file paths");
168
+ for (const file of [...example.files, ...example.fixedFiles ?? []]) {
169
+ assertSafeRelativePath(file.path, "example file path");
170
+ if (file.source.length === 0) {
171
+ throw new TypeError("example file source must not be empty");
172
+ }
173
+ }
174
+ assertSafeRelativePath(example.focusPath, "example focus path");
175
+ if (!paths.includes(example.focusPath)) {
176
+ throw new TypeError("example focus path must name one example file");
177
+ }
178
+ assertUnique((example.fixedFiles ?? []).map((file) => file.path), "fixed example file paths");
179
+ }
180
+ function assertSafeRelativePath(path, label) {
181
+ if (path.length === 0 || path.startsWith("/") || path.startsWith("\\") || /^[A-Za-z]:[\\/]/u.test(path) || path.split(/[\\/]/u).includes("..")) {
182
+ throw new TypeError(`${label} must be a safe relative path`);
183
+ }
184
+ }
185
+ function assertUnique(values, label) {
186
+ if (new Set(values).size !== values.length) {
187
+ throw new TypeError(`${label} must be unique`);
188
+ }
189
+ }
190
+ function deepFreeze(value) {
191
+ if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
192
+ for (const child of Object.values(value)) deepFreeze(child);
193
+ Object.freeze(value);
194
+ }
195
+ return value;
196
+ }
11
197
 
12
198
  // src/rules/_paths.ts
13
199
  var SCRIPT_FILE_RE = /(?:^|[\\/])scripts[\\/]|\.mjs$/;
@@ -96,6 +282,35 @@ function isScriptFile(filename) {
96
282
  }
97
283
 
98
284
  // src/rules/enforce-file-structure.ts
285
+ var enforceFileStructureDocumentation = {
286
+ summary: "Require imports before body statements and require `use server` to be the first statement.",
287
+ rationale: "Interleaved imports obscure module dependencies, while a displaced `use server` string is not an active directive.",
288
+ remediation: "Move `use server` to the first statement when present, then place imports before declarations and executable statements.",
289
+ category: "correctness",
290
+ limitations: [
291
+ "The rule skips tests and generated files, treats re-exports as neutral, and does not order body declarations."
292
+ ],
293
+ examples: [
294
+ {
295
+ id: "imports-first",
296
+ title: "Imports precede module declarations",
297
+ outcome: "no-match",
298
+ files: [{ path: "src/component.ts", source: "import { z } from 'zod';\nexport const schema = z.string();" }],
299
+ focusPath: "src/component.ts",
300
+ expectedCount: 0,
301
+ public: true
302
+ },
303
+ {
304
+ id: "import-after-declaration",
305
+ title: "An import follows a module declaration",
306
+ outcome: "match",
307
+ files: [{ path: "src/component.ts", source: "export const x = 1;\nimport { z } from 'zod';" }],
308
+ focusPath: "src/component.ts",
309
+ expectedCount: 1,
310
+ public: true
311
+ }
312
+ ]
313
+ };
99
314
  var classifyStatement = (statement) => {
100
315
  switch (statement.type) {
101
316
  case AST_NODE_TYPES.ImportDeclaration:
@@ -117,10 +332,11 @@ var isUseServerDirective = (statement) => {
117
332
  };
118
333
  var enforce_file_structure_default = createRule({
119
334
  name: "enforce-file-structure",
335
+ documentation: enforceFileStructureDocumentation,
120
336
  meta: {
121
337
  type: "suggestion",
122
338
  docs: {
123
- description: "Require `import` statements to come first, then allow step-down ordering (public API first, private helpers below) for the rest of the file. Exported statements are classified by WHAT they export \u2014 an exported interface is a declaration, an exported function is a function \u2014 so a public exported function followed by a private helper, or an exported interface among declarations, is allowed. Re-exports (`export { \u2026 } from`, `export *`, `export { \u2026 }`) are a neutral group, so generated namespace barrels pass. When a module contains a `use server` directive, it must be the first statement in the file."
339
+ description: "Require imports before body statements and require `use server` to be the first statement."
124
340
  },
125
341
  schema: [],
126
342
  messages: {
@@ -191,6 +407,41 @@ var OMITTED_AST_KEYS = /* @__PURE__ */ new Set([
191
407
  var MIN_STATEMENTS = 3;
192
408
  var MAX_NORMALIZED_STRING_LENGTH = 64;
193
409
  var TEST_MODULES = /* @__PURE__ */ new Set(["@jest/globals", "@playwright/test", "bun:test", "node:test", "vitest"]);
410
+ var duplicateTestBodyDocumentation = {
411
+ summary: "Disallow substantial sibling tests with the same body shape; express their differing inputs as a parameterized case table.",
412
+ rationale: "Copy-pasted test bodies hide the cases that differ and allow equivalent assertions to drift independently.",
413
+ remediation: "Move the varying inputs and expected values into a case table consumed by `test.each(...)` or `it.each(...)`.",
414
+ category: "testing",
415
+ limitations: [
416
+ "The rule compares substantial sibling tests within one suite and skips inline snapshots and materially different comments."
417
+ ],
418
+ examples: [
419
+ {
420
+ id: "parameterized-cases",
421
+ title: "A case table shares one test body",
422
+ outcome: "no-match",
423
+ files: [{
424
+ path: "src/user.test.ts",
425
+ source: "test.each(['a', 'b'])('parses %s', (value) => { const x = parse(value); expect(x.ok).toBe(true); expect(x.value).toBe(value); });"
426
+ }],
427
+ focusPath: "src/user.test.ts",
428
+ expectedCount: 0,
429
+ public: true
430
+ },
431
+ {
432
+ id: "copied-sibling-tests",
433
+ title: "Sibling tests repeat the same body",
434
+ outcome: "match",
435
+ files: [{
436
+ path: "src/user.test.ts",
437
+ source: "test('accepts a', () => { const result = parse('a'); expect(result.ok).toBe(true); expect(result.value).toBe('a'); });\ntest('accepts b', () => { const result = parse('b'); expect(result.ok).toBe(true); expect(result.value).toBe('b'); });"
438
+ }],
439
+ focusPath: "src/user.test.ts",
440
+ expectedCount: 1,
441
+ public: true
442
+ }
443
+ ]
444
+ };
194
445
  function rootIdentifier(callee) {
195
446
  if (callee.type === AST_NODE_TYPES2.Identifier) return callee;
196
447
  if (callee.type === AST_NODE_TYPES2.MemberExpression) return rootIdentifier(callee.object);
@@ -306,6 +557,7 @@ function normalizedLiteral(node) {
306
557
  }
307
558
  var duplicate_test_body_default = createRule({
308
559
  name: "duplicate-test-body",
560
+ documentation: duplicateTestBodyDocumentation,
309
561
  meta: {
310
562
  type: "suggestion",
311
563
  docs: {
@@ -372,12 +624,43 @@ var duplicate_test_body_default = createRule({
372
624
 
373
625
  // src/rules/no-async-callback-in-wait-for.ts
374
626
  import { AST_NODE_TYPES as AST_NODE_TYPES3 } from "@typescript-eslint/utils";
627
+ var noAsyncCallbackInWaitForDocumentation = {
628
+ summary: "Disallow async callbacks in `waitFor` to prevent swallowed promise rejections.",
629
+ rationale: "`waitFor` retries synchronous assertions; an async callback changes that contract and can hide a rejected assertion promise.",
630
+ remediation: "Remove `async` and keep the assertions inside `waitFor` synchronous.",
631
+ category: "testing",
632
+ aliases: ["no-async-callback-in-waitfor"],
633
+ limitations: [
634
+ "The rule checks inline first-argument callbacks to bare or non-computed `.waitFor` calls in test files."
635
+ ],
636
+ examples: [
637
+ {
638
+ id: "synchronous-wait-for-callback",
639
+ title: "waitFor retries a synchronous assertion",
640
+ outcome: "no-match",
641
+ files: [{ path: "src/component.test.ts", source: "it('works', async () => { await waitFor(() => expect(foo).toBe(true)); });" }],
642
+ focusPath: "src/component.test.ts",
643
+ expectedCount: 0,
644
+ public: true
645
+ },
646
+ {
647
+ id: "async-wait-for-callback",
648
+ title: "waitFor receives an async callback",
649
+ outcome: "match",
650
+ files: [{ path: "src/component.test.ts", source: "it('fails', async () => { await waitFor(async () => expect(foo).toBe(true)); });" }],
651
+ focusPath: "src/component.test.ts",
652
+ expectedCount: 1,
653
+ public: true
654
+ }
655
+ ]
656
+ };
375
657
  var isWaitForCallee = (callee) => {
376
658
  if (callee.type === AST_NODE_TYPES3.Identifier) return callee.name === "waitFor";
377
659
  return callee.type === AST_NODE_TYPES3.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES3.Identifier && callee.property.name === "waitFor";
378
660
  };
379
661
  var no_async_callback_in_wait_for_default = createRule({
380
662
  name: "no-async-callback-in-wait-for",
663
+ documentation: noAsyncCallbackInWaitForDocumentation,
381
664
  meta: {
382
665
  type: "problem",
383
666
  docs: {
@@ -410,6 +693,35 @@ var no_async_callback_in_wait_for_default = createRule({
410
693
 
411
694
  // src/rules/no-client-side-data-fetching.ts
412
695
  import { AST_NODE_TYPES as AST_NODE_TYPES4 } from "@typescript-eslint/utils";
696
+ var noClientSideDataFetchingDocumentation = {
697
+ summary: "Disallow direct data fetching inside `useEffect` or `useLayoutEffect`.",
698
+ rationale: "Effect-driven reads begin after rendering and can create request waterfalls, duplicate fetches, and loading-state layout shifts.",
699
+ remediation: "Fetch in a React Server Component or Server Action, or use a client cache such as SWR or React Query.",
700
+ category: "performance",
701
+ limitations: [
702
+ "The rule recognizes common fetch clients syntactically and exempts analytics endpoints and non-GET `fetch` calls."
703
+ ],
704
+ examples: [
705
+ {
706
+ id: "effect-without-fetch",
707
+ title: "An effect performs no data request",
708
+ outcome: "no-match",
709
+ files: [{ path: "src/users.tsx", source: "import { useEffect } from 'react'; useEffect(() => { console.log('mounted'); }, []);" }],
710
+ focusPath: "src/users.tsx",
711
+ expectedCount: 0,
712
+ public: true
713
+ },
714
+ {
715
+ id: "fetch-inside-effect",
716
+ title: "An effect starts a data request",
717
+ outcome: "match",
718
+ files: [{ path: "src/users.tsx", source: "useEffect(() => { fetch('/api/users'); }, []);" }],
719
+ focusPath: "src/users.tsx",
720
+ expectedCount: 1,
721
+ public: true
722
+ }
723
+ ]
724
+ };
413
725
  var FETCH_LIBS = /* @__PURE__ */ new Set(["axios", "ky", "superagent"]);
414
726
  var HTTP_METHOD_NAMES = /* @__PURE__ */ new Set([
415
727
  "get",
@@ -508,10 +820,11 @@ function extractUrlString(node) {
508
820
  }
509
821
  var no_client_side_data_fetching_default = createRule({
510
822
  name: "no-client-side-data-fetching",
823
+ documentation: noClientSideDataFetchingDocumentation,
511
824
  meta: {
512
825
  type: "problem",
513
826
  docs: {
514
- description: "Disallow data fetching inside `useEffect` / `useLayoutEffect`; prefer React Server Components, Server Actions, or a client-side cache (SWR / React Query)."
827
+ description: "Disallow direct data fetching inside `useEffect` or `useLayoutEffect`."
515
828
  },
516
829
  schema: [],
517
830
  messages: {
@@ -732,6 +1045,35 @@ function headTokens(source) {
732
1045
  }
733
1046
 
734
1047
  // src/rules/no-comment-cruft.ts
1048
+ var noCommentCruftDocumentation = {
1049
+ summary: "Flag commented-out code, section-banner comments, and leading file-header comment preambles.",
1050
+ rationale: "Decorative, narrated, or dead-code comments obscure the constraints and rationale that comments should preserve.",
1051
+ remediation: "Delete dead code and narration; express boundaries with named code and retain only comments that explain constraints or intent.",
1052
+ category: "maintainability",
1053
+ limitations: [
1054
+ "The rule skips generated files and conservatively preserves prose, issue references, licenses, examples, and tool directives."
1055
+ ],
1056
+ examples: [
1057
+ {
1058
+ id: "rationale-comment",
1059
+ title: "A comment explains why retry is required",
1060
+ outcome: "no-match",
1061
+ files: [{ path: "src/retry.ts", source: "// retry because the upstream API is flaky\nconst x = retry();" }],
1062
+ focusPath: "src/retry.ts",
1063
+ expectedCount: 0,
1064
+ public: true
1065
+ },
1066
+ {
1067
+ id: "region-banner",
1068
+ title: "A region comment decorates a code boundary",
1069
+ outcome: "match",
1070
+ files: [{ path: "src/helpers.ts", source: "const x = 1;\n// region helpers\nconst y = 2;" }],
1071
+ focusPath: "src/helpers.ts",
1072
+ expectedCount: 1,
1073
+ public: true
1074
+ }
1075
+ ]
1076
+ };
735
1077
  var LEADING_PREAMBLE_MIN = 4;
736
1078
  var WALL_MIN_STATEMENTS = 4;
737
1079
  var WALL_MIN_COMMENTS = 3;
@@ -1100,6 +1442,7 @@ function hasCommentedOutCode(texts, precedingProse, allowCall) {
1100
1442
  }
1101
1443
  var no_comment_cruft_default = createRule({
1102
1444
  name: "no-comment-cruft",
1445
+ documentation: noCommentCruftDocumentation,
1103
1446
  meta: {
1104
1447
  type: "suggestion",
1105
1448
  docs: {
@@ -1306,6 +1649,35 @@ var no_comment_cruft_default = createRule({
1306
1649
 
1307
1650
  // src/rules/no-conditional-in-test.ts
1308
1651
  import { AST_NODE_TYPES as AST_NODE_TYPES7 } from "@typescript-eslint/utils";
1652
+ var noConditionalInTestDocumentation = {
1653
+ summary: "Disallow test conditionals that can skip a runtime assertion or exit the test before one runs.",
1654
+ rationale: "A branch can skip the assertion that gives a test its meaning, allowing unexpected inputs to pass silently.",
1655
+ remediation: "Split each path into a separate test or use a parameterized case table with unconditional assertions.",
1656
+ category: "testing",
1657
+ limitations: [
1658
+ "The rule exempts lifecycle hooks, nested helpers, and narrow guards whose outcome is pinned by a preceding assertion."
1659
+ ],
1660
+ examples: [
1661
+ {
1662
+ id: "unconditional-assertion",
1663
+ title: "A test always executes its assertion",
1664
+ outcome: "no-match",
1665
+ files: [{ path: "src/component.test.ts", source: "it('works', () => { expect(1).toBe(1); });" }],
1666
+ focusPath: "src/component.test.ts",
1667
+ expectedCount: 0,
1668
+ public: true
1669
+ },
1670
+ {
1671
+ id: "conditional-assertion",
1672
+ title: "A branch can skip the assertion",
1673
+ outcome: "match",
1674
+ files: [{ path: "src/component.test.ts", source: "it('fails with if', () => { if (ready) { expect(value).toBe(1); } });" }],
1675
+ focusPath: "src/component.test.ts",
1676
+ expectedCount: 1,
1677
+ public: true
1678
+ }
1679
+ ]
1680
+ };
1309
1681
  var TEST_CALLERS2 = /* @__PURE__ */ new Set(["it", "test"]);
1310
1682
  var NON_TEST_MEMBERS = /* @__PURE__ */ new Set([
1311
1683
  "afterAll",
@@ -1586,6 +1958,7 @@ function isShortCircuitedAssertion(node) {
1586
1958
  }
1587
1959
  var no_conditional_in_test_default = createRule({
1588
1960
  name: "no-conditional-in-test",
1961
+ documentation: noConditionalInTestDocumentation,
1589
1962
  meta: {
1590
1963
  type: "problem",
1591
1964
  docs: {
@@ -1633,6 +2006,35 @@ var no_conditional_in_test_default = createRule({
1633
2006
 
1634
2007
  // src/rules/no-cors-wildcard-with-credentials.ts
1635
2008
  import "@typescript-eslint/utils";
2009
+ var noCorsWildcardWithCredentialsDocumentation = {
2010
+ summary: "Disallow wildcard CORS origins when credentials are enabled.",
2011
+ rationale: "Reflecting every origin while allowing credentials can let an untrusted site read authenticated cross-origin responses.",
2012
+ remediation: "Enumerate the trusted origins that may receive credentialed responses.",
2013
+ category: "security",
2014
+ limitations: [
2015
+ "The rule detects literal CORS option and header combinations within the same syntactic scope; it does not resolve runtime configuration."
2016
+ ],
2017
+ examples: [
2018
+ {
2019
+ id: "trusted-origin-with-credentials",
2020
+ title: "Credentials are limited to a trusted origin",
2021
+ outcome: "no-match",
2022
+ files: [{ path: "src/server.ts", source: "app.use(cors({ origin: 'https://app.example.com', credentials: true }));" }],
2023
+ focusPath: "src/server.ts",
2024
+ expectedCount: 0,
2025
+ public: true
2026
+ },
2027
+ {
2028
+ id: "wildcard-origin-with-credentials",
2029
+ title: "Credentials are enabled for every origin",
2030
+ outcome: "match",
2031
+ files: [{ path: "src/server.ts", source: "app.use(cors({ origin: '*', credentials: true }));" }],
2032
+ focusPath: "src/server.ts",
2033
+ expectedCount: 1,
2034
+ public: true
2035
+ }
2036
+ ]
2037
+ };
1636
2038
  var ACAO_HEADER = "access-control-allow-origin";
1637
2039
  var ACAC_HEADER = "access-control-allow-credentials";
1638
2040
  var HEADER_SET_METHODS = /* @__PURE__ */ new Set(["setheader", "set", "append"]);
@@ -1776,10 +2178,11 @@ function enclosingScope(node) {
1776
2178
  }
1777
2179
  var no_cors_wildcard_with_credentials_default = createRule({
1778
2180
  name: "no-cors-wildcard-with-credentials",
2181
+ documentation: noCorsWildcardWithCredentialsDocumentation,
1779
2182
  meta: {
1780
2183
  type: "problem",
1781
2184
  docs: {
1782
- description: 'Disallow CORS that reflects any Origin (`"*"`) while allowing credentials; any site could then read authenticated responses. Enumerate explicit trusted origins instead.'
2185
+ description: "Disallow wildcard CORS origins when credentials are enabled."
1783
2186
  },
1784
2187
  schema: [],
1785
2188
  messages: {
@@ -1986,6 +2389,35 @@ function createSqlListener(handler) {
1986
2389
  }
1987
2390
 
1988
2391
  // src/rules/no-dynamic-sql.ts
2392
+ var noDynamicSqlDocumentation = {
2393
+ summary: "Disallow runtime interpolation or concatenation in SQL passed to statement-execution methods.",
2394
+ rationale: "Embedding runtime values in SQL bypasses driver parameterization and can introduce injection defects or unstable query plans.",
2395
+ remediation: "Use SQL placeholders and pass runtime values through the driver's binding API.",
2396
+ category: "security",
2397
+ limitations: [
2398
+ "The rule recognizes SQL by syntax and configured method names; static fragments and parameterizing tagged templates are exempt."
2399
+ ],
2400
+ examples: [
2401
+ {
2402
+ id: "bound-sql-parameter",
2403
+ title: "A runtime value is bound separately",
2404
+ outcome: "no-match",
2405
+ files: [{ path: "src/users.ts", source: "db.prepare('select * from users where id = ?').bind(userId);" }],
2406
+ focusPath: "src/users.ts",
2407
+ expectedCount: 0,
2408
+ public: true
2409
+ },
2410
+ {
2411
+ id: "interpolated-sql-value",
2412
+ title: "A runtime value is interpolated into SQL",
2413
+ outcome: "match",
2414
+ files: [{ path: "src/users.ts", source: "db.prepare(`select * from users where id = '${userId}'`);" }],
2415
+ focusPath: "src/users.ts",
2416
+ expectedCount: 1,
2417
+ public: true
2418
+ }
2419
+ ]
2420
+ };
1989
2421
  var DEFAULT_METHODS = ["prepare", "exec", "query"];
1990
2422
  var CONSTANT_CASE_RE = /^[A-Z][A-Z0-9_]*$/;
1991
2423
  function isStaticFragment(expression) {
@@ -2053,10 +2485,11 @@ function statementMethodName(node, methods) {
2053
2485
  }
2054
2486
  var no_dynamic_sql_default = createRule({
2055
2487
  name: "no-dynamic-sql",
2488
+ documentation: noDynamicSqlDocumentation,
2056
2489
  meta: {
2057
2490
  type: "problem",
2058
2491
  docs: {
2059
- description: "Disallow interpolating or concatenating a runtime value into a SQL statement passed to `prepare`/`exec`/`query`; use a placeholder and bind the value."
2492
+ description: "Disallow runtime interpolation or concatenation in SQL passed to statement-execution methods."
2060
2493
  },
2061
2494
  schema: [
2062
2495
  {
@@ -2103,6 +2536,32 @@ var no_dynamic_sql_default = createRule({
2103
2536
 
2104
2537
  // src/rules/no-enum.ts
2105
2538
  import "@typescript-eslint/utils";
2539
+ var noEnumDocumentation = {
2540
+ summary: "Disallow TypeScript `enum`; use string-literal unions or `as const` objects instead.",
2541
+ rationale: "TypeScript enums emit runtime objects and numeric enums accept values outside their declared members, adding behavior where a type-only model is sufficient.",
2542
+ remediation: "Replace the enum with a string-literal union or an `as const` object and derive its value type from that object.",
2543
+ category: "maintainability",
2544
+ examples: [
2545
+ {
2546
+ id: "string-literal-union",
2547
+ title: "A string-literal union has no emitted runtime enum",
2548
+ outcome: "no-match",
2549
+ files: [{ path: "src/status.ts", source: 'type Status = "active" | "inactive";' }],
2550
+ focusPath: "src/status.ts",
2551
+ expectedCount: 0,
2552
+ public: true
2553
+ },
2554
+ {
2555
+ id: "numeric-enum",
2556
+ title: "A numeric enum emits a mutable runtime object",
2557
+ outcome: "match",
2558
+ files: [{ path: "src/status.ts", source: "enum Status { Active, Inactive }" }],
2559
+ focusPath: "src/status.ts",
2560
+ expectedCount: 1,
2561
+ public: true
2562
+ }
2563
+ ]
2564
+ };
2106
2565
  function matchesAnyPattern(filename, patterns) {
2107
2566
  for (const pattern of patterns) {
2108
2567
  const regexSource = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "::DOUBLESTAR::").replace(/\*/g, "[^/\\\\]*").replace(/::DOUBLESTAR::/g, ".*");
@@ -2114,6 +2573,7 @@ function matchesAnyPattern(filename, patterns) {
2114
2573
  }
2115
2574
  var no_enum_default = createRule({
2116
2575
  name: "no-enum",
2576
+ documentation: noEnumDocumentation,
2117
2577
  meta: {
2118
2578
  type: "suggestion",
2119
2579
  docs: {
@@ -2159,6 +2619,41 @@ var no_enum_default = createRule({
2159
2619
 
2160
2620
  // src/rules/no-fat-try-blocks.ts
2161
2621
  import { AST_NODE_TYPES as AST_NODE_TYPES10 } from "@typescript-eslint/utils";
2622
+ var noFatTryBlocksDocumentation = {
2623
+ summary: "Disallow `try` blocks containing more than three top-level operations that can throw.",
2624
+ rationale: "A broad `try` block obscures which operation failed and encourages one catch clause to recover from unrelated errors.",
2625
+ remediation: "Keep only the operations that share one recovery policy inside the `try` block and move other work outside it.",
2626
+ category: "correctness",
2627
+ limitations: [
2628
+ "The rule uses syntax to identify throwing operations and exempts generated files, finally blocks, rethrows, and terminal error boundaries."
2629
+ ],
2630
+ examples: [
2631
+ {
2632
+ id: "focused-try-block",
2633
+ title: "A try block contains three throwing operations",
2634
+ outcome: "no-match",
2635
+ files: [{
2636
+ path: "src/load.ts",
2637
+ source: "function f() { try { const a = one(); const b = two(); const c = three(); } catch (error) { handle(error); } finish(); }"
2638
+ }],
2639
+ focusPath: "src/load.ts",
2640
+ expectedCount: 0,
2641
+ public: true
2642
+ },
2643
+ {
2644
+ id: "broad-try-block",
2645
+ title: "A try block contains four throwing operations",
2646
+ outcome: "match",
2647
+ files: [{
2648
+ path: "src/load.ts",
2649
+ source: "function f() { try { const a = one(); const b = two(); const c = three(); const d = four(); } catch (error) { handle(error); } finish(); }"
2650
+ }],
2651
+ focusPath: "src/load.ts",
2652
+ expectedCount: 1,
2653
+ public: true
2654
+ }
2655
+ ]
2656
+ };
2162
2657
  var MAX_TRY_BODY_STATEMENTS = 3;
2163
2658
  var NESTED_FUNCTION_TYPES = /* @__PURE__ */ new Set([
2164
2659
  AST_NODE_TYPES10.FunctionDeclaration,
@@ -2494,10 +2989,11 @@ var handlerReturnsSuccessShaped = (handler) => subtreeMatches2(
2494
2989
  );
2495
2990
  var no_fat_try_blocks_default = createRule({
2496
2991
  name: "no-fat-try-blocks",
2992
+ documentation: noFatTryBlocksDocumentation,
2497
2993
  meta: {
2498
2994
  type: "problem",
2499
2995
  docs: {
2500
- description: "Disallow `try` blocks with more than three top-level statements that can throw \u2014 isolate the throwing statement and move non-throwing work outside."
2996
+ description: "Disallow `try` blocks containing more than three top-level operations that can throw."
2501
2997
  },
2502
2998
  schema: [],
2503
2999
  messages: {
@@ -2538,6 +3034,38 @@ var no_fat_try_blocks_default = createRule({
2538
3034
 
2539
3035
  // src/rules/no-hand-rolled-sleep.ts
2540
3036
  import { AST_NODE_TYPES as AST_NODE_TYPES11 } from "@typescript-eslint/utils";
3037
+ var noHandRolledSleepDocumentation = {
3038
+ summary: "Disallow uncancellable promisified timers and timeout arms.",
3039
+ rationale: "A timer that outlives an aborted operation or a lost promise race retains work and can keep the process alive until it fires.",
3040
+ remediation: "Use `node:timers/promises` with an abort signal for delays, or pass `AbortSignal.timeout(...)` to the timed operation.",
3041
+ category: "correctness",
3042
+ limitations: [
3043
+ "The rule skips tests, scripts, generated files, and client modules by default, and supports explicit path exemptions."
3044
+ ],
3045
+ examples: [
3046
+ {
3047
+ id: "cancellable-node-timer",
3048
+ title: "A standard-library timer accepts an abort signal",
3049
+ outcome: "no-match",
3050
+ files: [{
3051
+ path: "src/lib/queue.ts",
3052
+ source: 'import { setTimeout as sleep } from "node:timers/promises";\nawait sleep(500, undefined, { signal });'
3053
+ }],
3054
+ focusPath: "src/lib/queue.ts",
3055
+ expectedCount: 0,
3056
+ public: true
3057
+ },
3058
+ {
3059
+ id: "uncancellable-sleep",
3060
+ title: "A Promise wraps a timer without cancellation",
3061
+ outcome: "match",
3062
+ files: [{ path: "src/lib/queue.ts", source: "await new Promise((resolve) => setTimeout(resolve, 500));" }],
3063
+ focusPath: "src/lib/queue.ts",
3064
+ expectedCount: 1,
3065
+ public: true
3066
+ }
3067
+ ]
3068
+ };
2541
3069
  var GLOBAL_OBJECTS = /* @__PURE__ */ new Set([
2542
3070
  "globalThis",
2543
3071
  "window",
@@ -2617,10 +3145,11 @@ function isRaceArm(node) {
2617
3145
  }
2618
3146
  var no_hand_rolled_sleep_default = createRule({
2619
3147
  name: "no-hand-rolled-sleep",
3148
+ documentation: noHandRolledSleepDocumentation,
2620
3149
  meta: {
2621
3150
  type: "problem",
2622
3151
  docs: {
2623
- description: "Disallow hand-rolled promisified timers (`new Promise((r) => setTimeout(r, ms))`) and hand-rolled `Promise.race` timeout arms; the stdlib forms are cancellable, these are not."
3152
+ description: "Disallow uncancellable promisified timers and timeout arms."
2624
3153
  },
2625
3154
  schema: [
2626
3155
  {
@@ -2713,6 +3242,17 @@ var no_hand_rolled_sleep_default = createRule({
2713
3242
 
2714
3243
  // src/rules/no-hand-rolled-spinner.ts
2715
3244
  import { AST_NODE_TYPES as AST_NODE_TYPES12 } from "@typescript-eslint/utils";
3245
+ var noHandRolledSpinnerDocumentation = {
3246
+ summary: "Disallow intrinsic elements styled as Tailwind border-ring spinners outside the design-system implementation.",
3247
+ rationale: "One-off loading indicators duplicate a shared primitive and let accessibility and styling diverge.",
3248
+ remediation: "Render the design-system Spinner component instead.",
3249
+ category: "maintainability",
3250
+ limitations: ["Only static className values on div and span elements are inspected."],
3251
+ examples: [
3252
+ { id: "design-system-spinner", title: "Use the shared spinner", outcome: "no-match", files: [{ path: "src/loading-state.tsx", source: '<Spinner className="size-4" />' }], focusPath: "src/loading-state.tsx", expectedCount: 0, public: true },
3253
+ { id: "border-ring-spinner", title: "Do not rebuild a spinner", outcome: "match", files: [{ path: "src/loading-state.tsx", source: '<div className="size-4 animate-spin rounded-full border-2 border-t-transparent" />' }], focusPath: "src/loading-state.tsx", expectedCount: 1, public: true }
3254
+ ]
3255
+ };
2716
3256
  var DESIGN_SYSTEM_PATH = /(?:^|[/\\])components[/\\]ui[/\\]/u;
2717
3257
  var BORDER_WIDTH = /^border(?:-[0-9]+)?$/u;
2718
3258
  var TRANSPARENT_EDGE = /^border-[trbl]-transparent$/u;
@@ -2728,6 +3268,7 @@ function staticClassName(attribute) {
2728
3268
  }
2729
3269
  var no_hand_rolled_spinner_default = createRule({
2730
3270
  name: "no-hand-rolled-spinner",
3271
+ documentation: noHandRolledSpinnerDocumentation,
2731
3272
  meta: {
2732
3273
  type: "suggestion",
2733
3274
  docs: {
@@ -2765,6 +3306,17 @@ var no_hand_rolled_spinner_default = createRule({
2765
3306
 
2766
3307
  // src/rules/no-insecure-random-id.ts
2767
3308
  import "@typescript-eslint/utils";
3309
+ var noInsecureRandomIdDocumentation = {
3310
+ summary: "Disallow using `Math.random()` to generate identifiers, tokens, or secrets; use `crypto.randomUUID()` or `crypto.getRandomValues(...)` instead.",
3311
+ rationale: "Math.random is predictable and lacks the entropy required for security-sensitive values.",
3312
+ remediation: "Generate the value with crypto.randomUUID or crypto.getRandomValues.",
3313
+ category: "security",
3314
+ limitations: ["Ambiguous identifiers and test files are excluded to avoid flagging sampling and fixture data."],
3315
+ examples: [
3316
+ { id: "cryptographic-id", title: "Use the Web Crypto API", outcome: "no-match", files: [{ path: "src/session.ts", source: "const sessionToken = crypto.randomUUID();" }], focusPath: "src/session.ts", expectedCount: 0, public: true },
3317
+ { id: "predictable-token", title: "Do not derive a token from Math.random", outcome: "match", files: [{ path: "src/session.ts", source: "const sessionToken = Math.random();" }], focusPath: "src/session.ts", expectedCount: 1, public: true }
3318
+ ]
3319
+ };
2768
3320
  var STRONG_SECURITY_PATTERN = /token|secret|csrf|password|passwd|apikey|api[-_]?key|nonce|salt|uuid|authid/i;
2769
3321
  var NON_SECURITY_ID_PATTERN = /temp|tmp|cache|correlation|request|req|trace|execution|dev|hmr|mock|test|perf|marker/i;
2770
3322
  var PATH_OR_DOM_MARKER = /[\\/#]|\.[A-Za-z0-9]/;
@@ -2907,6 +3459,7 @@ function collectStaticStringParts(node, out) {
2907
3459
  }
2908
3460
  var no_insecure_random_id_default = createRule({
2909
3461
  name: "no-insecure-random-id",
3462
+ documentation: noInsecureRandomIdDocumentation,
2910
3463
  meta: {
2911
3464
  type: "problem",
2912
3465
  docs: {
@@ -2948,6 +3501,17 @@ var no_insecure_random_id_default = createRule({
2948
3501
 
2949
3502
  // src/rules/no-json-stringify-error.ts
2950
3503
  import "@typescript-eslint/utils";
3504
+ var noJsonStringifyErrorDocumentation = {
3505
+ summary: "Disallow `JSON.stringify` on an Error value; it yields `{}` because `message`/`stack` are non-enumerable.",
3506
+ rationale: "Native Error details are non-enumerable, so generic JSON serialization discards diagnostic information.",
3507
+ remediation: "Serialize explicit error fields or use an error-aware serializer.",
3508
+ category: "correctness",
3509
+ limitations: ["The rule uses local syntax and naming evidence rather than type information."],
3510
+ examples: [
3511
+ { id: "explicit-error-message", title: "Serialize an enumerable error field", outcome: "no-match", files: [{ path: "src/report.ts", source: "try { f(); } catch (err) { JSON.stringify({ error: err.message }); }" }], focusPath: "src/report.ts", expectedCount: 0, public: true },
3512
+ { id: "stringified-error", title: "Do not stringify an Error object", outcome: "match", files: [{ path: "src/report.ts", source: "try { f(); } catch (err) { JSON.stringify({ error: err }); }" }], focusPath: "src/report.ts", expectedCount: 1, public: true }
3513
+ ]
3514
+ };
2951
3515
  var ERROR_NAME_PATTERN = /^(e|err|error|ex|exc)$/i;
2952
3516
  var ERROR_PROP_PATTERN = /^(cause|lastError|error|err|exception|originalError|innerError)$/i;
2953
3517
  var SAFE_STRING_PROPS = /* @__PURE__ */ new Set(["message", "stack", "name"]);
@@ -3135,6 +3699,7 @@ function nestedExpressionSuggestsError(expression, scope) {
3135
3699
  }
3136
3700
  var no_json_stringify_error_default = createRule({
3137
3701
  name: "no-json-stringify-error",
3702
+ documentation: noJsonStringifyErrorDocumentation,
3138
3703
  meta: {
3139
3704
  type: "problem",
3140
3705
  docs: {
@@ -3187,6 +3752,47 @@ function isZodModule(source) {
3187
3752
  }
3188
3753
 
3189
3754
  // src/rules/no-impossible-zod-literal-bounds.ts
3755
+ var noImpossibleZodLiteralBoundsDocumentation = {
3756
+ summary: "Disallow same-chain literal Zod bounds whose accepted set is mathematically empty.",
3757
+ rationale: "A schema with contradictory literal bounds rejects every input, turning validation into an unreachable contract that usually reflects a typo.",
3758
+ remediation: "Choose compatible lower and upper bounds, or remove the constraint that does not express the intended domain.",
3759
+ category: "correctness",
3760
+ limitations: [
3761
+ "Only finite numeric literals in a single number, string, or array schema chain are compared.",
3762
+ "Chains with dynamic bounds, non-bound validators, transforms, pipes, or preprocessors are skipped.",
3763
+ "Test and generated files are excluded."
3764
+ ],
3765
+ examples: [
3766
+ {
3767
+ id: "compatible-number-bounds",
3768
+ title: "Allow a number admitted by both bounds",
3769
+ outcome: "no-match",
3770
+ files: [
3771
+ {
3772
+ path: "src/schema.ts",
3773
+ source: 'import { z } from "zod"; const S = z.number().gte(3).lte(3);'
3774
+ }
3775
+ ],
3776
+ focusPath: "src/schema.ts",
3777
+ expectedCount: 0,
3778
+ public: true
3779
+ },
3780
+ {
3781
+ id: "contradictory-number-bounds",
3782
+ title: "Reject an empty numeric interval",
3783
+ outcome: "match",
3784
+ files: [
3785
+ {
3786
+ path: "src/schema.ts",
3787
+ source: 'import { z } from "zod"; const S = z.number().min(5).max(4);'
3788
+ }
3789
+ ],
3790
+ focusPath: "src/schema.ts",
3791
+ expectedCount: 1,
3792
+ public: true
3793
+ }
3794
+ ]
3795
+ };
3190
3796
  var KINDS = /* @__PURE__ */ new Set(["array", "number", "string"]);
3191
3797
  var NUMBER_METHODS = /* @__PURE__ */ new Set([
3192
3798
  "gt",
@@ -3246,6 +3852,7 @@ function isOutermostCall(node) {
3246
3852
  }
3247
3853
  var no_impossible_zod_literal_bounds_default = createRule({
3248
3854
  name: "no-impossible-zod-literal-bounds",
3855
+ documentation: noImpossibleZodLiteralBoundsDocumentation,
3249
3856
  meta: {
3250
3857
  type: "problem",
3251
3858
  docs: {
@@ -3503,6 +4110,17 @@ function createLogMatcher(options = {}) {
3503
4110
  }
3504
4111
 
3505
4112
  // src/rules/no-log-only-catch.ts
4113
+ var noLogOnlyCatchDocumentation = {
4114
+ summary: "Disallow `catch` clauses that only log (or silently do nothing) and then swallow the error; rethrow or handle it instead.",
4115
+ rationale: "Swallowing an exception after logging lets execution continue as if the operation succeeded.",
4116
+ remediation: "Rethrow the error, return an explicit fallback, or perform concrete recovery.",
4117
+ category: "correctness",
4118
+ limitations: ["Documented intentional ignores, tests, and catches with observable recovery are excluded."],
4119
+ examples: [
4120
+ { id: "rethrow-after-log", title: "Preserve failure after logging", outcome: "no-match", files: [{ path: "src/task.ts", source: "try { run(); } catch (error) { console.error(error); throw error; }" }], focusPath: "src/task.ts", expectedCount: 0, public: true },
4121
+ { id: "log-and-swallow", title: "Do not only log a failure", outcome: "match", files: [{ path: "src/task.ts", source: "try { run(); } catch (error) { console.error(error); }" }], focusPath: "src/task.ts", expectedCount: 1, public: true }
4122
+ ]
4123
+ };
3506
4124
  var BENCHMARK_DIR_RE = /(?:^|[\\/])benchmarks?[\\/]/;
3507
4125
  var SINGLE_STATEMENT_HOSTS = /* @__PURE__ */ new Set([
3508
4126
  AST_NODE_TYPES14.DoWhileStatement,
@@ -3590,6 +4208,7 @@ function isSeedValue(node) {
3590
4208
  }
3591
4209
  var no_log_only_catch_default = createRule({
3592
4210
  name: "no-log-only-catch",
4211
+ documentation: noLogOnlyCatchDocumentation,
3593
4212
  meta: {
3594
4213
  type: "problem",
3595
4214
  docs: {
@@ -3771,6 +4390,17 @@ function typedFunction(node) {
3771
4390
  }
3772
4391
 
3773
4392
  // src/rules/no-long-comment.ts
4393
+ var noLongCommentDocumentation = {
4394
+ summary: "Flag unusually large unstructured prose blocks in implementation code.",
4395
+ rationale: "Large narrative comments become stale and obscure the local facts that belong beside the code.",
4396
+ remediation: "Keep only durable local constraints and express the remaining behavior in code.",
4397
+ category: "maintainability",
4398
+ limitations: ["Structured API docs, tests, scripts, generated files, and versioned dependencies are excluded."],
4399
+ examples: [
4400
+ { id: "local-fact", title: "Keep a concise local fact", outcome: "no-match", files: [{ path: "src/cache.ts", source: "// The cache is process local.\nconst cache = new Map();" }], focusPath: "src/cache.ts", expectedCount: 0, public: true },
4401
+ { id: "prose-wall", title: "Avoid an unstructured prose wall", outcome: "match", files: [{ path: "src/chart.ts", source: "/** One. Two. Three. Four. Five. Six. Seven. Eight. */\nconst chart = createChart();" }], focusPath: "src/chart.ts", expectedCount: 1, public: true }
4402
+ ]
4403
+ };
3774
4404
  var EXCESSIVE_SENTENCE_COUNT = 8;
3775
4405
  var EXCESSIVE_WORD_COUNT = 120;
3776
4406
  var PROSE_WORD_RE = /[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu;
@@ -3790,6 +4420,7 @@ function wordUnits(text) {
3790
4420
  }
3791
4421
  var no_long_comment_default = createRule({
3792
4422
  name: "no-long-comment",
4423
+ documentation: noLongCommentDocumentation,
3793
4424
  meta: {
3794
4425
  type: "suggestion",
3795
4426
  docs: { description: "Flag unusually large unstructured prose blocks in implementation code." },
@@ -3815,6 +4446,17 @@ var no_long_comment_default = createRule({
3815
4446
 
3816
4447
  // src/rules/no-generic-single-export-module.ts
3817
4448
  import { AST_NODE_TYPES as AST_NODE_TYPES17, ASTUtils as ASTUtils3 } from "@typescript-eslint/utils";
4449
+ var noGenericSingleExportModuleDocumentation = {
4450
+ summary: "Disallow generic module stems when one runtime export already names the responsibility.",
4451
+ rationale: "A generic filename hides the sole exported responsibility and makes navigation less descriptive.",
4452
+ remediation: "Rename the module after its single runtime export.",
4453
+ category: "maintainability",
4454
+ limitations: ["Only configured generic stems with exactly one public runtime export are reported."],
4455
+ examples: [
4456
+ { id: "responsibility-named-module", title: "Name the module after its export", outcome: "no-match", files: [{ path: "src/order-parser.ts", source: "export function parseOrder() { return {}; }" }], focusPath: "src/order-parser.ts", expectedCount: 0, public: true },
4457
+ { id: "generic-module-name", title: "Do not hide one export in a generic module", outcome: "match", files: [{ path: "src/utils.ts", source: "export function parseOrder() { return {}; }" }], focusPath: "src/utils.ts", expectedCount: 1, public: true }
4458
+ ]
4459
+ };
3818
4460
  var GENERIC_STEMS = /* @__PURE__ */ new Set([
3819
4461
  "base",
3820
4462
  "common",
@@ -3977,6 +4619,7 @@ function memberPropertyName(node) {
3977
4619
  }
3978
4620
  var no_generic_single_export_module_default = createRule({
3979
4621
  name: "no-generic-single-export-module",
4622
+ documentation: noGenericSingleExportModuleDocumentation,
3980
4623
  meta: {
3981
4624
  type: "suggestion",
3982
4625
  docs: { description: "Disallow generic module stems when one runtime export already names the responsibility." },
@@ -4029,10 +4672,22 @@ var no_generic_single_export_module_default = createRule({
4029
4672
 
4030
4673
  // src/rules/no-offset-pagination.ts
4031
4674
  import "@typescript-eslint/utils";
4675
+ var noOffsetPaginationDocumentation = {
4676
+ summary: "Disallow OFFSET pagination in embedded SQL; it is O(N) per page and drops or repeats rows under concurrent writes. Use a keyset cursor.",
4677
+ rationale: "Offset pagination scans skipped rows and shifts page boundaries under concurrent writes.",
4678
+ remediation: "Page with a stable ordered key and a cursor predicate.",
4679
+ category: "performance",
4680
+ limitations: ["Only embedded SQL is inspected; test files and non-pagination OFFSET syntax are excluded."],
4681
+ examples: [
4682
+ { id: "keyset-pagination", title: "Page from a stable cursor", outcome: "no-match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT id FROM runs WHERE id > ? ORDER BY id LIMIT ?`).all();" }], focusPath: "src/runs.ts", expectedCount: 0, public: true },
4683
+ { id: "offset-pagination", title: "Do not page by offset", outcome: "match", files: [{ path: "src/runs.ts", source: "db.query(`SELECT id FROM runs ORDER BY id LIMIT ? OFFSET ?`);" }], focusPath: "src/runs.ts", expectedCount: 1, public: true }
4684
+ ]
4685
+ };
4032
4686
  var OFFSET_PAGINATION = /\bOFFSET\s+(?:%s|%\(\w+\)s|\?\d*|:\w+|@\w+|\$\d+|\d+)/i;
4033
4687
  var OFFSET_GATE = /offset/i;
4034
4688
  var no_offset_pagination_default = createRule({
4035
4689
  name: "no-offset-pagination",
4690
+ documentation: noOffsetPaginationDocumentation,
4036
4691
  meta: {
4037
4692
  type: "problem",
4038
4693
  docs: {
@@ -4059,6 +4714,17 @@ var no_offset_pagination_default = createRule({
4059
4714
 
4060
4715
  // src/rules/no-positional-tuple-return.ts
4061
4716
  import { AST_NODE_TYPES as AST_NODE_TYPES18 } from "@typescript-eslint/utils";
4717
+ var noPositionalTupleReturnDocumentation = {
4718
+ summary: "Disallow returning a multi-field tuple from an exported function; return a named object so call sites cannot mismatch slots.",
4719
+ rationale: "Public tuple fields are identified only by position, so reordering can preserve types while changing meaning.",
4720
+ remediation: "Return an object whose property names describe each value.",
4721
+ category: "maintainability",
4722
+ limitations: ["Only declared multi-field tuple returns on public TypeScript surfaces are inspected."],
4723
+ examples: [
4724
+ { id: "named-object-return", title: "Return named fields", outcome: "no-match", files: [{ path: "src/download.ts", source: "export function download(): { body: string; status: number } { return impl(); }" }], focusPath: "src/download.ts", expectedCount: 0, public: true },
4725
+ { id: "tuple-return", title: "Do not expose positional fields", outcome: "match", files: [{ path: "src/download.ts", source: "export function download(): [string, number] { return impl(); }" }], focusPath: "src/download.ts", expectedCount: 1, public: true }
4726
+ ]
4727
+ };
4062
4728
  var MIN_ELEMENTS = 2;
4063
4729
  var AWAITABLE_TYPES = /* @__PURE__ */ new Set(["Promise", "PromiseLike", "Awaited", "Readonly"]);
4064
4730
  function staticMemberName2(key) {
@@ -4283,6 +4949,7 @@ function isExported(node, specifierExports) {
4283
4949
  }
4284
4950
  var no_positional_tuple_return_default = createRule({
4285
4951
  name: "no-positional-tuple-return",
4952
+ documentation: noPositionalTupleReturnDocumentation,
4286
4953
  meta: {
4287
4954
  type: "suggestion",
4288
4955
  docs: {
@@ -4379,6 +5046,17 @@ var no_positional_tuple_return_default = createRule({
4379
5046
 
4380
5047
  // src/rules/no-raw-env.ts
4381
5048
  import "@typescript-eslint/utils";
5049
+ var noRawEnvDocumentation = {
5050
+ summary: "Disallow direct `process.env` and `import.meta.env` reads outside validated boundaries.",
5051
+ rationale: "Raw environment reads are untyped and defer invalid configuration failures until use.",
5052
+ remediation: "Validate environment values at startup and import the typed configuration object.",
5053
+ category: "correctness",
5054
+ limitations: ["Host markers, assignment targets, tests, scripts, build config, and validated boundaries are excluded."],
5055
+ examples: [
5056
+ { id: "validated-environment", title: "Read validated configuration", outcome: "no-match", files: [{ path: "src/database.ts", source: "import { env } from './env.js'; const url = env.DATABASE_URL;" }], focusPath: "src/database.ts", expectedCount: 0, public: true },
5057
+ { id: "raw-environment-read", title: "Do not read raw configuration", outcome: "match", files: [{ path: "src/database.ts", source: "const url = process.env.DATABASE_URL;" }], focusPath: "src/database.ts", expectedCount: 1, public: true }
5058
+ ]
5059
+ };
4382
5060
  var CONFIG_FILE_RE = /(^|[\\/])[\w.-]+\.config\.[cm]?[jt]sx?$/;
4383
5061
  var ENV_BOUNDARY_FILE_RE = /(^|[\\/])(?:env|client-env|server-env|client-settings|server-settings)\.[cm]?[jt]sx?$/;
4384
5062
  var ENV_VALIDATION_MARKER_RE = /\bcreateEnv\s*\(|\bz\.object\s*\(|\.(?:safeParse|parse)\s*\(/;
@@ -4425,6 +5103,7 @@ function isWholeEnvSpread(node) {
4425
5103
  }
4426
5104
  var no_raw_env_default = createRule({
4427
5105
  name: "no-raw-env",
5106
+ documentation: noRawEnvDocumentation,
4428
5107
  meta: {
4429
5108
  type: "problem",
4430
5109
  docs: {
@@ -4456,6 +5135,17 @@ var no_raw_env_default = createRule({
4456
5135
 
4457
5136
  // src/rules/no-raw-fetch-outside-clients.ts
4458
5137
  import { AST_NODE_TYPES as AST_NODE_TYPES19 } from "@typescript-eslint/utils";
5138
+ var noRawFetchOutsideClientsDocumentation = {
5139
+ summary: "Disallow calling the global `fetch` outside the client layer; route outbound HTTP through a client module that owns retry, timeout and status handling.",
5140
+ rationale: "Scattered fetch calls bypass shared transport policy and are harder to stub and observe consistently.",
5141
+ remediation: "Move the request into a client module and call that abstraction from application code.",
5142
+ category: "architecture",
5143
+ limitations: ["Tests, client-layer paths, constructed handoffs, and pre-signed URL transfers are excluded."],
5144
+ examples: [
5145
+ { id: "client-call", title: "Use a client abstraction", outcome: "no-match", files: [{ path: "src/routes/handler.ts", source: "const response = await billingClient.getInvoice(id);" }], focusPath: "src/routes/handler.ts", expectedCount: 0, public: true },
5146
+ { id: "raw-fetch", title: "Do not call global fetch here", outcome: "match", files: [{ path: "src/routes/handler.ts", source: "const response = await fetch('/api/invoices');" }], focusPath: "src/routes/handler.ts", expectedCount: 1, public: true }
5147
+ ]
5148
+ };
4459
5149
  var DEFAULT_ALLOW = [
4460
5150
  "[\\\\/]clients?[\\\\/]",
4461
5151
  "-client\\.[cm]?[jt]sx?$",
@@ -4519,6 +5209,7 @@ function compile(patterns) {
4519
5209
  }
4520
5210
  var no_raw_fetch_outside_clients_default = createRule({
4521
5211
  name: "no-raw-fetch-outside-clients",
5212
+ documentation: noRawFetchOutsideClientsDocumentation,
4522
5213
  meta: {
4523
5214
  type: "problem",
4524
5215
  docs: {
@@ -4567,6 +5258,17 @@ var no_raw_fetch_outside_clients_default = createRule({
4567
5258
 
4568
5259
  // src/rules/no-restricted-library-load.ts
4569
5260
  import { AST_NODE_TYPES as AST_NODE_TYPES20, ASTUtils as ASTUtils4 } from "@typescript-eslint/utils";
5261
+ var noRestrictedLibraryLoadDocumentation = {
5262
+ summary: "Apply a configured library-replacement policy to literal dynamic imports, CommonJS loads, and TypeScript import-equals declarations.",
5263
+ rationale: "Runtime module loads can bypass the replacement policy enforced for static imports.",
5264
+ remediation: "Load the configured replacement library instead of the restricted module.",
5265
+ category: "architecture",
5266
+ limitations: ["Only literal dynamic imports, unshadowed CommonJS loads, and TypeScript import-equals declarations are checked."],
5267
+ examples: [
5268
+ { id: "static-import", title: "Static imports remain the static-import rule's responsibility", outcome: "no-match", files: [{ path: "src/client.ts", source: "import axios from 'axios';" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
5269
+ { id: "runtime-load", title: "Do not load a restricted library at runtime", outcome: "match", files: [{ path: "src/client.ts", source: "const client = require('axios');" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
5270
+ ]
5271
+ };
4570
5272
  function literalModule(node) {
4571
5273
  return node?.type === AST_NODE_TYPES20.Literal && typeof node.value === "string" ? node.value : null;
4572
5274
  }
@@ -4575,6 +5277,7 @@ function matchesModule(source, module) {
4575
5277
  }
4576
5278
  var no_restricted_library_load_default = createRule({
4577
5279
  name: "no-restricted-library-load",
5280
+ documentation: noRestrictedLibraryLoadDocumentation,
4578
5281
  meta: {
4579
5282
  type: "problem",
4580
5283
  docs: {
@@ -4670,6 +5373,17 @@ var FUNCTION_TYPES4 = /* @__PURE__ */ new Set([
4670
5373
  AST_NODE_TYPES21.FunctionExpression,
4671
5374
  AST_NODE_TYPES21.ArrowFunctionExpression
4672
5375
  ]);
5376
+ var noRepeatedStringLiteralDocumentation = {
5377
+ summary: "Disallow a long structured string literal repeated across functions; the copies drift when one is edited. Extract a module-level constant.",
5378
+ rationale: "Independent copies of a structured value can diverge and silently change behavior.",
5379
+ remediation: "Extract the repeated value to one module-level constant and reference it from each function.",
5380
+ category: "maintainability",
5381
+ limitations: ["Test files, short strings, prose, substitutions, module sources, JSX attributes, and repetition within one function are excluded."],
5382
+ examples: [
5383
+ { id: "shared-constant", title: "Share one structured value", outcome: "no-match", files: [{ path: "src/queries.ts", source: "const QUERY = 'SELECT id, status, created_at FROM candidates';\nfunction one() { return QUERY; }\nfunction two() { return QUERY; }" }], focusPath: "src/queries.ts", expectedCount: 0, public: true },
5384
+ { id: "repeated-query", title: "Do not copy a structured value across functions", outcome: "match", files: [{ path: "src/queries.ts", source: "function one() { return 'SELECT id, status, created_at FROM candidates'; }\nfunction two() { return 'SELECT id, status, created_at FROM candidates'; }" }], focusPath: "src/queries.ts", expectedCount: 1, public: true }
5385
+ ]
5386
+ };
4673
5387
  function isStructured(value) {
4674
5388
  return value.includes("\n") || SQL_KEYWORD_RE.test(value) || IDENTIFIER_RE.test(value);
4675
5389
  }
@@ -4695,6 +5409,7 @@ function isScaffolding(node) {
4695
5409
  }
4696
5410
  var no_repeated_string_literal_default = createRule({
4697
5411
  name: "no-repeated-string-literal",
5412
+ documentation: noRepeatedStringLiteralDocumentation,
4698
5413
  meta: {
4699
5414
  type: "suggestion",
4700
5415
  docs: {
@@ -4780,6 +5495,17 @@ var NON_ASCII_LETTER_RE = /[^\p{ASCII}\p{N}\p{P}\p{Z}]/u;
4780
5495
  var WALL_NARRATION_RE2 = /^(?:(?:\d+[.)]|(?:phase|step)\s+\d+\s*:?)\s*)?(?:add|build|call|check|compute|copy|count|create|fetch|filter|find|get|handle|load|map|merge|parse|process|read|remove|return|save|send|set|sort|store|update|validate|write)(?:s|es|d|ed|ing)?\b/i;
4781
5496
  var WALL_CLUSTER_MAX_LINE_GAP = 8;
4782
5497
  var WALL_CLUSTER_MIN_COMMENTS = 3;
5498
+ var noRestatedCommentDocumentation = {
5499
+ summary: "Flag a single-line comment whose every word already appears on the statement below it.",
5500
+ rationale: "A comment that only repeats code adds no context and can become stale independently.",
5501
+ remediation: "Delete the comment or replace it with the reason, constraint, or consequence absent from the code.",
5502
+ category: "maintainability",
5503
+ limitations: ["Directives, protected references, questions, multi-line prose, comments with novel content, and generated files are excluded."],
5504
+ examples: [
5505
+ { id: "reason-comment", title: "Keep the reason the code cannot express", outcome: "no-match", files: [{ path: "src/cache.ts", source: "// Serialize because the cache key is stable across deploys.\nconst key = serialize(input);" }], focusPath: "src/cache.ts", expectedCount: 0, public: true },
5506
+ { id: "restated-comment", title: "Remove a comment that repeats the statement", outcome: "match", files: [{ path: "src/cache.ts", source: "// Serialize key\nconst key = serialize(input);" }], focusPath: "src/cache.ts", expectedCount: 1, public: true }
5507
+ ]
5508
+ };
4783
5509
  function areAdjacentLineComments2(a, b) {
4784
5510
  return a !== void 0 && b !== void 0 && a.type === "Line" && b.type === "Line" && b.loc.start.line === a.loc.end.line + 1;
4785
5511
  }
@@ -4794,6 +5520,7 @@ function headsSiblingRun(node) {
4794
5520
  }
4795
5521
  var no_restated_comment_default = createRule({
4796
5522
  name: "no-restated-comment",
5523
+ documentation: noRestatedCommentDocumentation,
4797
5524
  meta: {
4798
5525
  type: "suggestion",
4799
5526
  docs: {
@@ -4879,6 +5606,19 @@ var no_restated_comment_default = createRule({
4879
5606
 
4880
5607
  // src/rules/no-restated-jsdoc.ts
4881
5608
  import { AST_NODE_TYPES as AST_NODE_TYPES23 } from "@typescript-eslint/utils";
5609
+ var noRestatedJsdocDocumentation = {
5610
+ summary: "Flag a JSDoc block whose description and tags only re-spell the signature they document.",
5611
+ rationale: "Signature-only JSDoc duplicates type information and drifts without helping callers.",
5612
+ remediation: "Delete the block or document behavior, constraints, failures, or context the signature cannot express.",
5613
+ category: "maintainability",
5614
+ aliases: ["jsdoc-restates-signature"],
5615
+ autofix: "suggestion",
5616
+ limitations: ["Generated files, detached blocks, unknown tags, empty blocks, and JSDoc with information absent from the signature are excluded."],
5617
+ examples: [
5618
+ { id: "behavioral-jsdoc", title: "Document behavior absent from the signature", outcome: "no-match", files: [{ path: "src/users.ts", source: "/** Get the user while bypassing the read replica. */\nexport function getUser(id: string) { return id; }" }], focusPath: "src/users.ts", expectedCount: 0, public: true },
5619
+ { id: "signature-jsdoc", title: "Remove JSDoc that only repeats the signature", outcome: "match", files: [{ path: "src/users.ts", source: "/** Get the user by id. */\nexport function getUserById(id: string) { return id; }" }], focusPath: "src/users.ts", expectedCount: 1, public: true }
5620
+ ]
5621
+ };
4882
5622
  var MODELLED_TAGS = /* @__PURE__ */ new Set([
4883
5623
  "arg",
4884
5624
  "argument",
@@ -4978,6 +5718,7 @@ function tokensOf(names) {
4978
5718
  }
4979
5719
  var no_restated_jsdoc_default = createRule({
4980
5720
  name: "no-restated-jsdoc",
5721
+ documentation: noRestatedJsdocDocumentation,
4981
5722
  meta: {
4982
5723
  type: "suggestion",
4983
5724
  hasSuggestions: true,
@@ -5209,6 +5950,17 @@ function isAuthSecretName(identifier) {
5209
5950
  }
5210
5951
 
5211
5952
  // src/rules/no-secret-in-log.ts
5953
+ var noSecretInLogDocumentation = {
5954
+ summary: "Disallow passing a secret-named value or a raw request/response blob to a logging call; both leak to log sinks. Redact or omit.",
5955
+ rationale: "Logs are widely retained and distributed, so credentials and raw bodies can become durable data leaks.",
5956
+ remediation: "Omit the value or log an explicitly redacted, truncated, or derived non-sensitive field.",
5957
+ category: "security",
5958
+ limitations: ["Detection uses configurable logger names and statically recognizable secret names, raw-body names, and redaction markers."],
5959
+ examples: [
5960
+ { id: "redacted-secret", title: "Log an explicitly redacted value", outcome: "no-match", files: [{ path: "src/auth.ts", source: "logger.info('auth', { tokenPrefix });" }], focusPath: "src/auth.ts", expectedCount: 0, public: true },
5961
+ { id: "logged-secret", title: "Do not send a secret to logs", outcome: "match", files: [{ path: "src/auth.ts", source: "logger.error('auth failed', { token });" }], focusPath: "src/auth.ts", expectedCount: 1, public: true }
5962
+ ]
5963
+ };
5212
5964
  var LOG_INNOCUOUS_WORDS = /* @__PURE__ */ new Set([
5213
5965
  ...INNOCUOUS_WORDS,
5214
5966
  "name",
@@ -5328,6 +6080,7 @@ function propertyKeyName2(prop) {
5328
6080
  }
5329
6081
  var no_secret_in_log_default = createRule({
5330
6082
  name: "no-secret-in-log",
6083
+ documentation: noSecretInLogDocumentation,
5331
6084
  meta: {
5332
6085
  type: "problem",
5333
6086
  docs: {
@@ -5402,6 +6155,17 @@ var no_secret_in_log_default = createRule({
5402
6155
 
5403
6156
  // src/rules/no-select-star.ts
5404
6157
  import "@typescript-eslint/utils";
6158
+ var noSelectStarDocumentation = {
6159
+ summary: "Disallow SELECT * in embedded SQL; it over-fetches and leaves the row contract implicit, so a schema change breaks row parsing silently.",
6160
+ rationale: "Wildcard projections couple row shape and query cost to unrelated schema changes.",
6161
+ remediation: "List every required column explicitly in the projection.",
6162
+ category: "correctness",
6163
+ limitations: ["Only statically visible embedded SQL is checked; function arguments such as COUNT(*) and stars inside EXISTS are excluded."],
6164
+ examples: [
6165
+ { id: "explicit-projection", title: "Select the required columns", outcome: "no-match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT id, status FROM runs`).all();" }], focusPath: "src/runs.ts", expectedCount: 0, public: true },
6166
+ { id: "wildcard-projection", title: "Do not select every column", outcome: "match", files: [{ path: "src/runs.ts", source: "db.prepare(`SELECT * FROM runs`).all();" }], focusPath: "src/runs.ts", expectedCount: 1, public: true }
6167
+ ]
6168
+ };
5405
6169
  var QUERY_SHAPE = /\bSELECT\b[\s\S]*?\bFROM\b/i;
5406
6170
  var SELECT_KEYWORD = /\bSELECT\b/gi;
5407
6171
  var FROM_KEYWORD = /^FROM\b/i;
@@ -5443,6 +6207,7 @@ function isProjectionStar(sql, pos) {
5443
6207
  }
5444
6208
  var no_select_star_default = createRule({
5445
6209
  name: "no-select-star",
6210
+ documentation: noSelectStarDocumentation,
5446
6211
  meta: {
5447
6212
  type: "problem",
5448
6213
  docs: {
@@ -5469,6 +6234,17 @@ var no_select_star_default = createRule({
5469
6234
 
5470
6235
  // src/rules/no-sentinel-return-on-catch.ts
5471
6236
  import { AST_NODE_TYPES as AST_NODE_TYPES24 } from "@typescript-eslint/utils";
6237
+ var noSentinelReturnOnCatchDocumentation = {
6238
+ summary: "Disallow swallowing a caught error by returning an empty sentinel unless the error is handled or the sentinel is part of the function contract.",
6239
+ rationale: "An unreported fallback makes operational failure indistinguishable from a legitimate empty result.",
6240
+ remediation: "Rethrow, report the error before returning, or model expected absence with an explicit predicate, safe-parse, or result contract.",
6241
+ category: "correctness",
6242
+ limitations: ["Recognized predicate, safe-parse, normal-path sentinel, deliberate parse, generated-client, and configured logging patterns are excluded."],
6243
+ examples: [
6244
+ { id: "reported-fallback", title: "Report an error before returning a fallback", outcome: "no-match", files: [{ path: "src/load.ts", source: "function load() { try { return read(); } catch (error) { logger.warn('load failed', error); return null; } }" }], focusPath: "src/load.ts", expectedCount: 0, public: true },
6245
+ { id: "silent-fallback", title: "Do not turn an unreported error into absence", outcome: "match", files: [{ path: "src/load.ts", source: "function load() { try { return read(); } catch { return null; } }" }], focusPath: "src/load.ts", expectedCount: 1, public: true }
6246
+ ]
6247
+ };
5472
6248
  function unwrapSentinelExpression(arg) {
5473
6249
  let current = arg;
5474
6250
  while (current?.type === AST_NODE_TYPES24.TSAsExpression || current?.type === AST_NODE_TYPES24.TSTypeAssertion || current?.type === AST_NODE_TYPES24.TSSatisfiesExpression) {
@@ -5792,10 +6568,11 @@ function isWithin(node, ancestor) {
5792
6568
  }
5793
6569
  var no_sentinel_return_on_catch_default = createRule({
5794
6570
  name: "no-sentinel-return-on-catch",
6571
+ documentation: noSentinelReturnOnCatchDocumentation,
5795
6572
  meta: {
5796
6573
  type: "problem",
5797
6574
  docs: {
5798
- description: "Disallow swallowing a caught error by returning an empty sentinel (`null`, `undefined`, `false`, `[]`, `{}`) as the final statement of a `catch` block, unless the error is logged/reported or the sentinel is the declared safe-parse/predicate contract."
6575
+ description: "Disallow swallowing a caught error by returning an empty sentinel unless the error is handled or the sentinel is part of the function contract."
5799
6576
  },
5800
6577
  schema: [
5801
6578
  {
@@ -5873,6 +6650,17 @@ var no_sentinel_return_on_catch_default = createRule({
5873
6650
 
5874
6651
  // src/rules/no-silent-promise-catch.ts
5875
6652
  import { AST_NODE_TYPES as AST_NODE_TYPES25 } from "@typescript-eslint/utils";
6653
+ var noSilentPromiseCatchDocumentation = {
6654
+ summary: "Disallow `.catch()` and second-argument `.then()` handlers that silently swallow a rejection; log, rethrow, or handle the error.",
6655
+ rationale: "A swallowed rejection hides failures and gives callers an indistinguishable fallback value.",
6656
+ remediation: "Log, rethrow, or explicitly recover from the rejection; explain intentional teardown suppression.",
6657
+ category: "correctness",
6658
+ limitations: ["Test files, teardown calls, explanatory comments, non-function handlers, and handlers that consume or report the error are excluded."],
6659
+ examples: [
6660
+ { id: "reported-rejection", title: "Report the rejection", outcome: "no-match", files: [{ path: "src/load.ts", source: "load().catch((error) => logger.error({ error }, 'load failed'));" }], focusPath: "src/load.ts", expectedCount: 0, public: true },
6661
+ { id: "silent-rejection", title: "Do not swallow the rejection", outcome: "match", files: [{ path: "src/load.ts", source: "load().catch(() => null);" }], focusPath: "src/load.ts", expectedCount: 1, public: true }
6662
+ ]
6663
+ };
5876
6664
  function isBodyParseCall(node) {
5877
6665
  return node.type === AST_NODE_TYPES25.CallExpression && node.arguments.length === 0 && node.callee.type === AST_NODE_TYPES25.MemberExpression && !node.callee.computed && node.callee.property.type === AST_NODE_TYPES25.Identifier && (node.callee.property.name === "json" || node.callee.property.name === "text");
5878
6666
  }
@@ -5927,6 +6715,7 @@ function isSilentExpression(node) {
5927
6715
  }
5928
6716
  var no_silent_promise_catch_default = createRule({
5929
6717
  name: "no-silent-promise-catch",
6718
+ documentation: noSilentPromiseCatchDocumentation,
5930
6719
  meta: {
5931
6720
  type: "problem",
5932
6721
  docs: {
@@ -5996,6 +6785,18 @@ var no_silent_promise_catch_default = createRule({
5996
6785
 
5997
6786
  // src/rules/no-sleep-in-test-body.ts
5998
6787
  import { AST_NODE_TYPES as AST_NODE_TYPES26 } from "@typescript-eslint/utils";
6788
+ var noSleepInTestBodyDocumentation = {
6789
+ summary: "Disallow a fixed timed sleep directly in a test body; it flakes under CI load. Synchronize on the signal or use fake timers.",
6790
+ rationale: "Wall-clock delays make test correctness depend on scheduler and machine speed.",
6791
+ remediation: "Await the observable signal or advance deterministic fake timers.",
6792
+ category: "testing",
6793
+ filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**", "**/__tests__/**"],
6794
+ limitations: ["Only fixed nonzero sleeps directly inside test and per-test hook callbacks are checked; nested fakes and parameterized delays are excluded."],
6795
+ examples: [
6796
+ { id: "fake-timer", title: "Advance time deterministically", outcome: "no-match", files: [{ path: "src/retry.test.ts", source: "it('retries', async () => { vi.useFakeTimers(); const result = retry(); await vi.advanceTimersByTimeAsync(50); await result; });" }], focusPath: "src/retry.test.ts", expectedCount: 0, public: true },
6797
+ { id: "fixed-sleep", title: "Do not wait for wall-clock time", outcome: "match", files: [{ path: "src/retry.test.ts", source: "it('retries', async () => { await sleep(50); expect(done()).toBe(true); });" }], focusPath: "src/retry.test.ts", expectedCount: 1, public: true }
6798
+ ]
6799
+ };
5999
6800
  var SLEEP_HELPERS = /* @__PURE__ */ new Set(["sleep", "delay", "wait", "pause"]);
6000
6801
  var TEST_CALLERS3 = /* @__PURE__ */ new Set([
6001
6802
  "it",
@@ -6071,6 +6872,7 @@ function testCallerName2(callee) {
6071
6872
  }
6072
6873
  var no_sleep_in_test_body_default = createRule({
6073
6874
  name: "no-sleep-in-test-body",
6875
+ documentation: noSleepInTestBodyDocumentation,
6074
6876
  meta: {
6075
6877
  type: "problem",
6076
6878
  docs: {
@@ -6116,6 +6918,17 @@ var DEFAULT_METHODS2 = [
6116
6918
  "getWithMetadata"
6117
6919
  ];
6118
6920
  var MIN_ARGUMENTS = /* @__PURE__ */ new Map([["put", 2]]);
6921
+ var noStorageInStatelessModulesDocumentation = {
6922
+ summary: "Disallow SQL or key/value access inside configured stateless modules; derive state from a system of record instead.",
6923
+ rationale: "Private storage in a stateless workflow creates another source of truth that can silently diverge.",
6924
+ remediation: "Read from the system of record or derive state from an artifact the workflow already produces.",
6925
+ category: "architecture",
6926
+ limitations: ["The rule is disabled until module path patterns are configured and recognizes only configured storage method names."],
6927
+ examples: [
6928
+ { id: "system-of-record", title: "Read from the system of record", outcome: "no-match", files: [{ path: "src/engineer-digest/post.ts", source: "const issues = await linear.listIssues();" }], focusPath: "src/engineer-digest/post.ts", expectedCount: 0, public: true },
6929
+ { id: "private-storage", title: "Do not write private state in a stateless module", outcome: "match", files: [{ path: "src/engineer-digest/post.ts", source: "await kv.put('digest:last', timestamp);" }], focusPath: "src/engineer-digest/post.ts", expectedCount: 1, public: true }
6930
+ ]
6931
+ };
6119
6932
  function compile2(patterns) {
6120
6933
  const compiled = [];
6121
6934
  for (const pattern of patterns) {
@@ -6142,10 +6955,11 @@ function storageMethodName(node, methods) {
6142
6955
  }
6143
6956
  var no_storage_in_stateless_modules_default = createRule({
6144
6957
  name: "no-storage-in-stateless-modules",
6958
+ documentation: noStorageInStatelessModulesDocumentation,
6145
6959
  meta: {
6146
6960
  type: "problem",
6147
6961
  docs: {
6148
- description: "Disallow SQL or key/value access inside modules a team has declared stateless; derive state from the systems of record instead. No-op until `modules` is configured."
6962
+ description: "Disallow SQL or key/value access inside configured stateless modules; derive state from a system of record instead."
6149
6963
  },
6150
6964
  schema: [
6151
6965
  {
@@ -6197,6 +7011,35 @@ var no_storage_in_stateless_modules_default = createRule({
6197
7011
 
6198
7012
  // src/rules/no-string-concat-in-loop.ts
6199
7013
  import "@typescript-eslint/utils";
7014
+ var noStringConcatInLoopDocumentation = {
7015
+ summary: "Disallow O(n^2) string building via `+=` on a string variable inside a loop; push parts to an array and `join` instead.",
7016
+ rationale: "Repeatedly rebuilding a growing string can copy all prior content on each iteration, making total work grow quadratically.",
7017
+ remediation: "Collect each fragment in an array, then join the fragments after the loop.",
7018
+ category: "performance",
7019
+ limitations: [
7020
+ "Only local identifiers initialized with a string or template literal and accumulated in a loop body are inspected."
7021
+ ],
7022
+ examples: [
7023
+ {
7024
+ id: "join-fragments",
7025
+ title: "Join collected fragments after the loop",
7026
+ outcome: "no-match",
7027
+ files: [{ path: "src/render.ts", source: 'const parts = []; for (const item of items) { parts.push(item); } const output = parts.join("");' }],
7028
+ focusPath: "src/render.ts",
7029
+ expectedCount: 0,
7030
+ public: true
7031
+ },
7032
+ {
7033
+ id: "rebuild-string",
7034
+ title: "Do not rebuild a growing string in a loop",
7035
+ outcome: "match",
7036
+ files: [{ path: "src/render.ts", source: "let output = ''; for (const item of items) { output = `${output}${item}`; }" }],
7037
+ focusPath: "src/render.ts",
7038
+ expectedCount: 1,
7039
+ public: true
7040
+ }
7041
+ ]
7042
+ };
6200
7043
  var LOOP_NODE_TYPES = /* @__PURE__ */ new Set([
6201
7044
  "ForStatement",
6202
7045
  "ForOfStatement",
@@ -6288,6 +7131,7 @@ function enclosingLoop(node) {
6288
7131
  }
6289
7132
  var no_string_concat_in_loop_default = createRule({
6290
7133
  name: "no-string-concat-in-loop",
7134
+ documentation: noStringConcatInLoopDocumentation,
6291
7135
  meta: {
6292
7136
  type: "suggestion",
6293
7137
  docs: {
@@ -6348,6 +7192,35 @@ var no_string_concat_in_loop_default = createRule({
6348
7192
 
6349
7193
  // src/rules/no-tautological-expect.ts
6350
7194
  import { AST_NODE_TYPES as AST_NODE_TYPES28 } from "@typescript-eslint/utils";
7195
+ var noTautologicalExpectDocumentation = {
7196
+ summary: "Disallow an assertion whose operands are all literals; its outcome is fixed before the code runs, so it can never fail.",
7197
+ rationale: "An assertion determined entirely by literals does not observe the code under test and can keep passing after that code is removed.",
7198
+ remediation: "Assert on a value produced by the behavior under test, or remove the assertion.",
7199
+ category: "testing",
7200
+ limitations: [
7201
+ "Only direct supported `expect` matcher calls in recognized test files are inspected."
7202
+ ],
7203
+ examples: [
7204
+ {
7205
+ id: "produced-value",
7206
+ title: "Assert on a produced value",
7207
+ outcome: "no-match",
7208
+ files: [{ path: "src/add.test.ts", source: "it('adds', () => { expect(add(1, 1)).toBe(2); });" }],
7209
+ focusPath: "src/add.test.ts",
7210
+ expectedCount: 0,
7211
+ public: true
7212
+ },
7213
+ {
7214
+ id: "literal-only-assertion",
7215
+ title: "Do not compare identical literals",
7216
+ outcome: "match",
7217
+ files: [{ path: "src/add.test.ts", source: "it('works', () => { expect(true).toBe(true); });" }],
7218
+ focusPath: "src/add.test.ts",
7219
+ expectedCount: 1,
7220
+ public: true
7221
+ }
7222
+ ]
7223
+ };
6351
7224
  var EQUALITY_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual", "toStrictEqual"]);
6352
7225
  var ZERO_ARG_MATCHERS = /* @__PURE__ */ new Set([
6353
7226
  "toBeDefined",
@@ -6386,6 +7259,7 @@ function expectOperand(callee) {
6386
7259
  }
6387
7260
  var no_tautological_expect_default = createRule({
6388
7261
  name: "no-tautological-expect",
7262
+ documentation: noTautologicalExpectDocumentation,
6389
7263
  meta: {
6390
7264
  type: "problem",
6391
7265
  docs: {
@@ -6446,8 +7320,36 @@ var no_tautological_expect_default = createRule({
6446
7320
  });
6447
7321
 
6448
7322
  // src/rules/no-typed-doc-sections.ts
7323
+ var noTypedDocSectionsDocumentation = {
7324
+ summary: "Reject typed-signature repetition while preserving behavior that types cannot express.",
7325
+ rationale: "Parameter and return tags repeat typed signatures and can drift without adding runtime behavior or constraints.",
7326
+ remediation: "Remove repeated parameter and return tags; retain documentation for behavior, failures, and external contracts.",
7327
+ category: "maintainability",
7328
+ limitations: ["Parameter and return tags are reported only when the documented function has corresponding explicit TypeScript types."],
7329
+ examples: [
7330
+ {
7331
+ id: "behavioral-documentation",
7332
+ title: "Keep behavior that the signature cannot express",
7333
+ outcome: "no-match",
7334
+ files: [{ path: "src/client.ts", source: "/** Retries when the vendor returns 429. */\nexport function fetchValue(id: string): number { return 1; }" }],
7335
+ focusPath: "src/client.ts",
7336
+ expectedCount: 0,
7337
+ public: true
7338
+ },
7339
+ {
7340
+ id: "repeated-typed-sections",
7341
+ title: "Do not restate typed parameters and returns",
7342
+ outcome: "match",
7343
+ files: [{ path: "src/client.ts", source: "/** @param id external identifier\n * @returns the value\n */\nexport function fetchValue(id: string): number { return 1; }" }],
7344
+ focusPath: "src/client.ts",
7345
+ expectedCount: 1,
7346
+ public: true
7347
+ }
7348
+ ]
7349
+ };
6449
7350
  var no_typed_doc_sections_default = createRule({
6450
7351
  name: "no-typed-doc-sections",
7352
+ documentation: noTypedDocSectionsDocumentation,
6451
7353
  meta: {
6452
7354
  type: "suggestion",
6453
7355
  docs: { description: "Reject typed-signature repetition while preserving behavior that types cannot express." },
@@ -6472,6 +7374,34 @@ var no_typed_doc_sections_default = createRule({
6472
7374
 
6473
7375
  // src/rules/no-trailing-value-narration.ts
6474
7376
  import "@typescript-eslint/utils";
7377
+ var noTrailingValueNarrationDocumentation = {
7378
+ summary: "Flag a trailing comment that repeats the line's numeric value only to name its unit.",
7379
+ rationale: "A repeated value can disagree with the expression after either the code or comment changes.",
7380
+ remediation: "Put the unit in the identifier and keep comments only when they explain a constraint or non-obvious conversion.",
7381
+ category: "maintainability",
7382
+ aliases: ["trailing-value-narration"],
7383
+ limitations: ["Only trailing comments with numeric values and recognized unit words are inspected."],
7384
+ examples: [
7385
+ {
7386
+ id: "explain-constraint",
7387
+ title: "Explain a domain constraint",
7388
+ outcome: "no-match",
7389
+ files: [{ path: "src/timeouts.ts", source: "const timeout = 5 * 60; // 5 minutes for cold starts" }],
7390
+ focusPath: "src/timeouts.ts",
7391
+ expectedCount: 0,
7392
+ public: true
7393
+ },
7394
+ {
7395
+ id: "repeat-duration",
7396
+ title: "Do not narrate the numeric duration",
7397
+ outcome: "match",
7398
+ files: [{ path: "src/timeouts.ts", source: "const staleTime = 5 * 60 * 1000; // 5 minutes" }],
7399
+ focusPath: "src/timeouts.ts",
7400
+ expectedCount: 1,
7401
+ public: true
7402
+ }
7403
+ ]
7404
+ };
6475
7405
  var NUMBER_RE = /(?<![\w.])(\d+(?:\.\d+)?)(?![\w.])/g;
6476
7406
  var WORD_RE3 = /[A-Za-z]+(?:'[a-z]+)?|\d+(?:\.\d+)?/g;
6477
7407
  var UNIT_WORDS = /* @__PURE__ */ new Set([
@@ -6558,6 +7488,7 @@ function numbersIn(text) {
6558
7488
  }
6559
7489
  var no_trailing_value_narration_default = createRule({
6560
7490
  name: "no-trailing-value-narration",
7491
+ documentation: noTrailingValueNarrationDocumentation,
6561
7492
  meta: {
6562
7493
  type: "suggestion",
6563
7494
  docs: {
@@ -6734,6 +7665,33 @@ function bodyOf(member) {
6734
7665
  }
6735
7666
 
6736
7667
  // src/rules/no-declaration-comment-wall.ts
7668
+ var noDeclarationCommentWallDocumentation = {
7669
+ summary: "Flag an enum body or class body whose member comments mostly re-spell the members' own names.",
7670
+ rationale: "A dense block of repetitive member comments obscures the few comments that add information and drifts with renamed members.",
7671
+ remediation: "Delete comments that restate member names and retain comments that explain constraints, lifecycle, or behavior.",
7672
+ category: "maintainability",
7673
+ limitations: ["Only enum and class bodies meeting the configured comment-count and restatement-ratio thresholds are reported."],
7674
+ examples: [
7675
+ {
7676
+ id: "uncommented-members",
7677
+ title: "Let clear member names stand alone",
7678
+ outcome: "no-match",
7679
+ files: [{ path: "src/status.ts", source: "enum Status { Pending = 'pending', Done = 'done', Failed = 'failed' }" }],
7680
+ focusPath: "src/status.ts",
7681
+ expectedCount: 0,
7682
+ public: true
7683
+ },
7684
+ {
7685
+ id: "restated-enum-members",
7686
+ title: "Do not restate every enum member",
7687
+ outcome: "match",
7688
+ files: [{ path: "src/status.ts", source: "enum Status {\n /** The pending status. */\n Pending = 'pending',\n /** The finished status. */\n Finished = 'finished',\n /** The failed status. */\n Failed = 'failed',\n}" }],
7689
+ focusPath: "src/status.ts",
7690
+ expectedCount: 1,
7691
+ public: true
7692
+ }
7693
+ ]
7694
+ };
6737
7695
  function named(node) {
6738
7696
  switch (node.type) {
6739
7697
  case AST_NODE_TYPES30.TSEnumMember:
@@ -6749,6 +7707,7 @@ function named(node) {
6749
7707
  }
6750
7708
  var no_declaration_comment_wall_default = createRule({
6751
7709
  name: "no-declaration-comment-wall",
7710
+ documentation: noDeclarationCommentWallDocumentation,
6752
7711
  meta: {
6753
7712
  type: "suggestion",
6754
7713
  docs: {
@@ -6842,6 +7801,33 @@ var no_declaration_comment_wall_default = createRule({
6842
7801
 
6843
7802
  // src/rules/no-union-in-comment.ts
6844
7803
  import { AST_NODE_TYPES as AST_NODE_TYPES31 } from "@typescript-eslint/utils";
7804
+ var noUnionInCommentDocumentation = {
7805
+ summary: "Flag a comment that lists a `string` field's allowed values instead of the type listing them.",
7806
+ rationale: "A comment cannot prevent callers from supplying strings outside the listed set, and the list can drift from runtime behavior.",
7807
+ remediation: "Move the allowed values into a string-literal union and remove the redundant comment.",
7808
+ category: "correctness",
7809
+ limitations: ["Only bare quoted-value lists attached to supported string declarations and schema-builder fields are inspected."],
7810
+ examples: [
7811
+ {
7812
+ id: "literal-union",
7813
+ title: "Encode allowed values in the type",
7814
+ outcome: "no-match",
7815
+ files: [{ path: "src/record.ts", source: "interface R { kind: 'aa' | 'bb'; }" }],
7816
+ focusPath: "src/record.ts",
7817
+ expectedCount: 0,
7818
+ public: true
7819
+ },
7820
+ {
7821
+ id: "comment-only-union",
7822
+ title: "Do not leave allowed values in a comment",
7823
+ outcome: "match",
7824
+ files: [{ path: "src/record.ts", source: "interface R {\n kind: string; // 'aa' | 'bb'\n}" }],
7825
+ focusPath: "src/record.ts",
7826
+ expectedCount: 1,
7827
+ public: true
7828
+ }
7829
+ ]
7830
+ };
6845
7831
  var MAX_LITERAL_LENGTH = 28;
6846
7832
  var LITERAL = String.raw`(?:'[^'\n]*'|"[^"\n]*"|\`[^\`\n]*\`)`;
6847
7833
  var LEAD_IN_RE2 = /^(?:one of|either|values?|allowed(?: values)?|options?|possible(?: values)?)\s*[:=-]?\s*/i;
@@ -6930,6 +7916,7 @@ function unionLiterals(body2) {
6930
7916
  }
6931
7917
  var no_union_in_comment_default = createRule({
6932
7918
  name: "no-union-in-comment",
7919
+ documentation: noUnionInCommentDocumentation,
6933
7920
  meta: {
6934
7921
  type: "suggestion",
6935
7922
  docs: {
@@ -6990,11 +7977,39 @@ var no_union_in_comment_default = createRule({
6990
7977
 
6991
7978
  // src/rules/no-type-member-comment-wall.ts
6992
7979
  import { AST_NODE_TYPES as AST_NODE_TYPES32 } from "@typescript-eslint/utils";
7980
+ var noTypeMemberCommentWallDocumentation = {
7981
+ summary: "Flag an object type whose member comments mostly re-spell the members' own names and types.",
7982
+ rationale: "Repetitive member comments add scanning cost while hiding the comments that describe facts absent from the type.",
7983
+ remediation: "Delete comments that restate member names or types and keep comments that add constraints or behavior.",
7984
+ category: "maintainability",
7985
+ limitations: ["Only interface and type-literal bodies meeting the configured comment-count and restatement-ratio thresholds are reported."],
7986
+ examples: [
7987
+ {
7988
+ id: "uncommented-members",
7989
+ title: "Let clear member names and types stand alone",
7990
+ outcome: "no-match",
7991
+ files: [{ path: "src/credentials.ts", source: "interface Credentials { host: string; port: number; username: string; }" }],
7992
+ focusPath: "src/credentials.ts",
7993
+ expectedCount: 0,
7994
+ public: true
7995
+ },
7996
+ {
7997
+ id: "restated-type-members",
7998
+ title: "Do not restate member names and types",
7999
+ outcome: "match",
8000
+ files: [{ path: "src/credentials.ts", source: "interface Credentials {\n // Database host.\n host?: string;\n // Database host port.\n port?: number;\n // Database username.\n username?: string;\n // Database password.\n password?: string;\n}" }],
8001
+ focusPath: "src/credentials.ts",
8002
+ expectedCount: 1,
8003
+ public: true
8004
+ }
8005
+ ]
8006
+ };
6993
8007
  function isNamedMember(node) {
6994
8008
  return (node.type === AST_NODE_TYPES32.TSPropertySignature || node.type === AST_NODE_TYPES32.TSMethodSignature) && !node.computed;
6995
8009
  }
6996
8010
  var no_type_member_comment_wall_default = createRule({
6997
8011
  name: "no-type-member-comment-wall",
8012
+ documentation: noTypeMemberCommentWallDocumentation,
6998
8013
  meta: {
6999
8014
  type: "suggestion",
7000
8015
  docs: {
@@ -7079,6 +8094,33 @@ var no_type_member_comment_wall_default = createRule({
7079
8094
 
7080
8095
  // src/rules/no-unnecessary-use-client.ts
7081
8096
  import { AST_NODE_TYPES as AST_NODE_TYPES33 } from "@typescript-eslint/utils";
8097
+ var noUnnecessaryUseClientDocumentation = {
8098
+ summary: "Flag `'use client'` files with no hooks or event handlers \u2014 they could be RSC.",
8099
+ rationale: "An unnecessary client boundary sends the component and its transitive dependencies to the browser without using client-only behavior.",
8100
+ remediation: "Remove the directive, or keep it only when the module uses a supported client-side API or boundary dependency.",
8101
+ category: "performance",
8102
+ limitations: ["Client need is inferred from recognized hooks, handlers, browser globals, exports, classes, and known client-only imports."],
8103
+ examples: [
8104
+ {
8105
+ id: "interactive-component",
8106
+ title: "Keep the directive for interactive components",
8107
+ outcome: "no-match",
8108
+ files: [{ path: "src/counter.tsx", source: "'use client'; import { useState } from 'react'; export default function X() { const [n] = useState(0); return <div>{n}</div>; }" }],
8109
+ focusPath: "src/counter.tsx",
8110
+ expectedCount: 0,
8111
+ public: true
8112
+ },
8113
+ {
8114
+ id: "static-component",
8115
+ title: "Remove the directive from static components",
8116
+ outcome: "match",
8117
+ files: [{ path: "src/banner.tsx", source: "'use client'; export default function X() { return <div>hello</div>; }" }],
8118
+ focusPath: "src/banner.tsx",
8119
+ expectedCount: 1,
8120
+ public: true
8121
+ }
8122
+ ]
8123
+ };
7082
8124
  var HOOK_REGEX = /^use([A-Z]|$)/;
7083
8125
  var EVENT_PROP_REGEX = /^on[A-Z]/;
7084
8126
  var ERROR_FILE_REGEX = /\b(?:global-)?error\.[jt]sx?$/;
@@ -7153,6 +8195,7 @@ var isGlobalReference = (node, context) => {
7153
8195
  };
7154
8196
  var no_unnecessary_use_client_default = createRule({
7155
8197
  name: "no-unnecessary-use-client",
8198
+ documentation: noUnnecessaryUseClientDocumentation,
7156
8199
  meta: {
7157
8200
  type: "suggestion",
7158
8201
  docs: {
@@ -7286,8 +8329,36 @@ var MOCK_MODULES = /* @__PURE__ */ new Set([
7286
8329
  "jest-mock",
7287
8330
  "@jest/globals"
7288
8331
  ]);
8332
+ var noUnsafeMockCastingDocumentation = {
8333
+ summary: "Disallow casting to mock types like `jest.Mock` or `vi.Mock`. Use `vi.mocked()` or `jest.mocked()` instead.",
8334
+ rationale: "A type assertion can claim an unmocked value is a mock and bypass checking between the original callable and the mock API.",
8335
+ remediation: "Use the test framework's `mocked` helper to obtain the typed mock reference.",
8336
+ category: "testing",
8337
+ limitations: ["Only mock types imported from Vitest or Jest modules are inspected."],
8338
+ examples: [
8339
+ {
8340
+ id: "typed-mock-helper",
8341
+ title: "Use the framework helper",
8342
+ outcome: "no-match",
8343
+ files: [{ path: "src/client.test.ts", source: "const m = vi.mocked(myFn);" }],
8344
+ focusPath: "src/client.test.ts",
8345
+ expectedCount: 0,
8346
+ public: true
8347
+ },
8348
+ {
8349
+ id: "mock-type-assertion",
8350
+ title: "Do not assert that a value is a mock",
8351
+ outcome: "match",
8352
+ files: [{ path: "src/client.test.ts", source: 'import type * as vi from "vitest"; const m = myFn as vi.Mock;' }],
8353
+ focusPath: "src/client.test.ts",
8354
+ expectedCount: 1,
8355
+ public: true
8356
+ }
8357
+ ]
8358
+ };
7289
8359
  var no_unsafe_mock_casting_default = createRule({
7290
8360
  name: "no-unsafe-mock-casting",
8361
+ documentation: noUnsafeMockCastingDocumentation,
7291
8362
  meta: {
7292
8363
  type: "problem",
7293
8364
  docs: {
@@ -7358,6 +8429,35 @@ import {
7358
8429
  AST_NODE_TYPES as AST_NODE_TYPES35
7359
8430
  } from "@typescript-eslint/utils";
7360
8431
  import * as ts from "typescript";
8432
+ var noZodNativeEnumDocumentation = {
8433
+ summary: 'Disallow `z.nativeEnum()` (and `z.enum()` over a TypeScript enum); use `z.enum(["a", "b"])` with a string-literal union instead.',
8434
+ rationale: "Wrapping a TypeScript enum preserves its emitted runtime object and duplicates the schema's value definition across two constructs.",
8435
+ remediation: "Pass string literals directly to `z.enum` and derive the TypeScript type with `z.infer`.",
8436
+ category: "maintainability",
8437
+ autofix: "safe",
8438
+ limitations: ["Automatic fixes are limited to inline object literals whose unique values are all string literals."],
8439
+ examples: [
8440
+ {
8441
+ id: "zod-literal-enum",
8442
+ title: "Declare string values directly in Zod",
8443
+ outcome: "no-match",
8444
+ files: [{ path: "src/status.ts", source: 'import { z } from "zod"; const S = z.enum(["active", "inactive"]);' }],
8445
+ focusPath: "src/status.ts",
8446
+ expectedCount: 0,
8447
+ public: true
8448
+ },
8449
+ {
8450
+ id: "zod-native-enum",
8451
+ title: "Do not wrap a TypeScript enum",
8452
+ outcome: "match",
8453
+ files: [{ path: "src/status.ts", source: 'import { z } from "zod"; const S = z.nativeEnum({ Active: "active", Inactive: "inactive" });' }],
8454
+ focusPath: "src/status.ts",
8455
+ expectedCount: 1,
8456
+ public: true,
8457
+ fixedFiles: [{ path: "src/status.ts", source: 'import { z } from "zod"; const S = z.enum(["active", "inactive"]);' }]
8458
+ }
8459
+ ]
8460
+ };
7361
8461
  var IGNORE_PATTERNS = [
7362
8462
  /[\\/]generated[\\/]/,
7363
8463
  /\.gen\.tsx?$/,
@@ -7427,6 +8527,7 @@ function resolvesToImportedEnum(node, services) {
7427
8527
  }
7428
8528
  var no_zod_native_enum_default = createRule({
7429
8529
  name: "no-zod-native-enum",
8530
+ documentation: noZodNativeEnumDocumentation,
7430
8531
  meta: {
7431
8532
  type: "suggestion",
7432
8533
  fixable: "code",
@@ -7538,6 +8639,18 @@ var no_zod_native_enum_default = createRule({
7538
8639
 
7539
8640
  // src/rules/test-loops-over-literal-cases.ts
7540
8641
  import { AST_NODE_TYPES as AST_NODE_TYPES36, ASTUtils as ASTUtils6 } from "@typescript-eslint/utils";
8642
+ var testLoopsOverLiteralCasesDocumentation = {
8643
+ summary: "Disallow assertions over an inline literal case loop in a test; parameterization reports and names every case independently.",
8644
+ rationale: "A loop is reported as one test, so failures hide the individual case name and may stop later cases from running.",
8645
+ remediation: "Create one named parameterized test or runner-aware subtest for each literal case.",
8646
+ category: "testing",
8647
+ filePatterns: ["**/*.test.*", "**/*.spec.*", "**/tests/**"],
8648
+ limitations: ["Only inline literal for-of cases containing framework assertions are reported."],
8649
+ examples: [
8650
+ { id: "parameterized-cases", title: "Use a parameterized test", outcome: "no-match", files: [{ path: "src/parser.test.ts", source: "test.each(['a', 'b'])('parses %s', (value) => { expect(parse(value)).toBe(value); });" }], focusPath: "src/parser.test.ts", expectedCount: 0, public: true },
8651
+ { id: "looped-cases", title: "Do not hide cases in a loop", outcome: "match", files: [{ path: "src/parser.test.ts", source: "test('parses', () => { for (const value of ['a', 'b']) { expect(parse(value)).toBe(value); } });" }], focusPath: "src/parser.test.ts", expectedCount: 1, public: true }
8652
+ ]
8653
+ };
7541
8654
  var TEST_CALLERS4 = /* @__PURE__ */ new Set(["it", "test"]);
7542
8655
  var TEST_MODIFIERS2 = /* @__PURE__ */ new Set(["concurrent", "fails", "only", "sequential", "skip"]);
7543
8656
  var ASSERTION_ROOTS2 = /* @__PURE__ */ new Set([
@@ -7671,6 +8784,7 @@ var LOOP_CARRIED_CONTROL = /* @__PURE__ */ new Set([
7671
8784
  ]);
7672
8785
  var test_loops_over_literal_cases_default = createRule({
7673
8786
  name: "test-loops-over-literal-cases",
8787
+ documentation: testLoopsOverLiteralCasesDocumentation,
7674
8788
  meta: {
7675
8789
  type: "suggestion",
7676
8790
  docs: {
@@ -7730,6 +8844,17 @@ function unwrapExpression(node) {
7730
8844
 
7731
8845
  // src/rules/prefer-constant-time-secret-compare.ts
7732
8846
  import { AST_NODE_TYPES as AST_NODE_TYPES37 } from "@typescript-eslint/utils";
8847
+ var preferConstantTimeSecretCompareDocumentation = {
8848
+ summary: "Disallow `===`/`!==` on a secret-like value; short-circuiting comparison leaks the secret through timing. Use a constant-time compare.",
8849
+ rationale: "Ordinary equality stops at the first differing byte, allowing repeated measurements to reveal secret material.",
8850
+ remediation: "Compare equal-length cryptographic digests with a constant-time comparison primitive.",
8851
+ category: "security",
8852
+ limitations: ["Secret-like values are identified conservatively from their names; test files and public sentinel comparisons are excluded."],
8853
+ examples: [
8854
+ { id: "constant-time-compare", title: "Use a constant-time comparison", outcome: "no-match", files: [{ path: "src/auth.ts", source: "if (await constantTimeEqual(presentedToken, expectedToken)) { allow(); }" }], focusPath: "src/auth.ts", expectedCount: 0, public: true },
8855
+ { id: "secret-equality", title: "Do not compare secrets with equality", outcome: "match", files: [{ path: "src/auth.ts", source: "if (presentedToken === expectedToken) { allow(); }" }], focusPath: "src/auth.ts", expectedCount: 1, public: true }
8856
+ ]
8857
+ };
7733
8858
  var EQUALITY_OPERATORS = /* @__PURE__ */ new Set(["===", "!==", "==", "!="]);
7734
8859
  var SENTINEL_IDENTIFIERS = /* @__PURE__ */ new Set(["undefined", "NaN"]);
7735
8860
  var SENTINEL_WORDS = /(^|_)(SENTINEL|EMPTY|NONE|NULL|UNSET|MISSING|PLACEHOLDER|DUMMY|FAKE|EXAMPLE)(_|$)/;
@@ -7784,6 +8909,7 @@ function secretNameOf(node) {
7784
8909
  }
7785
8910
  var prefer_constant_time_secret_compare_default = createRule({
7786
8911
  name: "prefer-constant-time-secret-compare",
8912
+ documentation: preferConstantTimeSecretCompareDocumentation,
7787
8913
  meta: {
7788
8914
  type: "problem",
7789
8915
  docs: {
@@ -7825,6 +8951,17 @@ var prefer_constant_time_secret_compare_default = createRule({
7825
8951
  // src/rules/prefer-discriminated-union.ts
7826
8952
  import "@typescript-eslint/utils";
7827
8953
  import { AST_NODE_TYPES as AST_NODE_TYPES38 } from "@typescript-eslint/utils";
8954
+ var preferDiscriminatedUnionDocumentation = {
8955
+ summary: "Flag flat result objects with a required positive boolean status and optional success/failure payloads.",
8956
+ rationale: "A boolean status plus optional branch data permits contradictory and incomplete states.",
8957
+ remediation: "Represent each result branch as a discriminated union member with its required payload.",
8958
+ category: "correctness",
8959
+ limitations: ["Only local object shapes with recognized positive status and payload names are inspected."],
8960
+ examples: [
8961
+ { id: "explicit-result-branches", title: "Use explicit result branches", outcome: "no-match", files: [{ path: "src/result.ts", source: "type Result = { ok: true; data: string } | { ok: false; error: string };" }], focusPath: "src/result.ts", expectedCount: 0, public: true },
8962
+ { id: "optional-result-payloads", title: "Do not make both result payloads optional", outcome: "match", files: [{ path: "src/result.ts", source: "type Result = { ok: boolean; data?: string; error?: string };" }], focusPath: "src/result.ts", expectedCount: 1, public: true }
8963
+ ]
8964
+ };
7828
8965
  var STATUS_MEMBER_NAMES = /* @__PURE__ */ new Set([
7829
8966
  "success",
7830
8967
  "ok"
@@ -7906,6 +9043,7 @@ function inlineReturnTypeLiteral(node) {
7906
9043
  }
7907
9044
  var prefer_discriminated_union_default = createRule({
7908
9045
  name: "prefer-discriminated-union",
9046
+ documentation: preferDiscriminatedUnionDocumentation,
7909
9047
  meta: {
7910
9048
  type: "suggestion",
7911
9049
  docs: {
@@ -7954,6 +9092,17 @@ var prefer_discriminated_union_default = createRule({
7954
9092
 
7955
9093
  // src/rules/prefer-input-group-search.ts
7956
9094
  import { AST_NODE_TYPES as AST_NODE_TYPES39 } from "@typescript-eslint/utils";
9095
+ var preferInputGroupSearchDocumentation = {
9096
+ summary: "Require search icons and shared Input controls in the same visual wrapper to use InputGroup.",
9097
+ rationale: "The shared compound control provides consistent spacing, focus behavior, and accessible composition.",
9098
+ remediation: "Compose the search icon and field with InputGroup, InputGroupAddon, and InputGroupInput.",
9099
+ category: "style",
9100
+ limitations: ["Only Search and Input bindings imported from the recognized shared modules are paired."],
9101
+ examples: [
9102
+ { id: "grouped-search", title: "Use the shared input group", outcome: "no-match", files: [{ path: "src/search.tsx", source: "import { Search } from 'lucide-react'; import { InputGroup, InputGroupAddon, InputGroupInput } from '@/components/ui/input-group'; const field = <InputGroup><InputGroupAddon><Search /></InputGroupAddon><InputGroupInput /></InputGroup>;" }], focusPath: "src/search.tsx", expectedCount: 0, public: true },
9103
+ { id: "loose-search-input", title: "Do not pair loose search controls", outcome: "match", files: [{ path: "src/search.tsx", source: "import { Search } from 'lucide-react'; import { Input } from '@/components/ui/input'; const field = <div><Search /><Input /></div>;" }], focusPath: "src/search.tsx", expectedCount: 1, public: true }
9104
+ ]
9105
+ };
7957
9106
  var INPUT_MODULE = /(?:^|\/)components\/ui\/input$/u;
7958
9107
  var INPUT_GROUP_MODULE = /(?:^|\/)components\/ui\/input-group$/u;
7959
9108
  var MAX_JSX_DISTANCE = 2;
@@ -7997,6 +9146,7 @@ function nearestEligibleCommonAncestor(search, input, inputGroupNames) {
7997
9146
  }
7998
9147
  var prefer_input_group_search_default = createRule({
7999
9148
  name: "prefer-input-group-search",
9149
+ documentation: preferInputGroupSearchDocumentation,
8000
9150
  meta: {
8001
9151
  type: "suggestion",
8002
9152
  docs: {
@@ -8067,6 +9217,35 @@ var prefer_input_group_search_default = createRule({
8067
9217
 
8068
9218
  // src/rules/prefer-immutable-module-constant.ts
8069
9219
  import { AST_NODE_TYPES as AST_NODE_TYPES40, ASTUtils as ASTUtils7 } from "@typescript-eslint/utils";
9220
+ var preferImmutableModuleConstantDocumentation = {
9221
+ summary: "Require module-level constant collections to expose readonly state.",
9222
+ rationale: "A const binding prevents reassignment but does not stop callers from mutating its array, object, Set, or Map contents.",
9223
+ remediation: "Expose literals with `as const` or a readonly type, and expose Set or Map values through ReadonlySet or ReadonlyMap.",
9224
+ category: "correctness",
9225
+ limitations: [
9226
+ "The rule skips generated files, test files, JavaScript files, and collections that are deliberately mutated in their declaring module."
9227
+ ],
9228
+ examples: [
9229
+ {
9230
+ id: "readonly-array-literal",
9231
+ title: "A module constant exposes a readonly literal",
9232
+ outcome: "no-match",
9233
+ files: [{ path: "src/constants.ts", source: "const VALUES = [1, 2, 3] as const;" }],
9234
+ focusPath: "src/constants.ts",
9235
+ expectedCount: 0,
9236
+ public: true
9237
+ },
9238
+ {
9239
+ id: "mutable-array-literal",
9240
+ title: "A module constant exposes a mutable array",
9241
+ outcome: "match",
9242
+ files: [{ path: "src/constants.ts", source: "const VALUES = [1, 2, 3];" }],
9243
+ focusPath: "src/constants.ts",
9244
+ expectedCount: 1,
9245
+ public: true
9246
+ }
9247
+ ]
9248
+ };
8070
9249
  var CONSTANT_NAME = /^_?[A-Z][A-Z0-9_]*$/;
8071
9250
  var JAVASCRIPT_FILE_RE = /\.[cm]?jsx?$/i;
8072
9251
  var MUTATING_METHODS = /* @__PURE__ */ new Set([
@@ -8174,6 +9353,7 @@ function referenceMutates(identifier, isUnshadowedGlobal) {
8174
9353
  }
8175
9354
  var prefer_immutable_module_constant_default = createRule({
8176
9355
  name: "prefer-immutable-module-constant",
9356
+ documentation: preferImmutableModuleConstantDocumentation,
8177
9357
  meta: {
8178
9358
  type: "suggestion",
8179
9359
  docs: {
@@ -8260,6 +9440,17 @@ function unwrapTransparentExport(node) {
8260
9440
 
8261
9441
  // src/rules/prefer-shadcn-primitives.ts
8262
9442
  import { AST_NODE_TYPES as AST_NODE_TYPES41 } from "@typescript-eslint/utils";
9443
+ var preferShadcnPrimitivesDocumentation = {
9444
+ summary: "Require visible raw JSX controls to use the corresponding shared shadcn primitive.",
9445
+ rationale: "Shared primitives centralize interaction, accessibility, and visual behavior across the product.",
9446
+ remediation: "Replace the raw visible control with the corresponding shared shadcn component.",
9447
+ category: "style",
9448
+ limitations: ["Hidden and file inputs, unassociated labels, and non-control semantic elements are excluded."],
9449
+ examples: [
9450
+ { id: "shared-button", title: "Use a shared button", outcome: "no-match", files: [{ path: "src/form.tsx", source: "import { Button } from '@/components/ui/button'; const action = <Button>Save</Button>;" }], focusPath: "src/form.tsx", expectedCount: 0, public: true },
9451
+ { id: "raw-button", title: "Do not use a raw button", outcome: "match", files: [{ path: "src/form.tsx", source: "const action = <button>Save</button>;" }], focusPath: "src/form.tsx", expectedCount: 1, public: true }
9452
+ ]
9453
+ };
8263
9454
  var SHADCN_PRIMITIVES = {
8264
9455
  button: "Button",
8265
9456
  dialog: "Dialog or AlertDialog family",
@@ -8362,6 +9553,7 @@ function replacementFor(node, element) {
8362
9553
  }
8363
9554
  var prefer_shadcn_primitives_default = createRule({
8364
9555
  name: "prefer-shadcn-primitives",
9556
+ documentation: preferShadcnPrimitivesDocumentation,
8365
9557
  meta: {
8366
9558
  type: "suggestion",
8367
9559
  docs: {
@@ -8393,6 +9585,17 @@ var prefer_shadcn_primitives_default = createRule({
8393
9585
 
8394
9586
  // src/rules/prefer-module-level-constant.ts
8395
9587
  import { AST_NODE_TYPES as AST_NODE_TYPES42 } from "@typescript-eslint/utils";
9588
+ var preferModuleLevelConstantDocumentation = {
9589
+ summary: "Hoist literal-only constant collections and regexes out of function bodies to module scope so they are allocated once.",
9590
+ rationale: "Recreating immutable lookup data on every call wastes allocations and obscures its constant nature.",
9591
+ remediation: "Declare immutable literal collections and non-stateful regular expressions once at module scope.",
9592
+ category: "performance",
9593
+ limitations: ["Collections that are small, mutated, escape the function, or depend on local values are not reported."],
9594
+ examples: [
9595
+ { id: "hoisted-collection", title: "Hoist a constant collection", outcome: "no-match", files: [{ path: "src/keys.ts", source: "const KEYS = ['a', 'b', 'c'] as const; function isAllowed(key: string) { return KEYS.includes(key); }" }], focusPath: "src/keys.ts", expectedCount: 0, public: true },
9596
+ { id: "local-collection", title: "Do not recreate a constant collection", outcome: "match", files: [{ path: "src/keys.ts", source: "function isAllowed(key: string) { const KEYS = ['a', 'b', 'c']; return KEYS.includes(key); }" }], focusPath: "src/keys.ts", expectedCount: 1, public: true }
9597
+ ]
9598
+ };
8396
9599
  var DEFAULT_MIN_ELEMENTS = 3;
8397
9600
  var MAX_LITERAL_DEPTH = 4;
8398
9601
  var IGNORE_PATTERNS2 = [
@@ -8598,6 +9801,7 @@ function isNonRetainingBuiltinCall(node, argument) {
8598
9801
  }
8599
9802
  var prefer_module_level_constant_default = createRule({
8600
9803
  name: "prefer-module-level-constant",
9804
+ documentation: preferModuleLevelConstantDocumentation,
8601
9805
  meta: {
8602
9806
  type: "suggestion",
8603
9807
  docs: {
@@ -8689,6 +9893,17 @@ var prefer_module_level_constant_default = createRule({
8689
9893
 
8690
9894
  // src/rules/prefer-module-level-schema.ts
8691
9895
  import { AST_NODE_TYPES as AST_NODE_TYPES43 } from "@typescript-eslint/utils";
9896
+ var preferModuleLevelSchemaDocumentation = {
9897
+ summary: "Declare a Zod schema at module scope when it closes over nothing in the enclosing function",
9898
+ rationale: "A closed schema created inside a function is rebuilt on every call and cannot be reused or exported for inference.",
9899
+ remediation: "Move the closed schema declaration to module scope and reference it from the function.",
9900
+ category: "performance",
9901
+ limitations: ["Schemas that depend on local state or are wrapped in a recognized memoization helper are excluded."],
9902
+ examples: [
9903
+ { id: "module-schema", title: "Declare the schema once", outcome: "no-match", files: [{ path: "src/handler.ts", source: "import { z } from 'zod'; const ZBody = z.object({ id: z.string(), name: z.string() }); export function handle(raw: unknown) { return ZBody.parse(raw); }" }], focusPath: "src/handler.ts", expectedCount: 0, public: true },
9904
+ { id: "local-schema", title: "Do not rebuild a closed schema", outcome: "match", files: [{ path: "src/handler.ts", source: "import { z } from 'zod'; export function handle(raw: unknown) { const ZBody = z.object({ id: z.string(), name: z.string() }); return ZBody.parse(raw); }" }], focusPath: "src/handler.ts", expectedCount: 1, public: true }
9905
+ ]
9906
+ };
8692
9907
  var DEFAULT_FACTORIES = [
8693
9908
  "discriminatedUnion",
8694
9909
  "intersection",
@@ -8835,6 +10050,7 @@ function collectReferences(scope, out) {
8835
10050
  }
8836
10051
  var prefer_module_level_schema_default = createRule({
8837
10052
  name: "prefer-module-level-schema",
10053
+ documentation: preferModuleLevelSchemaDocumentation,
8838
10054
  meta: {
8839
10055
  type: "problem",
8840
10056
  docs: {
@@ -9032,11 +10248,24 @@ var prefer_module_level_schema_default = createRule({
9032
10248
 
9033
10249
  // src/rules/prefer-native-random-uuid.ts
9034
10250
  import { AST_NODE_TYPES as AST_NODE_TYPES44, ASTUtils as ASTUtils8 } from "@typescript-eslint/utils";
10251
+ var preferNativeRandomUuidDocumentation = {
10252
+ summary: "Prefer `globalThis.crypto.randomUUID()` over resolved zero-argument UUID v4 bindings from the `uuid` package.",
10253
+ rationale: "The platform implementation avoids an unnecessary dependency for standard random UUID generation.",
10254
+ remediation: "Call `globalThis.crypto.randomUUID()` and remove the unused `uuid` v4 import when possible.",
10255
+ category: "maintainability",
10256
+ autofix: "suggestion",
10257
+ limitations: ["Only resolved zero-argument UUID v4 calls are reported; customized and other UUID versions are excluded."],
10258
+ examples: [
10259
+ { id: "native-random-uuid", title: "Use the platform UUID generator", outcome: "no-match", files: [{ path: "src/id.ts", source: "const id = globalThis.crypto.randomUUID();" }], focusPath: "src/id.ts", expectedCount: 0, public: true },
10260
+ { id: "uuid-v4-package", title: "Do not call uuid v4 without options", outcome: "match", files: [{ path: "src/id.ts", source: "import { v4 } from 'uuid'; const id = v4();" }], focusPath: "src/id.ts", expectedCount: 1, public: true }
10261
+ ]
10262
+ };
9035
10263
  function requireUuid(node) {
9036
10264
  return node?.type === AST_NODE_TYPES44.CallExpression && node.callee.type === AST_NODE_TYPES44.Identifier && node.callee.name === "require" && node.arguments.length === 1 && node.arguments[0]?.type === AST_NODE_TYPES44.Literal && node.arguments[0].value === "uuid";
9037
10265
  }
9038
10266
  var prefer_native_random_uuid_default = createRule({
9039
10267
  name: "prefer-native-random-uuid",
10268
+ documentation: preferNativeRandomUuidDocumentation,
9040
10269
  meta: {
9041
10270
  type: "suggestion",
9042
10271
  docs: {
@@ -9118,6 +10347,17 @@ var prefer_native_random_uuid_default = createRule({
9118
10347
 
9119
10348
  // src/rules/prefer-non-nullable-collection.ts
9120
10349
  import { AST_NODE_TYPES as AST_NODE_TYPES45, ASTUtils as ASTUtils9 } from "@typescript-eslint/utils";
10350
+ var preferNonNullableCollectionDocumentation = {
10351
+ summary: "Suggest non-null arrays only when local control flow proves the nullish state is equivalent to an empty collection.",
10352
+ rationale: "A redundant nullish collection state spreads defaults and guards through consumers without carrying information.",
10353
+ remediation: "Use a non-null collection type and normalize omitted input to an empty collection at the boundary.",
10354
+ category: "maintainability",
10355
+ limitations: ["The rule requires local evidence that nullish and empty values are treated identically and skips exported wire shapes."],
10356
+ examples: [
10357
+ { id: "non-null-array", title: "Model an always-present collection", outcome: "no-match", files: [{ path: "src/search.ts", source: "interface Input { items: string[] } function search({ items }: Input) { return items.length; }" }], focusPath: "src/search.ts", expectedCount: 0, public: true },
10358
+ { id: "defaulted-nullish-array", title: "Do not retain a redundant nullish state", outcome: "match", files: [{ path: "src/search.ts", source: "interface Input { items: string[] | undefined } function search({ items = [] }: Input) { return items.length; }" }], focusPath: "src/search.ts", expectedCount: 1, public: true }
10359
+ ]
10360
+ };
9121
10361
  var ARRAY_TYPE_NAMES = /* @__PURE__ */ new Set(["Array", "ReadonlyArray"]);
9122
10362
  function propertyName(node) {
9123
10363
  const key = node.key;
@@ -9255,6 +10495,7 @@ function memberIsOnlyCoalesced(context, object, property, fn) {
9255
10495
  }
9256
10496
  var prefer_non_nullable_collection_default = createRule({
9257
10497
  name: "prefer-non-nullable-collection",
10498
+ documentation: preferNonNullableCollectionDocumentation,
9258
10499
  meta: {
9259
10500
  type: "suggestion",
9260
10501
  docs: {
@@ -9351,6 +10592,17 @@ var prefer_non_nullable_collection_default = createRule({
9351
10592
 
9352
10593
  // src/rules/prefer-schema-for-api-payload.ts
9353
10594
  import { AST_NODE_TYPES as AST_NODE_TYPES46 } from "@typescript-eslint/utils";
10595
+ var preferSchemaForApiPayloadDocumentation = {
10596
+ summary: "Require Zod (or similar) schema validation on `response.json()` / `JSON.parse()` results before property access.",
10597
+ rationale: "External JSON is untrusted at runtime even when its expected TypeScript shape is known statically.",
10598
+ remediation: "Parse the payload through a schema or establish a recognized runtime validation guard before reading fields.",
10599
+ category: "correctness",
10600
+ limitations: ["Test fixtures, generated clients, local JSON files, and recognized validation guards are excluded."],
10601
+ examples: [
10602
+ { id: "validated-payload", title: "Validate before property access", outcome: "no-match", files: [{ path: "src/client.ts", source: "async function load(response) { const body = UserSchema.parse(await response.json()); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
10603
+ { id: "unvalidated-payload", title: "Do not trust response JSON directly", outcome: "match", files: [{ path: "src/client.ts", source: "async function load(response) { const body = await response.json(); return body.id; }" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
10604
+ ]
10605
+ };
9354
10606
  var unwrap4 = (node) => {
9355
10607
  let current = node;
9356
10608
  while (current !== null && current !== void 0) {
@@ -9592,6 +10844,7 @@ var unvalidatedVariableRef = (node, scope, tracked) => {
9592
10844
  };
9593
10845
  var prefer_schema_for_api_payload_default = createRule({
9594
10846
  name: "prefer-schema-for-api-payload",
10847
+ documentation: preferSchemaForApiPayloadDocumentation,
9595
10848
  meta: {
9596
10849
  type: "problem",
9597
10850
  docs: {
@@ -9794,6 +11047,17 @@ var tailwindBase = (token) => token.replace(/^(?:[a-z0-9-]+:)+/i, "").replace(/^
9794
11047
  var classTokens = (value) => value.split(/\s+/).filter(Boolean);
9795
11048
 
9796
11049
  // src/rules/prefer-semantic-colors.ts
11050
+ var preferSemanticColorsDocumentation = {
11051
+ summary: "Enforce semantic color tokens over raw Tailwind palette classes, arbitrary color values, and inline color literals.",
11052
+ rationale: "Semantic tokens keep themes and product meaning consistent while raw colors couple components to a palette value.",
11053
+ remediation: "Replace raw palette and literal colors with the closest semantic design-system token or CSS variable.",
11054
+ category: "style",
11055
+ limitations: ["Email, PDF, icon artwork, masks, gradients, stories, and explicitly configured non-token projects have targeted exclusions."],
11056
+ examples: [
11057
+ { id: "semantic-text-color", title: "Use a semantic color token", outcome: "no-match", files: [{ path: "src/notice.tsx", source: 'const notice = <div className="text-destructive" />;' }], focusPath: "src/notice.tsx", expectedCount: 0, public: true },
11058
+ { id: "raw-text-color", title: "Do not use a raw palette color", outcome: "match", files: [{ path: "src/notice.tsx", source: 'const notice = <div className="text-red-500" />;' }], focusPath: "src/notice.tsx", expectedCount: 1, public: true }
11059
+ ]
11060
+ };
9797
11061
  var COLOR_PREFIXES = "text|bg|border(?:-[trblxyse])?|ring(?:-offset)?|fill|stroke|from|via|to|divide|decoration|placeholder|accent|caret|shadow|outline";
9798
11062
  var PALETTE = "red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone";
9799
11063
  var COLOR_FN = "rgba?|hsla?|hwb|oklch|oklab|lab|lch|color";
@@ -10072,10 +11336,11 @@ var staticallyImportsEmailOrPdfRenderer = (program) => program.body.some((statem
10072
11336
  });
10073
11337
  var prefer_semantic_colors_default = createRule({
10074
11338
  name: "prefer-semantic-colors",
11339
+ documentation: preferSemanticColorsDocumentation,
10075
11340
  meta: {
10076
11341
  type: "suggestion",
10077
11342
  docs: {
10078
- description: "Enforce design-system semantic color tokens (bg-primary, text-destructive, \u2026) over raw Tailwind palette classes (text-red-500), arbitrary color values (bg-[#fff]), and inline color literals."
11343
+ description: "Enforce semantic color tokens over raw Tailwind palette classes, arbitrary color values, and inline color literals."
10079
11344
  },
10080
11345
  schema: [
10081
11346
  {
@@ -10210,6 +11475,17 @@ var prefer_semantic_colors_default = createRule({
10210
11475
 
10211
11476
  // src/rules/prefer-server-actions.ts
10212
11477
  import "@typescript-eslint/utils";
11478
+ var preferServerActionsDocumentation = {
11479
+ summary: "Prefer Next.js Server Actions over /api/* mutations.",
11480
+ rationale: "Server Actions preserve typed application calls and avoid an internal JSON request-response boundary.",
11481
+ remediation: "Move the mutation into a Server Action and invoke that action from the React client.",
11482
+ category: "architecture",
11483
+ limitations: ["Only statically recognizable /api/ mutations in applicable React modules are reported."],
11484
+ examples: [
11485
+ { id: "server-action-call", title: "Call a Server Action", outcome: "no-match", files: [{ path: "app/tasks/page.tsx", source: "import { createTask } from './actions'; await createTask(input);" }], focusPath: "app/tasks/page.tsx", expectedCount: 0, public: true },
11486
+ { id: "api-mutation", title: "Do not mutate through an API route", outcome: "match", files: [{ path: "app/tasks/page.tsx", source: "await fetch('/api/tasks', { method: 'POST', body });" }], focusPath: "app/tasks/page.tsx", expectedCount: 1, public: true }
11487
+ ]
11488
+ };
10213
11489
  var MUTATION_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "DELETE", "PATCH"]);
10214
11490
  var AXIOS_MUTATION_METHODS = /* @__PURE__ */ new Set(["post", "put", "delete", "patch"]);
10215
11491
  var SKIP_FILE_REGEX = /(?:\.test\.[jt]sx?$|\.spec\.[jt]sx?$|-(?:test|spec)\.[jt]sx?$|\/tests?\/|\/__tests__\/|\/__testfixtures__\/|\/scripts?\/|\/app\/api\/.*\/route\.[jt]sx?$|\/pages\/api\/)/;
@@ -10307,6 +11583,7 @@ function getPropertyNode(objNode, propName2) {
10307
11583
  }
10308
11584
  var prefer_server_actions_default = createRule({
10309
11585
  name: "prefer-server-actions",
11586
+ documentation: preferServerActionsDocumentation,
10310
11587
  meta: {
10311
11588
  type: "suggestion",
10312
11589
  docs: {
@@ -10382,6 +11659,18 @@ var COLLECTION_PROPERTIES = /* @__PURE__ */ new Set(["length", "size"]);
10382
11659
  var LITERAL_KEY_HAZARDS = /* @__PURE__ */ new Set(["__proto__"]);
10383
11660
  var NUMERIC_SIGNS2 = /* @__PURE__ */ new Set(["-", "+"]);
10384
11661
  var MIN_RUN_LENGTH = 2;
11662
+ var preferWholeObjectAssertionDocumentation = {
11663
+ summary: "Collapse consecutive assertions on one object into a whole-object assertion so related mismatches are reported together.",
11664
+ rationale: "One whole-object assertion presents related expectations together and produces a complete structural diff.",
11665
+ remediation: "Replace consecutive member assertions with one `toMatchObject` assertion.",
11666
+ category: "testing",
11667
+ aliases: ["strict-test-assertions"],
11668
+ autofix: "safe",
11669
+ examples: [
11670
+ { id: "whole-object", title: "Assert the object once", outcome: "no-match", files: [{ path: "src/user.test.ts", source: "expect(user).toMatchObject({ id: 1, name: 'Ada' });" }], focusPath: "src/user.test.ts", expectedCount: 0, public: true },
11671
+ { id: "member-run", title: "Do not split one object across assertions", outcome: "match", files: [{ path: "src/user.test.ts", source: "expect(user.id).toBe(1);\nexpect(user.name).toBe('Ada');" }], focusPath: "src/user.test.ts", expectedCount: 1, public: true, fixedFiles: [{ path: "src/user.test.ts", source: "expect(user).toMatchObject({ id: 1, name: 'Ada' });\n" }] }
11672
+ ]
11673
+ };
10385
11674
  function literalText(node, getText) {
10386
11675
  switch (node.type) {
10387
11676
  case AST_NODE_TYPES48.Literal:
@@ -10419,10 +11708,11 @@ function literalIndex(node) {
10419
11708
  }
10420
11709
  var prefer_whole_object_assertion_default = createRule({
10421
11710
  name: "prefer-whole-object-assertion",
11711
+ documentation: preferWholeObjectAssertionDocumentation,
10422
11712
  meta: {
10423
11713
  type: "suggestion",
10424
11714
  docs: {
10425
- description: "Collapse a run of consecutive assertions on the same object into one assertion about the whole object, so every mismatch is reported and nothing outside the asserted keys goes unchecked."
11715
+ description: "Collapse consecutive assertions on one object into a whole-object assertion so related mismatches are reported together."
10426
11716
  },
10427
11717
  fixable: "code",
10428
11718
  messages: {
@@ -10597,6 +11887,16 @@ var prefer_whole_object_assertion_default = createRule({
10597
11887
 
10598
11888
  // src/rules/prefer-zod-infer.ts
10599
11889
  import { AST_NODE_TYPES as AST_NODE_TYPES49 } from "@typescript-eslint/utils";
11890
+ var preferZodInferDocumentation = {
11891
+ summary: "Derive a type from its Zod schema with `z.infer` instead of hand-writing a twin declaration beside it.",
11892
+ rationale: "A derived type stays synchronized when the runtime schema changes.",
11893
+ remediation: "Replace the hand-written twin with `z.infer<typeof Schema>`.",
11894
+ category: "correctness",
11895
+ examples: [
11896
+ { id: "inferred-type", title: "Infer the schema type", outcome: "no-match", files: [{ path: "src/user.ts", source: 'import { z } from "zod"; const UserSchema = z.object({ id: z.string() }); type User = z.infer<typeof UserSchema>;' }], focusPath: "src/user.ts", expectedCount: 0, public: true },
11897
+ { id: "handwritten-twin", title: "Do not duplicate the schema shape", outcome: "match", files: [{ path: "src/user.ts", source: 'import { z } from "zod"; const UserSchema = z.object({ id: z.string() }); interface User { id: string }' }], focusPath: "src/user.ts", expectedCount: 1, public: true }
11898
+ ]
11899
+ };
10600
11900
  var SHAPE_PRESERVING_METHODS = /* @__PURE__ */ new Set([
10601
11901
  "describe",
10602
11902
  "refine",
@@ -10722,6 +12022,7 @@ function leafAgrees(leaf, annotation) {
10722
12022
  }
10723
12023
  var prefer_zod_infer_default = createRule({
10724
12024
  name: "prefer-zod-infer",
12025
+ documentation: preferZodInferDocumentation,
10725
12026
  meta: {
10726
12027
  type: "problem",
10727
12028
  docs: {
@@ -11010,6 +12311,16 @@ import {
11010
12311
  AST_NODE_TYPES as AST_NODE_TYPES50
11011
12312
  } from "@typescript-eslint/utils";
11012
12313
  import ts2 from "typescript";
12314
+ var requireAssertNeverDocumentation = {
12315
+ summary: "Require an empty switch default to call `assertNever` so discriminated unions remain exhaustive at compile time.",
12316
+ rationale: "An empty default silently accepts new union members instead of making the compiler identify the missing case.",
12317
+ remediation: "Call `assertNever` with the discriminant in the exhaustive switch default.",
12318
+ category: "correctness",
12319
+ examples: [
12320
+ { id: "assert-never-default", title: "Make the default exhaustive", outcome: "no-match", files: [{ path: "src/render.ts", source: "declare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: assertNever(kind); }" }], focusPath: "src/render.ts", expectedCount: 0, public: true },
12321
+ { id: "empty-default", title: "Do not leave an exhaustive default empty", outcome: "match", files: [{ path: "src/render.ts", source: "declare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: }" }], focusPath: "src/render.ts", expectedCount: 1, public: true }
12322
+ ]
12323
+ };
11013
12324
  var isRuntimeHandlingStatement = (statement) => {
11014
12325
  if (statement.type === AST_NODE_TYPES50.EmptyStatement) return false;
11015
12326
  if (statement.type === AST_NODE_TYPES50.TSTypeAliasDeclaration || statement.type === AST_NODE_TYPES50.TSInterfaceDeclaration) {
@@ -11067,10 +12378,11 @@ function finiteTypeKey(type, checker) {
11067
12378
  }
11068
12379
  var require_assert_never_default = createRule({
11069
12380
  name: "require-assert-never",
12381
+ documentation: requireAssertNeverDocumentation,
11070
12382
  meta: {
11071
12383
  type: "problem",
11072
12384
  docs: {
11073
- description: "Require an exhaustive-style switch whose `default` case does no runtime work to call `assertNever(_)` so that discriminated unions are exhaustively checked at compile time. Switches with a legitimate runtime default (a reducer's `return state`, an HTTP-status `return fallback()`, a `break`, a `throw`, etc.) are left alone."
12385
+ description: "Require an empty switch default to call `assertNever` so discriminated unions remain exhaustive at compile time."
11074
12386
  },
11075
12387
  schema: [],
11076
12388
  messages: {
@@ -11108,6 +12420,16 @@ var require_assert_never_default = createRule({
11108
12420
 
11109
12421
  // src/rules/require-fetch-timeout.ts
11110
12422
  import { AST_NODE_TYPES as AST_NODE_TYPES51, ASTUtils as ASTUtils10 } from "@typescript-eslint/utils";
12423
+ var requireFetchTimeoutDocumentation = {
12424
+ summary: "Require an abort `signal` (e.g. `AbortSignal.timeout(ms)`) on global `fetch()` calls so stalled upstreams cannot hang the caller forever.",
12425
+ rationale: "An unbounded request can occupy work indefinitely when an upstream stalls.",
12426
+ remediation: "Pass an abort signal, such as `AbortSignal.timeout(ms)`, in the fetch init.",
12427
+ category: "correctness",
12428
+ examples: [
12429
+ { id: "bounded-fetch", title: "Bound the request", outcome: "no-match", files: [{ path: "src/client.ts", source: "await fetch(url, { signal: AbortSignal.timeout(5000) });" }], focusPath: "src/client.ts", expectedCount: 0, public: true },
12430
+ { id: "unbounded-fetch", title: "Do not leave fetch unbounded", outcome: "match", files: [{ path: "src/client.ts", source: "await fetch('https://api.example.com/items');" }], focusPath: "src/client.ts", expectedCount: 1, public: true }
12431
+ ]
12432
+ };
11111
12433
  var GLOBAL_OBJECTS2 = /* @__PURE__ */ new Set([
11112
12434
  "globalThis",
11113
12435
  "window",
@@ -11144,6 +12466,7 @@ function isInlineUrl(node, resolvesToGlobal) {
11144
12466
  }
11145
12467
  var require_fetch_timeout_default = createRule({
11146
12468
  name: "require-fetch-timeout",
12469
+ documentation: requireFetchTimeoutDocumentation,
11147
12470
  meta: {
11148
12471
  type: "problem",
11149
12472
  docs: {
@@ -11203,8 +12526,19 @@ var require_fetch_timeout_default = createRule({
11203
12526
  }
11204
12527
  });
11205
12528
 
11206
- // src/rules/require-interface-for-injected-service.ts
12529
+ // src/rules/require-port-for-service.ts
11207
12530
  import { AST_NODE_TYPES as AST_NODE_TYPES52 } from "@typescript-eslint/utils";
12531
+ var requirePortForServiceDocumentation = {
12532
+ summary: "Advise when an exported service with injected collaborators has public methods not covered by its declared ports.",
12533
+ rationale: "A declared port keeps consumers coupled to the service capability instead of its concrete implementation.",
12534
+ remediation: "Declare and implement an interface covering the service's public methods.",
12535
+ category: "architecture",
12536
+ aliases: ["require-interface-for-injected-service"],
12537
+ examples: [
12538
+ { id: "declared-service-port", title: "Implement the service port", outcome: "no-match", files: [{ path: "src/service.ts", source: "interface Handler { handle(): void }\nexport class RequestHandler implements Handler { constructor(private readonly store: TaskStore) {} handle(): void { this.store.handle(); } }" }], focusPath: "src/service.ts", expectedCount: 0, public: true },
12539
+ { id: "concrete-injected-service", title: "Do not expose only the concrete service", outcome: "match", files: [{ path: "src/service.ts", source: "export class RequestHandler { constructor(private readonly store: TaskStore) {} handle(): void { this.store.handle(); } }" }], focusPath: "src/service.ts", expectedCount: 1, public: true }
12540
+ ]
12541
+ };
11208
12542
  var CONFIGISH_TYPE_RE = /(?:Options|Opts|Config|Configuration|Settings|Params|Props|Args|Env|Environment|Callbacks|Flags)$/;
11209
12543
  var CONFIGISH_NAME_RE = /^(?:options|opts|config|configuration|settings|params|props|args|env|environment|callbacks|flags|logger|log|clock)$/i;
11210
12544
  var HTTP_TRANSPORT_TYPE_RE = /^(?:KyInstance|AxiosInstance|Session)$/;
@@ -11643,8 +12977,9 @@ function hasServicePort(node, methods, classes, interfaces) {
11643
12977
  }
11644
12978
  return methods.every((method) => combined.has(method));
11645
12979
  }
11646
- var require_interface_for_injected_service_default = createRule({
11647
- name: "require-interface-for-injected-service",
12980
+ var require_port_for_service_default = createRule({
12981
+ name: "require-port-for-service",
12982
+ documentation: requirePortForServiceDocumentation,
11648
12983
  meta: {
11649
12984
  type: "suggestion",
11650
12985
  docs: {
@@ -11709,6 +13044,16 @@ var require_interface_for_injected_service_default = createRule({
11709
13044
 
11710
13045
  // src/rules/require-static-next-matcher.ts
11711
13046
  import { AST_NODE_TYPES as AST_NODE_TYPES53 } from "@typescript-eslint/utils";
13047
+ var requireStaticNextMatcherDocumentation = {
13048
+ summary: "Require Next.js middleware and proxy matcher configuration to contain only build-time literals.",
13049
+ rationale: "Next.js must statically analyze matcher values at build time; computed values are ignored.",
13050
+ remediation: "Write matcher strings, arrays, and object fields as literals in the exported config.",
13051
+ category: "correctness",
13052
+ examples: [
13053
+ { id: "literal-matcher", title: "Use a literal matcher", outcome: "no-match", files: [{ path: "src/middleware.ts", source: 'export const config = { matcher: "/api/:path*" };' }], focusPath: "src/middleware.ts", expectedCount: 0, public: true },
13054
+ { id: "computed-matcher", title: "Do not compute the matcher", outcome: "match", files: [{ path: "src/middleware.ts", source: 'const matcher = "/api/:path*"; export const config = { matcher };' }], focusPath: "src/middleware.ts", expectedCount: 1, public: true }
13055
+ ]
13056
+ };
11712
13057
  var NEXT_ENTRY_FILE = /(?:^|[/\\])(?:middleware|proxy)\.[cm]?[jt]sx?$/u;
11713
13058
  function unwrapExpression3(node) {
11714
13059
  if (node.type === AST_NODE_TYPES53.TSAsExpression || node.type === AST_NODE_TYPES53.TSSatisfiesExpression || node.type === AST_NODE_TYPES53.TSNonNullExpression || node.type === AST_NODE_TYPES53.TSTypeAssertion) {
@@ -11743,6 +13088,7 @@ function propertyName2(property) {
11743
13088
  }
11744
13089
  var require_static_next_matcher_default = createRule({
11745
13090
  name: "require-static-next-matcher",
13091
+ documentation: requireStaticNextMatcherDocumentation,
11746
13092
  meta: {
11747
13093
  type: "problem",
11748
13094
  docs: {
@@ -11787,6 +13133,16 @@ var require_static_next_matcher_default = createRule({
11787
13133
 
11788
13134
  // src/rules/require-zod-form-validation.ts
11789
13135
  import { AST_NODE_TYPES as AST_NODE_TYPES54 } from "@typescript-eslint/utils";
13136
+ var requireZodFormValidationDocumentation = {
13137
+ summary: "Require Zod validation (`Schema.parse(...)` / `Schema.safeParse(...)`) when reading values out of a `FormData` object.",
13138
+ rationale: "FormData values are untrusted strings or files and need runtime validation before use.",
13139
+ remediation: "Read the value inside a Zod schema's `parse` or `safeParse` input.",
13140
+ category: "security",
13141
+ examples: [
13142
+ { id: "validated-form-value", title: "Validate the form value", outcome: "no-match", files: [{ path: "src/action.ts", source: "const input = UserSchema.parse({ name: formData.get('name') });" }], focusPath: "src/action.ts", expectedCount: 0, public: true },
13143
+ { id: "raw-form-value", title: "Do not use a raw form value", outcome: "match", files: [{ path: "src/action.ts", source: "const name = formData.get('name');" }], focusPath: "src/action.ts", expectedCount: 1, public: true }
13144
+ ]
13145
+ };
11790
13146
  var isZodParseCall = (node) => {
11791
13147
  if (node.type !== AST_NODE_TYPES54.CallExpression) return false;
11792
13148
  const callee = node.callee;
@@ -11827,6 +13183,7 @@ var isFormDataMethodCall = (node) => {
11827
13183
  };
11828
13184
  var require_zod_form_validation_default = createRule({
11829
13185
  name: "require-zod-form-validation",
13186
+ documentation: requireZodFormValidationDocumentation,
11830
13187
  meta: {
11831
13188
  type: "problem",
11832
13189
  docs: {
@@ -11915,11 +13272,22 @@ var require_zod_form_validation_default = createRule({
11915
13272
 
11916
13273
  // src/rules/store-insert-requires-on-conflict.ts
11917
13274
  import "@typescript-eslint/utils";
13275
+ var storeInsertRequiresOnConflictDocumentation = {
13276
+ summary: "Require an embedded SQL INSERT to carry ON CONFLICT; store writes replay under cron re-runs and queue redelivery and must be idempotent upserts.",
13277
+ rationale: "A replayed bare insert can duplicate data or fail on a uniqueness constraint.",
13278
+ remediation: "Add an appropriate `ON CONFLICT` action or supported replay-safe insert form.",
13279
+ category: "correctness",
13280
+ examples: [
13281
+ { id: "conflict-safe-insert", title: "Handle a replayed insert", outcome: "no-match", files: [{ path: "src/store.ts", source: "db.prepare(`INSERT INTO runs (id) VALUES (?) ON CONFLICT(id) DO NOTHING`).run();" }], focusPath: "src/store.ts", expectedCount: 0, public: true },
13282
+ { id: "bare-insert", title: "Do not issue a replay-unsafe insert", outcome: "match", files: [{ path: "src/store.ts", source: "db.prepare(`INSERT INTO runs (id) VALUES (?)`).run();" }], focusPath: "src/store.ts", expectedCount: 1, public: true }
13283
+ ]
13284
+ };
11918
13285
  var INSERT_WRITE = /\bINSERT\s+(?:OR\s+\w+\s+)?INTO\s+[\w."'`?$:@-]+\s*(?:\([^)]*\)\s*)?(?:VALUES|SELECT|DEFAULT\s+VALUES)\b/i;
11919
13286
  var CONFLICT_HANDLED = /\bON\s+CONFLICT\b|\bON\s+DUPLICATE\s+KEY\b|\bINSERT\s+OR\s+(?:IGNORE|REPLACE)\b/i;
11920
13287
  var INSERT_GATE = /insert/i;
11921
13288
  var store_insert_requires_on_conflict_default = createRule({
11922
13289
  name: "store-insert-requires-on-conflict",
13290
+ documentation: storeInsertRequiresOnConflictDocumentation,
11923
13291
  meta: {
11924
13292
  type: "problem",
11925
13293
  docs: {
@@ -11946,6 +13314,16 @@ var store_insert_requires_on_conflict_default = createRule({
11946
13314
 
11947
13315
  // src/rules/stepdown.ts
11948
13316
  import { AST_NODE_TYPES as AST_NODE_TYPES55, ASTUtils as ASTUtils11 } from "@typescript-eslint/utils";
13317
+ var stepdownDocumentation = {
13318
+ summary: "Place a private helper below its sole direct same-scope caller.",
13319
+ rationale: "Caller-first ordering lets a reader follow the main flow before descending into implementation details.",
13320
+ remediation: "Move the private helper immediately below its sole caller.",
13321
+ category: "maintainability",
13322
+ examples: [
13323
+ { id: "caller-before-helper", title: "Place the caller first", outcome: "no-match", files: [{ path: "src/run.ts", source: "function run() { return load(); }\nfunction load() { return 1; }" }], focusPath: "src/run.ts", expectedCount: 0, public: true },
13324
+ { id: "helper-before-caller", title: "Do not lead with a sole-caller helper", outcome: "match", files: [{ path: "src/run.ts", source: "function load() { return 1; }\nfunction run() { return load(); }" }], focusPath: "src/run.ts", expectedCount: 1, public: true }
13325
+ ]
13326
+ };
11949
13327
  function isFunction(node) {
11950
13328
  return node.type === AST_NODE_TYPES55.ArrowFunctionExpression || node.type === AST_NODE_TYPES55.FunctionDeclaration || node.type === AST_NODE_TYPES55.FunctionExpression;
11951
13329
  }
@@ -12300,6 +13678,7 @@ function classScope(context, node, computedReferenceNames) {
12300
13678
  }
12301
13679
  var stepdown_default = createRule({
12302
13680
  name: "stepdown",
13681
+ documentation: stepdownDocumentation,
12303
13682
  meta: {
12304
13683
  type: "suggestion",
12305
13684
  docs: { description: "Place a private helper below its sole direct same-scope caller." },
@@ -12336,6 +13715,16 @@ import {
12336
13715
  AST_NODE_TYPES as AST_NODE_TYPES56,
12337
13716
  ASTUtils as ASTUtils12
12338
13717
  } from "@typescript-eslint/utils";
13718
+ var zodNamingConventionDocumentation = {
13719
+ summary: "Enforce a consistent Zod schema naming convention \u2014 a `Z` prefix (`ZUser`) or a `Schema` suffix (`userSchema`); both are accepted by default.",
13720
+ rationale: "A recognizable schema name distinguishes runtime validators from ordinary values at each use site.",
13721
+ remediation: "Rename the schema with a `Z` prefix or `Schema` suffix, according to the configured convention.",
13722
+ category: "style",
13723
+ examples: [
13724
+ { id: "recognizable-schema-name", title: "Mark the value as a schema", outcome: "no-match", files: [{ path: "src/user.ts", source: "import { z } from 'zod';\nconst userSchema = z.object({ id: z.string() });" }], focusPath: "src/user.ts", expectedCount: 0, public: true },
13725
+ { id: "unmarked-schema-name", title: "Do not hide the schema behind a value name", outcome: "match", files: [{ path: "src/user.ts", source: "import { z } from 'zod';\nconst user = z.object({ id: z.string() });" }], focusPath: "src/user.ts", expectedCount: 1, public: true }
13726
+ ]
13727
+ };
12339
13728
  var CONVENTIONS = {
12340
13729
  prefix: { test: ZOD_PREFIX_RE, messageId: "zPrefix" },
12341
13730
  suffix: { test: ZOD_SUFFIX_RE, messageId: "schemaSuffix" },
@@ -12380,6 +13769,7 @@ var calleeChainRoot = (node) => {
12380
13769
  };
12381
13770
  var zod_naming_convention_default = createRule({
12382
13771
  name: "zod-naming-convention",
13772
+ documentation: zodNamingConventionDocumentation,
12383
13773
  meta: {
12384
13774
  type: "suggestion",
12385
13775
  docs: {
@@ -12462,6 +13852,7 @@ var zod_naming_convention_default = createRule({
12462
13852
  var renamedRules = {
12463
13853
  "jsdoc-restates-signature": "no-restated-jsdoc",
12464
13854
  "no-async-callback-in-waitfor": "no-async-callback-in-wait-for",
13855
+ "require-interface-for-injected-service": "require-port-for-service",
12465
13856
  "strict-test-assertions": "prefer-whole-object-assertion",
12466
13857
  "trailing-value-narration": "no-trailing-value-narration"
12467
13858
  };
@@ -12587,7 +13978,7 @@ var rules = {
12587
13978
  "prefer-zod-infer": prefer_zod_infer_default,
12588
13979
  "require-assert-never": require_assert_never_default,
12589
13980
  "require-fetch-timeout": require_fetch_timeout_default,
12590
- "require-interface-for-injected-service": require_interface_for_injected_service_default,
13981
+ "require-port-for-service": require_port_for_service_default,
12591
13982
  "require-static-next-matcher": require_static_next_matcher_default,
12592
13983
  "require-zod-form-validation": require_zod_form_validation_default,
12593
13984
  "store-insert-requires-on-conflict": store_insert_requires_on_conflict_default,
@@ -12596,7 +13987,7 @@ var rules = {
12596
13987
  };
12597
13988
  var meta = {
12598
13989
  name: "@sarj/eslint-plugin",
12599
- version: "12.0.0"
13990
+ version: "13.0.0"
12600
13991
  };
12601
13992
  var applicationOnlyRules = [
12602
13993
  "no-restricted-library-load",
@@ -12656,7 +14047,7 @@ var recommendedRules = {
12656
14047
  "@sarj/prefer-zod-infer": "error",
12657
14048
  "@sarj/require-assert-never": "error",
12658
14049
  "@sarj/require-fetch-timeout": "error",
12659
- "@sarj/require-interface-for-injected-service": "error",
14050
+ "@sarj/require-port-for-service": "error",
12660
14051
  "@sarj/require-static-next-matcher": "error",
12661
14052
  "@sarj/require-zod-form-validation": "error",
12662
14053
  "@sarj/store-insert-requires-on-conflict": "error",
@@ -12720,7 +14111,7 @@ var strictRules = {
12720
14111
  "@sarj/prefer-zod-infer": "error",
12721
14112
  "@sarj/require-assert-never": "error",
12722
14113
  "@sarj/require-fetch-timeout": "error",
12723
- "@sarj/require-interface-for-injected-service": "error",
14114
+ "@sarj/require-port-for-service": "error",
12724
14115
  "@sarj/require-static-next-matcher": "error",
12725
14116
  "@sarj/require-zod-form-validation": "error",
12726
14117
  "@sarj/store-insert-requires-on-conflict": "error",
@@ -12750,6 +14141,7 @@ var index_default = plugin;
12750
14141
  export {
12751
14142
  applicationOnlyRules,
12752
14143
  index_default as default,
14144
+ publicDocumentation,
12753
14145
  recommendedRules,
12754
14146
  renamedRules,
12755
14147
  retiredRules,