@blundergoat/gruff-ts 0.1.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CONTRIBUTING.md +87 -0
  3. package/LICENSE +21 -0
  4. package/README.md +303 -0
  5. package/SECURITY.md +45 -0
  6. package/bin/gruff-ts +25 -0
  7. package/docs/CONFIGURATION.md +220 -0
  8. package/docs/RELEASING.md +103 -0
  9. package/docs/REPORTS_AND_CI.md +156 -0
  10. package/fixtures/sample.ts +21 -0
  11. package/package.json +56 -0
  12. package/scripts/bump-version.sh +145 -0
  13. package/scripts/check.sh +4 -0
  14. package/scripts/npm-publish.sh +258 -0
  15. package/scripts/preflight-checks.sh +357 -0
  16. package/scripts/start-dev.sh +8 -0
  17. package/scripts/test-performance.sh +695 -0
  18. package/src/analyser.ts +461 -0
  19. package/src/baseline.ts +90 -0
  20. package/src/blocks.ts +687 -0
  21. package/src/class-rules.ts +326 -0
  22. package/src/cli-program.ts +326 -0
  23. package/src/cli.ts +19 -0
  24. package/src/comment-rules.ts +605 -0
  25. package/src/comment-scanner.ts +357 -0
  26. package/src/config.ts +622 -0
  27. package/src/constants.ts +4 -0
  28. package/src/context-doc-rules.ts +241 -0
  29. package/src/dashboard.ts +114 -0
  30. package/src/dead-code-rules.ts +183 -0
  31. package/src/discovery.ts +508 -0
  32. package/src/doc-rules.ts +368 -0
  33. package/src/findings-helpers.ts +108 -0
  34. package/src/findings.ts +45 -0
  35. package/src/fixture-purpose-rules.ts +334 -0
  36. package/src/fixtures/rule-catalogue-security-doctrine.ts +132 -0
  37. package/src/github-actions-rules.ts +413 -0
  38. package/src/line-rules.ts +538 -0
  39. package/src/naming-pushers.ts +191 -0
  40. package/src/project-config-rules.ts +555 -0
  41. package/src/project-rules.ts +545 -0
  42. package/src/report-renderers.ts +691 -0
  43. package/src/rule-list.ts +179 -0
  44. package/src/rules.ts +135 -0
  45. package/src/safety-rules.ts +355 -0
  46. package/src/scoring.ts +74 -0
  47. package/src/security-flow-rules.ts +112 -0
  48. package/src/sensitive-data-rules.ts +288 -0
  49. package/src/source-text.ts +722 -0
  50. package/src/test-block-rules.ts +347 -0
  51. package/src/test-fixtures.ts +621 -0
  52. package/src/text-scans.ts +193 -0
  53. package/src/types.ts +113 -0
  54. package/tsconfig.json +15 -0
@@ -0,0 +1,179 @@
1
+ // Console command catalogue, shell completion scripts, and rule-list renderers for CLI surfaces.
2
+ import { VERSION } from "./constants.ts";
3
+ import { ruleDescriptors } from "./rules.ts";
4
+
5
+ type RuleListFormat = "text" | "json";
6
+ type CompletionShell = "bash" | "fish" | "zsh";
7
+
8
+ // Pre-formatted token strings, not arrays - the shell-specific renderers paste them straight into
9
+ // completion scripts (bash `$commands`, zsh `_describe`, etc.) so the formatting must already be shell-safe.
10
+ interface CompletionContext {
11
+ commands: string;
12
+ options: string;
13
+ }
14
+
15
+ const ANSI_GREEN = "\x1b[32m";
16
+ const ANSI_YELLOW = "\x1b[33m";
17
+ const ANSI_RESET_FG = "\x1b[39m";
18
+
19
+ const CONSOLE_COMMANDS = [
20
+ { name: "analyse", description: "Run gruff analysis." },
21
+ { name: "completion", description: "Dump the shell completion script" },
22
+ { name: "dashboard", description: "Serve the local gruff dashboard." },
23
+ { name: "help", description: "Display help for a command" },
24
+ { name: "list", description: "List commands" },
25
+ { name: "list-rules", description: "List gruff rule metadata." },
26
+ { name: "report", description: "Render a gruff report to stdout or a file." },
27
+ {
28
+ name: "summary",
29
+ description:
30
+ "Print a compact digest of a scan: per-pillar finding counts, top rules, and top file offenders. Runs the analyser once and renders only the summary; no per-finding spam.",
31
+ },
32
+ ] as const;
33
+
34
+ // Rule catalogue renderer. JSON is the integration surface for docs and audits; text is allowed to
35
+ // stay plain because it is only for terminal inspection and should not become another schema.
36
+ function renderRuleList(format: RuleListFormat): string {
37
+ const descriptors = ruleDescriptors();
38
+ if (format === "json") {
39
+ return `${JSON.stringify({ tool: { name: "gruff-ts", version: VERSION }, rules: descriptors }, null, 2)}\n`;
40
+ }
41
+ const lines = ["gruff-ts " + VERSION + ` rules (${descriptors.length})`, ""];
42
+ for (const descriptor of descriptors) {
43
+ const threshold = typeof descriptor.threshold === "number" ? ` | threshold: ${descriptor.threshold}` : "";
44
+ const options = descriptor.optionKeys && descriptor.optionKeys.length > 0 ? ` | options: ${descriptor.optionKeys.join(",")}` : "";
45
+ lines.push(`${descriptor.ruleId} | ${descriptor.pillar} | ${descriptor.severity} | ${descriptor.confidence} | ${descriptor.description}${threshold}${options}`);
46
+ }
47
+ return `${lines.join("\n")}\n`;
48
+ }
49
+
50
+ // The Symfony-style command catalogue shown for `gruff-ts`, `gruff-ts list`, and `gruff-ts help`.
51
+ // `shouldUseAnsi` is autodetected by the caller from TTY and `--ansi` flags, never from this layer.
52
+ function renderConsoleList(shouldUseAnsi = false): string {
53
+ const listCommand = ansiWrap("list", ANSI_GREEN, shouldUseAnsi);
54
+ return [
55
+ "gruff-ts " + ansiWrap(VERSION, ANSI_GREEN, shouldUseAnsi),
56
+ "",
57
+ ansiWrap("Usage:", ANSI_YELLOW, shouldUseAnsi),
58
+ " command [options] [arguments]",
59
+ "",
60
+ ansiWrap("Options:", ANSI_YELLOW, shouldUseAnsi),
61
+ formatConsoleRow("-h, --help", `Display help for the given command. When no command is given display help for the ${listCommand} command`, 22, shouldUseAnsi),
62
+ formatConsoleRow(" --silent", "Do not output any message", 22, shouldUseAnsi),
63
+ formatConsoleRow("-q, --quiet", "Only errors are displayed. All other output is suppressed", 22, shouldUseAnsi),
64
+ formatConsoleRow("-V, --version", "Display this application version", 22, shouldUseAnsi),
65
+ formatConsoleRow(" --ansi|--no-ansi", "Force (or disable --no-ansi) ANSI output", 22, shouldUseAnsi),
66
+ formatConsoleRow("-n, --no-interaction", "Do not ask any interactive question", 22, shouldUseAnsi),
67
+ formatConsoleRow("-v|vv|vvv, --verbose", "Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug", 22, shouldUseAnsi),
68
+ "",
69
+ ansiWrap("Available commands:", ANSI_YELLOW, shouldUseAnsi),
70
+ ...CONSOLE_COMMANDS.map((command) => formatConsoleRow(command.name, command.description, 12, shouldUseAnsi)),
71
+ ].join("\n") + "\n";
72
+ }
73
+
74
+ // Two-column row: label padded to `width`, then description. Width is fixed per section so the
75
+ // catalogue lines up regardless of label length - matches Symfony console output conventions.
76
+ function formatConsoleRow(label: string, description: string, width: number, shouldUseAnsi: boolean): string {
77
+ const paddedLabel = ansiWrap(label, ANSI_GREEN, shouldUseAnsi);
78
+ const padding = " ".repeat(Math.max(1, width - label.length));
79
+ const rowDescription = description;
80
+ return ` ${paddedLabel}${padding}${rowDescription}`;
81
+ }
82
+
83
+ // `shouldUseAnsi` is the only gate. We never sniff `process.stdout.isTTY` here because the caller may be
84
+ // rendering to a file or capture buffer where ANSI codes would be garbage.
85
+ function ansiWrap(text: string, color: string, shouldUseAnsi: boolean): string {
86
+ if (!shouldUseAnsi) {
87
+ return text;
88
+ }
89
+ const ansiColor = color;
90
+ return `${ansiColor}${text}${ANSI_RESET_FG}`;
91
+ }
92
+
93
+ // Dispatches by shell with bash as the default - chosen because bash completion is most likely to
94
+ // be installed and least likely to break silently if the user pipes the output through `source`.
95
+ function renderCompletionScript(shell: CompletionShell): string {
96
+ const context = completionContext();
97
+ if (shell === "fish") {
98
+ return renderFishCompletion(context);
99
+ }
100
+ if (shell === "zsh") {
101
+ return renderZshCompletion(context);
102
+ }
103
+ return renderBashCompletion(context);
104
+ }
105
+
106
+ // Filters out `help` from the completion list - Commander handles it implicitly and exposing it
107
+ // would suggest `gruff-ts help` is a real subcommand on equal footing with `analyse`.
108
+ function completionContext(): CompletionContext {
109
+ return {
110
+ commands: CONSOLE_COMMANDS.filter((command) => command.name !== "help").map((command) => command.name).join(" "),
111
+ options: "-h --help --silent -q --quiet -V --version --ansi --no-ansi -n --no-interaction -v -vv -vvv --verbose",
112
+ };
113
+ }
114
+
115
+ // Fish's `complete` builtin is invoked once per token. Trailing newline matters - fish ignores
116
+ // the final entry without one.
117
+ function renderFishCompletion(context: CompletionContext): string {
118
+ return [
119
+ "complete -c gruff-ts -f",
120
+ ...context.commands.split(" ").map((command) => `complete -c gruff-ts -n '__fish_use_subcommand' -a '${command}'`),
121
+ ...context.options.split(" ").map((option) => `complete -c gruff-ts -a '${option}'`),
122
+ "",
123
+ ].join("\n");
124
+ }
125
+
126
+ // `#compdef gruff-ts` must be on line 1 - zsh's autoloader rejects the file otherwise. Uses
127
+ // `_arguments` rather than `_describe` so option completion still works after a subcommand.
128
+ function renderZshCompletion(context: CompletionContext): string {
129
+ return [
130
+ "#compdef gruff-ts",
131
+ "_gruff_ts() {",
132
+ " local -a commands",
133
+ ` commands=(${context.commands})`,
134
+ " _arguments '1:command:->commands' '*::arg:->args'",
135
+ " case $state in",
136
+ " commands) _describe 'command' commands ;;",
137
+ " args) _values 'option' " + context.options.split(" ").map((option) => `'${option}'`).join(" ") + " ;;",
138
+ " esac",
139
+ "}",
140
+ "_gruff_ts \"$@\"",
141
+ "",
142
+ ].join("\n");
143
+ }
144
+
145
+ // Bash completion: a function registered via `complete -F`. The `--format` / `--fail-on` value
146
+ // lists are duplicated from `cli-program.ts` because completion runs without loading the analyser.
147
+ function renderBashCompletion({ commands, options }: CompletionContext): string {
148
+ const commandsLine = ` commands=\"${commands}\"`;
149
+ const optionsLine = ` options=\"${options}\"`;
150
+ return [
151
+ "_gruff_ts_completion() {",
152
+ " local current previous commands options",
153
+ " COMPREPLY=()",
154
+ " current=\"${COMP_WORDS[COMP_CWORD]}\"",
155
+ " previous=\"${COMP_WORDS[COMP_CWORD-1]}\"",
156
+ commandsLine,
157
+ optionsLine,
158
+ " if [ \"$COMP_CWORD\" -eq 1 ]; then",
159
+ " COMPREPLY=( $(compgen -W \"$commands $options\" -- \"$current\") )",
160
+ " else",
161
+ " case \"$previous\" in",
162
+ " --format) COMPREPLY=( $(compgen -W \"text json html markdown github hotspot sarif\" -- \"$current\") ) ;;",
163
+ " --fail-on) COMPREPLY=( $(compgen -W \"none advisory warning error\" -- \"$current\") ) ;;",
164
+ " *) COMPREPLY=( $(compgen -W \"$options\" -- \"$current\") ) ;;",
165
+ " esac",
166
+ " fi",
167
+ "}",
168
+ "complete -F _gruff_ts_completion gruff-ts",
169
+ "",
170
+ ].join("\n");
171
+ }
172
+
173
+ // Bash is the fallback for any unknown shell name - see `renderCompletionScript` for why bash wins.
174
+ function completionShell(shellName: unknown): CompletionShell {
175
+ return shellName === "fish" || shellName === "zsh" ? shellName : "bash";
176
+ }
177
+
178
+ export type { RuleListFormat, CompletionShell };
179
+ export { renderRuleList, renderConsoleList, renderCompletionScript, completionShell };
package/src/rules.ts ADDED
@@ -0,0 +1,135 @@
1
+ // Rule descriptor catalogue that defines the public rule metadata emitted by list-rules and reports.
2
+ import type { RuleDescriptor } from "./types.ts";
3
+
4
+ const RULE_DESCRIPTORS: readonly RuleDescriptor[] = [
5
+ { ruleId: "complexity.cognitive", pillar: "complexity", severity: "warning", confidence: "high", description: "Flags functions with high combined branch and nesting complexity.", remediation: "Split nested decisions into smaller named functions.", threshold: 15 },
6
+ { ruleId: "complexity.cyclomatic", pillar: "complexity", severity: "warning", confidence: "high", description: "Flags functions with many independent branch paths.", remediation: "Reduce branching or move policy tables out of imperative code.", threshold: 15 },
7
+ { ruleId: "complexity.npath", pillar: "complexity", severity: "warning", confidence: "medium", description: "Flags functions with high approximate NPath complexity.", remediation: "Break apart compound branch combinations.", threshold: 200 },
8
+ { ruleId: "dead-code.unused-private-method", pillar: "dead-code", severity: "advisory", confidence: "low", description: "Flags private methods without an apparent same-file call site.", remediation: "Remove the method or add a direct call site." },
9
+ { ruleId: "design.circular-import", pillar: "design", severity: "warning", confidence: "medium", description: "Flags simple relative import cycles inside the discovered source set.", remediation: "Extract the shared contract or invert one dependency." },
10
+ { ruleId: "design.deep-relative-import", pillar: "design", severity: "advisory", confidence: "medium", description: "Flags relative imports that climb too many parent directories.", remediation: "Move the shared module closer or add a local boundary.", threshold: 2 },
11
+ { ruleId: "design.god-function", pillar: "design", severity: "warning", confidence: "high", description: "Flags functions that are both long and complex.", remediation: "Split responsibilities into smaller functions." },
12
+ { ruleId: "design.large-module-concentration", pillar: "design", severity: "advisory", confidence: "medium", description: "Flags a production module that dominates project source lines.", remediation: "Split unrelated responsibilities once stable seams are visible.", threshold: 55, optionKeys: ["minFiles", "minLines"] },
13
+ { ruleId: "design.package-bin-missing", pillar: "design", severity: "warning", confidence: "high", description: "Flags package bin entries that point at missing files.", remediation: "Update the bin path or add the executable file." },
14
+ { ruleId: "design.package-bin-not-executable", pillar: "design", severity: "warning", confidence: "high", description: "Flags package bin targets that are not executable.", remediation: "Make the bin target executable and keep its shebang valid." },
15
+ { ruleId: "docs.fixture-purpose-missing", pillar: "documentation", severity: "advisory", confidence: "medium", description: "Flags large or scanner-relevant fixtures without a nearby purpose comment.", remediation: "Explain what scanner path, regression, or fixture behavior the source covers." },
16
+ { ruleId: "docs.magic-threshold-without-rationale", pillar: "documentation", severity: "advisory", confidence: "medium", description: "Flags threshold-like numeric values without a nearby rationale comment.", remediation: "Explain the threshold, limit, budget, or default near the value." },
17
+ { ruleId: "docs.missing-error-behavior-doc", pillar: "documentation", severity: "advisory", confidence: "medium", description: "Flags commented functions whose error behavior is not described.", remediation: "Document thrown errors, diagnostics, exits, reports, or recovery behavior." },
18
+ { ruleId: "docs.missing-file-overview", pillar: "documentation", severity: "advisory", confidence: "medium", description: "Flags source files without a top-of-file purpose comment.", remediation: "Add a brief file overview before imports or declarations." },
19
+ { ruleId: "docs.missing-function-doc", pillar: "documentation", severity: "advisory", confidence: "medium", description: "Flags functions without a leading maintainer comment.", remediation: "Add a short JSDoc or line comment explaining the function's purpose." },
20
+ { ruleId: "docs.missing-interface-doc", pillar: "documentation", severity: "advisory", confidence: "medium", description: "Flags interfaces without a leading maintainer comment.", remediation: "Add a short JSDoc or line comment explaining the interface contract." },
21
+ { ruleId: "docs.missing-invariant-doc", pillar: "documentation", severity: "advisory", confidence: "medium", description: "Flags commented declarations that own schema, fingerprint, baseline, or determinism contracts without saying so.", remediation: "Document the invariant, contract, schema, fingerprint, or deterministic behavior." },
22
+ { ruleId: "docs.missing-param-tag", pillar: "documentation", severity: "advisory", confidence: "medium", description: "Flags documented exports with parameters missing @param tags.", remediation: "Document every current parameter in the JSDoc." },
23
+ { ruleId: "docs.missing-public-doc", pillar: "documentation", severity: "advisory", confidence: "medium", description: "Flags exported class, type, and enum APIs without a nearby doc comment.", remediation: "Add a short comment explaining the public API contract." },
24
+ { ruleId: "docs.missing-return-tag", pillar: "documentation", severity: "advisory", confidence: "medium", description: "Flags documented non-void exports without @returns.", remediation: "Document the returned value or remove stale JSDoc." },
25
+ { ruleId: "docs.missing-side-effect-doc", pillar: "documentation", severity: "advisory", confidence: "medium", description: "Flags commented functions that perform observable side effects without naming them.", remediation: "Name the filesystem, process, environment, network, or persistence side effect." },
26
+ { ruleId: "docs.missing-why-for-complex-code", pillar: "documentation", severity: "advisory", confidence: "medium", description: "Flags comments on complex functions that do not explain why the shape exists.", remediation: "Explain the tradeoff, compatibility reason, or invariant behind the complex control flow." },
27
+ { ruleId: "docs.stale-comment", pillar: "documentation", severity: "advisory", confidence: "medium", description: "Flags comments that reference missing files, unknown rules, stale CLI flags, or the wrong declaration.", remediation: "Update the comment or add historical context explaining why it remains useful." },
28
+ { ruleId: "docs.stale-param-tag", pillar: "documentation", severity: "advisory", confidence: "medium", description: "Flags @param tags for parameters no longer in the signature.", remediation: "Remove stale tags or update the function signature." },
29
+ { ruleId: "docs.suppression-without-rationale", pillar: "documentation", severity: "advisory", confidence: "medium", description: "Flags lint, formatter, coverage, or tool suppressions without a maintainer rationale.", remediation: "Explain why the suppression is intentional, a false positive, or tracked elsewhere." },
30
+ { ruleId: "docs.todo-density", pillar: "documentation", severity: "advisory", confidence: "high", description: "Flags files with a high count of TODO/FIXME markers (opt-in, disabled by default).", remediation: "Resolve stale markers, link them to tracked work, or enable docs.todo-without-tracking for context-aware coverage.", threshold: 4 },
31
+ { ruleId: "docs.todo-without-tracking", pillar: "documentation", severity: "advisory", confidence: "high", description: "Flags TODO, FIXME, HACK, and XXX comments without tracking context.", remediation: "Attach a tracking URL, issue id, owner, date, ADR, or .goat-flow task reference." },
32
+ { ruleId: "docs.useless-docblock", pillar: "documentation", severity: "advisory", confidence: "medium", description: "Flags comments or docblocks that only restate the symbol name.", remediation: "Replace the comment with useful contract or behavior detail." },
33
+ { ruleId: "modernisation.date-now-candidate", pillar: "modernisation", severity: "advisory", confidence: "high", description: "Flags verbose current-time expressions that can use Date.now().", remediation: "Use Date.now() for current epoch milliseconds." },
34
+ { ruleId: "modernisation.double-cast", pillar: "modernisation", severity: "warning", confidence: "medium", description: "Flags casts through unknown or any into another type.", remediation: "Use a parser, type guard, or narrower assertion." },
35
+ { ruleId: "modernisation.loose-equality", pillar: "modernisation", severity: "advisory", confidence: "medium", description: "Flags loose equality comparisons that may coerce values.", remediation: "Use === or !== unless intentionally checking nullish values." },
36
+ { ruleId: "modernisation.non-null-assertion", pillar: "modernisation", severity: "warning", confidence: "medium", description: "Flags non-null assertions that bypass null checks.", remediation: "Narrow the value or handle null and undefined explicitly." },
37
+ { ruleId: "modernisation.nullish-coalescing-candidate", pillar: "modernisation", severity: "advisory", confidence: "medium", description: "Flags || fallbacks that may erase valid falsy values.", remediation: "Use ?? when only null or undefined should fall back." },
38
+ { ruleId: "modernisation.object-spread-candidate", pillar: "modernisation", severity: "advisory", confidence: "medium", description: "Flags Object.assign({}, ...) cloning that can use object spread.", remediation: "Use object spread when shallow-cloning plain objects." },
39
+ { ruleId: "modernisation.optional-chaining-candidate", pillar: "modernisation", severity: "advisory", confidence: "medium", description: "Flags repeated guard-and-property access patterns.", remediation: "Use optional chaining for clearer null-safe access." },
40
+ { ruleId: "modernisation.public-property", pillar: "modernisation", severity: "advisory", confidence: "high", description: "Flags public class properties that expose representation.", remediation: "Prefer readonly properties or accessors when invariants matter." },
41
+ { ruleId: "modernisation.readonly-property-candidate", pillar: "modernisation", severity: "advisory", confidence: "medium", description: "Flags class properties that appear readonly-worthy.", remediation: "Mark the property readonly when mutation is not part of the contract." },
42
+ { ruleId: "modernisation.ts-comment-without-rationale", pillar: "modernisation", severity: "warning", confidence: "medium", description: "Flags TypeScript suppression comments without a rationale.", remediation: "Add a short reason or remove the suppression." },
43
+ { ruleId: "modernisation.tsconfig-exact-optional-disabled", pillar: "modernisation", severity: "warning", confidence: "high", description: "Flags tsconfig files without exactOptionalPropertyTypes enabled.", remediation: "Enable exactOptionalPropertyTypes unless migration is blocked." },
44
+ { ruleId: "modernisation.tsconfig-index-safety-disabled", pillar: "modernisation", severity: "warning", confidence: "high", description: "Flags tsconfig files without noUncheckedIndexedAccess enabled.", remediation: "Enable noUncheckedIndexedAccess unless migration is blocked." },
45
+ { ruleId: "modernisation.tsconfig-strict-disabled", pillar: "modernisation", severity: "warning", confidence: "high", description: "Flags tsconfig files without strict mode enabled.", remediation: "Enable strict unless migration is blocked." },
46
+ { ruleId: "modernisation.var-declaration", pillar: "modernisation", severity: "advisory", confidence: "high", description: "Flags var declarations.", remediation: "Use let or const with the narrowest useful scope." },
47
+ { ruleId: "naming.abbreviation", pillar: "naming", severity: "advisory", confidence: "medium", description: "Flags identifiers in the project abbreviation denylist (opt-in, disabled by default).", remediation: "Use the full domain term or add the abbreviation to allowlists.acceptedAbbreviations." },
48
+ { ruleId: "naming.acronym-case", pillar: "naming", severity: "advisory", confidence: "medium", description: "Flags mixed casings of a known acronym in one file.", remediation: "Use one casing for each acronym throughout the file." },
49
+ { ruleId: "naming.boolean-prefix", pillar: "naming", severity: "advisory", confidence: "medium", description: "Flags boolean names without intent-revealing prefixes on declarations, function parameters (typed `: boolean` or with `= true|false` default), and interface/type-literal fields.", remediation: "Use prefixes such as is, has, can, should, or will." },
50
+ { ruleId: "naming.class-file-mismatch", pillar: "naming", severity: "advisory", confidence: "medium", description: "Flags exported classes whose name differs from the file name.", remediation: "Rename the class or file so the primary export is easy to locate." },
51
+ { ruleId: "naming.generic-function", pillar: "naming", severity: "advisory", confidence: "high", description: "Flags generic function names that hide intent.", remediation: "Name the domain action instead of a generic operation." },
52
+ { ruleId: "naming.generic-parameter", pillar: "naming", severity: "advisory", confidence: "medium", description: "Flags placeholder parameter names in multi-parameter, long, exported, or complex functions.", remediation: "Use a name that describes the parameter's role.", optionKeys: ["minCyclomatic", "minLineCount", "minParameters"] },
53
+ { ruleId: "naming.hungarian-notation", pillar: "naming", severity: "advisory", confidence: "medium", description: "Flags identifiers named after storage type prefixes.", remediation: "Name the domain concept instead of the storage type." },
54
+ { ruleId: "naming.identifier-quality", pillar: "naming", severity: "advisory", confidence: "medium", description: "Flags placeholder or numbered identifiers on declarations, function parameters, and destructured locals.", remediation: "Use names that explain domain role or intent." },
55
+ { ruleId: "naming.inconsistent-casing", pillar: "naming", severity: "advisory", confidence: "medium", description: "Flags the same canonical identifier appearing in two different surface forms (for example CONSTANT_CASE and camelCase) in one file.", remediation: "Choose one form and use it consistently within the file." },
56
+ { ruleId: "naming.negative-boolean", pillar: "naming", severity: "advisory", confidence: "medium", description: "Flags boolean identifiers framed as a negation on declarations, parameters, and interface fields.", remediation: "Invert the framing so callers do not read a double negation." },
57
+ { ruleId: "naming.short-variable", pillar: "naming", severity: "advisory", confidence: "medium", description: "Flags very short variable names outside common loop counters; covers declarations, function parameters, and destructured locals.", remediation: "Use a name that describes the domain role." },
58
+ { ruleId: "security.async-foreach", pillar: "security", severity: "warning", confidence: "medium", description: "Flags async callbacks passed to forEach.", remediation: "Use for...of with await, Promise.all, or an explicit queue." },
59
+ { ruleId: "security.disabled-tls-verification", pillar: "security", severity: "error", confidence: "high", description: "Flags code that disables TLS certificate verification.", remediation: "Remove the override and fix certificate trust at the source." },
60
+ { ruleId: "security.document-write", pillar: "security", severity: "warning", confidence: "high", description: "Flags document.write usage.", remediation: "Use safe DOM APIs and encode untrusted content." },
61
+ { ruleId: "security.dynamic-regexp", pillar: "security", severity: "warning", confidence: "medium", description: "Flags external input used to construct regular expressions.", remediation: "Use a fixed pattern, escape user input, or enforce strict length and character limits." },
62
+ { ruleId: "security.eval-call", pillar: "security", severity: "error", confidence: "high", description: "Flags eval() dynamic code execution.", remediation: "Replace eval with explicit parsing or a safe dispatch table." },
63
+ { ruleId: "security.floating-promise", pillar: "security", severity: "warning", confidence: "medium", description: "Flags promise-like calls without await, return, or void.", remediation: "Await it, return it, or mark intentional fire-and-forget with void." },
64
+ { ruleId: "security.github-actions-broad-permissions", pillar: "security", severity: "warning", confidence: "medium", description: "Flags GitHub Actions workflows that grant broad write permissions.", remediation: "Reduce workflow permissions to read-only by default and grant write scopes only to trusted jobs." },
65
+ { ruleId: "security.github-actions-pull-request-target", pillar: "security", severity: "warning", confidence: "medium", description: "Flags pull_request_target workflows paired with risky execution or trust context.", remediation: "Use pull_request for untrusted code or isolate checkout, secrets, and write permissions behind trust checks." },
66
+ { ruleId: "security.github-actions-remote-shell", pillar: "security", severity: "warning", confidence: "medium", description: "Flags workflow run steps that pipe remote downloads to a shell.", remediation: "Vendor the installer, pin an audited action, or verify downloaded content before execution." },
67
+ { ruleId: "security.github-actions-secrets-in-pr", pillar: "security", severity: "warning", confidence: "medium", description: "Flags pull request workflows that reference GitHub secrets.", remediation: "Avoid exposing secrets to pull request workflows unless the code path is trusted and tightly gated." },
68
+ { ruleId: "security.github-actions-unpinned-action", pillar: "security", severity: "warning", confidence: "medium", description: "Flags third-party GitHub Actions that are not pinned to a full commit SHA.", remediation: "Pin third-party actions to a reviewed 40-character commit SHA and update them deliberately." },
69
+ { ruleId: "security.inner-html", pillar: "security", severity: "warning", confidence: "high", description: "Flags innerHTML assignment.", remediation: "Use safe DOM APIs or sanitize trusted HTML centrally." },
70
+ { ruleId: "security.insecure-random", pillar: "security", severity: "warning", confidence: "high", description: "Flags Math.random usage in source.", remediation: "Use crypto-backed randomness for security-sensitive values." },
71
+ { ruleId: "security.javascript-url", pillar: "security", severity: "error", confidence: "high", description: "Flags javascript: URL literals that execute script.", remediation: "Use safe routes or event handlers instead of executable URL strings." },
72
+ { ruleId: "security.new-function", pillar: "security", severity: "error", confidence: "high", description: "Flags Function constructor dynamic code execution.", remediation: "Replace dynamic construction with explicit functions or dispatch." },
73
+ { ruleId: "security.open-redirect-candidate", pillar: "security", severity: "warning", confidence: "medium", description: "Flags external input sent to redirect or navigation sinks.", remediation: "Redirect only to relative paths or destinations from an allowlist." },
74
+ { ruleId: "security.path-traversal-candidate", pillar: "security", severity: "warning", confidence: "medium", description: "Flags external input sent to filesystem path sinks.", remediation: "Validate paths against an allowlist and resolve them under a safe root." },
75
+ { ruleId: "security.process-exec", pillar: "security", severity: "warning", confidence: "high", description: "Flags child-process execution calls.", remediation: "Validate arguments and prefer fixed command vectors." },
76
+ { ruleId: "security.proto-access", pillar: "security", severity: "warning", confidence: "medium", description: "Flags direct __proto__ access that can enable prototype pollution.", remediation: "Use Object.getPrototypeOf, Object.create(null), or validated keys." },
77
+ { ruleId: "security.remote-install-script", pillar: "security", severity: "error", confidence: "medium", description: "Flags package scripts that pipe remote content to a shell.", remediation: "Vendor, pin, or remove remote shell execution." },
78
+ { ruleId: "security.risky-lifecycle-script", pillar: "security", severity: "warning", confidence: "medium", description: "Flags package lifecycle scripts that run automatically.", remediation: "Move setup behind an explicit command when possible." },
79
+ { ruleId: "security.sql-concatenation", pillar: "security", severity: "warning", confidence: "high", description: "Flags SQL text composed with runtime string interpolation.", remediation: "Use parameterized queries or query builders." },
80
+ { ruleId: "security.ssrf-candidate", pillar: "security", severity: "warning", confidence: "medium", description: "Flags external input sent to network request sinks.", remediation: "Validate destinations against an allowlist and block internal hosts." },
81
+ { ruleId: "security.string-timer", pillar: "security", severity: "warning", confidence: "high", description: "Flags string callbacks passed to timers.", remediation: "Pass a function callback instead of source text." },
82
+ { ruleId: "security.throw-non-error", pillar: "security", severity: "warning", confidence: "medium", description: "Flags thrown non-Error values.", remediation: "Throw an Error subclass with a clear message." },
83
+ { ruleId: "security.url-dependency", pillar: "security", severity: "warning", confidence: "medium", description: "Flags dependencies installed from URL or git specs.", remediation: "Prefer registry versions that can be locked and audited." },
84
+ { ruleId: "security.weak-crypto", pillar: "security", severity: "warning", confidence: "high", description: "Flags weak crypto primitives such as md5, sha1, or createCipher.", remediation: "Use modern algorithms and authenticated encryption." },
85
+ { ruleId: "sensitive-data.api-key-pattern", pillar: "sensitive-data", severity: "error", confidence: "high", description: "Flags vendor API key patterns.", remediation: "Remove the secret and load it from a secure runtime source." },
86
+ { ruleId: "sensitive-data.aws-access-key", pillar: "sensitive-data", severity: "error", confidence: "high", description: "Flags AWS access key looking values.", remediation: "Remove the key and rotate it immediately." },
87
+ { ruleId: "sensitive-data.database-url-password", pillar: "sensitive-data", severity: "error", confidence: "high", description: "Flags database URLs that include passwords.", remediation: "Move credentials into a secret store or runtime environment." },
88
+ { ruleId: "sensitive-data.hardcoded-env-value", pillar: "sensitive-data", severity: "error", confidence: "medium", description: "Flags environment-style secret values committed in text.", remediation: "Load secret-like values from secure runtime configuration.", threshold: 16 },
89
+ { ruleId: "sensitive-data.high-entropy-string", pillar: "sensitive-data", severity: "error", confidence: "medium", description: "Flags high-entropy string literals that may be secrets.", remediation: "Remove the value and load it from a secure runtime source.", threshold: 32 },
90
+ { ruleId: "sensitive-data.jwt-token", pillar: "sensitive-data", severity: "error", confidence: "high", description: "Flags JWT-looking token literals.", remediation: "Remove the token and rotate the credential if real." },
91
+ { ruleId: "sensitive-data.pii-pattern", pillar: "sensitive-data", severity: "error", confidence: "high", description: "Flags PII-like identifier patterns.", remediation: "Remove personal data from source and fixtures." },
92
+ { ruleId: "sensitive-data.private-key", pillar: "sensitive-data", severity: "error", confidence: "high", description: "Flags private key block markers.", remediation: "Remove the key material and rotate affected credentials." },
93
+ { ruleId: "size.file-length", pillar: "size", severity: "warning", confidence: "high", description: "Flags files longer than the configured threshold.", remediation: "Split unrelated responsibilities into smaller files.", threshold: 750 },
94
+ { ruleId: "size.function-length", pillar: "size", severity: "warning", confidence: "high", description: "Flags functions longer than the configured threshold.", remediation: "Extract named helpers or split workflows.", threshold: 200 },
95
+ { ruleId: "size.parameter-count", pillar: "size", severity: "warning", confidence: "high", description: "Flags functions with too many parameters.", remediation: "Group related options or reduce the function's responsibility.", threshold: 7 },
96
+ { ruleId: "size.stylesheet-length", pillar: "size", severity: "warning", confidence: "high", description: "Flags stylesheets longer than the configured threshold.", remediation: "Split unrelated component styles into smaller files.", threshold: 1500 },
97
+ { ruleId: "test-quality.conditional-logic", pillar: "test-quality", severity: "advisory", confidence: "high", description: "Flags tests with conditional logic.", remediation: "Split branch-specific expectations into separate tests." },
98
+ { ruleId: "test-quality.exception-type-only", pillar: "test-quality", severity: "advisory", confidence: "high", description: "Flags tests that only assert exception type.", remediation: "Assert message, code, or observable behavior as well." },
99
+ { ruleId: "test-quality.global-state-mutation", pillar: "test-quality", severity: "warning", confidence: "high", description: "Flags tests mutating process or global runtime state.", remediation: "Isolate state changes and restore them around the test." },
100
+ { ruleId: "test-quality.loop-in-test", pillar: "test-quality", severity: "advisory", confidence: "high", description: "Flags loops inside test bodies.", remediation: "Use table tests or separate named cases." },
101
+ { ruleId: "test-quality.magic-number-assertion", pillar: "test-quality", severity: "advisory", confidence: "medium", description: "Flags assertions against unexplained numeric literals.", remediation: "Name expected values or assert domain-specific outcomes." },
102
+ { ruleId: "test-quality.missing-nearby-test", pillar: "test-quality", severity: "advisory", confidence: "medium", description: "Flags exported production files without nearby tests.", remediation: "Add a focused test beside the source or in a nearby test directory." },
103
+ { ruleId: "test-quality.mock-only-test", pillar: "test-quality", severity: "advisory", confidence: "high", description: "Flags tests that only verify mock interaction.", remediation: "Assert observable behavior in addition to collaboration." },
104
+ { ruleId: "test-quality.no-assertions", pillar: "test-quality", severity: "warning", confidence: "high", description: "Flags tests without apparent assertions.", remediation: "Add assertions for observable behavior." },
105
+ { ruleId: "test-quality.no-throw-only-test", pillar: "test-quality", severity: "advisory", confidence: "high", description: "Flags tests that only assert code does not throw.", remediation: "Assert the observable result or state change." },
106
+ { ruleId: "test-quality.only-skip", pillar: "test-quality", severity: "advisory", confidence: "high", description: "Flags focused or skipped test markers.", remediation: "Remove .only and either enable or delete skipped tests." },
107
+ { ruleId: "test-quality.setup-bloat", pillar: "test-quality", severity: "advisory", confidence: "medium", description: "Flags tests with too much setup before the first assertion.", remediation: "Extract builders or reduce fixture setup.", threshold: 12 },
108
+ { ruleId: "test-quality.sleep-in-test", pillar: "test-quality", severity: "advisory", confidence: "high", description: "Flags sleeps in tests.", remediation: "Synchronize on behavior instead of wall-clock time." },
109
+ { ruleId: "test-quality.snapshot-only-test", pillar: "test-quality", severity: "advisory", confidence: "high", description: "Flags tests that rely only on snapshots.", remediation: "Add targeted assertions for important behavior." },
110
+ { ruleId: "test-quality.trivial-assertion", pillar: "test-quality", severity: "warning", confidence: "high", description: "Flags tautological assertions.", remediation: "Assert a real result from the system under test." },
111
+ { ruleId: "test-quality.unused-mock", pillar: "test-quality", severity: "advisory", confidence: "medium", description: "Flags mocks created but not used.", remediation: "Remove unused mocks or wire them into the behavior under test." },
112
+ { ruleId: "waste.any-type", pillar: "waste", severity: "warning", confidence: "high", description: "Flags any type usage.", remediation: "Use unknown with validation or a precise type." },
113
+ { ruleId: "waste.broad-runtime-version", pillar: "waste", severity: "advisory", confidence: "medium", description: "Flags broad runtime dependency version ranges.", remediation: "Use bounded semver ranges and lockfiles." },
114
+ { ruleId: "waste.commented-out-code", pillar: "waste", severity: "advisory", confidence: "high", description: "Flags comments that appear to contain disabled code.", remediation: "Delete dead code or restore it behind a real feature path." },
115
+ { ruleId: "waste.console-log", pillar: "waste", severity: "advisory", confidence: "high", description: "Flags console log/debug calls in source.", remediation: "Remove debug logging or route through structured logging." },
116
+ { ruleId: "waste.empty-function", pillar: "waste", severity: "advisory", confidence: "high", description: "Flags functions with no executable body.", remediation: "Delete the function or add the missing implementation." },
117
+ { ruleId: "waste.exported-any", pillar: "waste", severity: "warning", confidence: "medium", description: "Flags exported APIs exposing any.", remediation: "Use a named interface, unknown with validation, or precise generics." },
118
+ { ruleId: "waste.redundant-boolean-cast", pillar: "waste", severity: "advisory", confidence: "medium", description: "Flags redundant boolean casts in condition expressions.", remediation: "Use the condition value directly or name the boolean conversion." },
119
+ { ruleId: "waste.redundant-variable", pillar: "waste", severity: "advisory", confidence: "medium", description: "Flags variables returned immediately after assignment.", remediation: "Return the expression directly." },
120
+ { ruleId: "waste.swallowed-catch", pillar: "waste", severity: "warning", confidence: "medium", description: "Flags empty catch blocks.", remediation: "Handle, report, rethrow, or document intentional ignore paths." },
121
+ { ruleId: "waste.useless-catch", pillar: "waste", severity: "advisory", confidence: "high", description: "Flags catch blocks that only rethrow the caught value.", remediation: "Remove the catch block or add meaningful handling." },
122
+ { ruleId: "waste.useless-return", pillar: "waste", severity: "advisory", confidence: "medium", description: "Flags terminal bare return statements in void functions.", remediation: "Remove the final return statement." },
123
+ { ruleId: "waste.unreachable-code", pillar: "waste", severity: "warning", confidence: "high", description: "Flags statements after terminating statements.", remediation: "Delete unreachable code or restructure the control flow." },
124
+ { ruleId: "waste.unused-import", pillar: "waste", severity: "advisory", confidence: "medium", description: "Flags named imports with no apparent usage.", remediation: "Remove unused imports." },
125
+ { ruleId: "waste.unused-parameter", pillar: "waste", severity: "advisory", confidence: "medium", description: "Flags parameters with no apparent usage.", remediation: "Remove the parameter or prefix it with _ if intentional." },
126
+ ];
127
+
128
+ /**
129
+ * Return the deterministic rule catalogue used by list-rules.
130
+ *
131
+ * @returns Sorted rule descriptor metadata.
132
+ */
133
+ export function ruleDescriptors(): RuleDescriptor[] {
134
+ return [...RULE_DESCRIPTORS].sort((left, right) => left.ruleId.localeCompare(right.ruleId));
135
+ }