claude-crap 0.1.2

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 (202) hide show
  1. package/CHANGELOG.md +308 -0
  2. package/LICENSE +21 -0
  3. package/README.md +550 -0
  4. package/bin/claude-crap.mjs +141 -0
  5. package/dist/adapters/bandit.d.ts +48 -0
  6. package/dist/adapters/bandit.d.ts.map +1 -0
  7. package/dist/adapters/bandit.js +145 -0
  8. package/dist/adapters/bandit.js.map +1 -0
  9. package/dist/adapters/common.d.ts +73 -0
  10. package/dist/adapters/common.d.ts.map +1 -0
  11. package/dist/adapters/common.js +78 -0
  12. package/dist/adapters/common.js.map +1 -0
  13. package/dist/adapters/eslint.d.ts +52 -0
  14. package/dist/adapters/eslint.d.ts.map +1 -0
  15. package/dist/adapters/eslint.js +142 -0
  16. package/dist/adapters/eslint.js.map +1 -0
  17. package/dist/adapters/index.d.ts +47 -0
  18. package/dist/adapters/index.d.ts.map +1 -0
  19. package/dist/adapters/index.js +64 -0
  20. package/dist/adapters/index.js.map +1 -0
  21. package/dist/adapters/semgrep.d.ts +30 -0
  22. package/dist/adapters/semgrep.d.ts.map +1 -0
  23. package/dist/adapters/semgrep.js +130 -0
  24. package/dist/adapters/semgrep.js.map +1 -0
  25. package/dist/adapters/stryker.d.ts +55 -0
  26. package/dist/adapters/stryker.d.ts.map +1 -0
  27. package/dist/adapters/stryker.js +165 -0
  28. package/dist/adapters/stryker.js.map +1 -0
  29. package/dist/ast/cyclomatic.d.ts +48 -0
  30. package/dist/ast/cyclomatic.d.ts.map +1 -0
  31. package/dist/ast/cyclomatic.js +106 -0
  32. package/dist/ast/cyclomatic.js.map +1 -0
  33. package/dist/ast/index.d.ts +26 -0
  34. package/dist/ast/index.d.ts.map +1 -0
  35. package/dist/ast/index.js +23 -0
  36. package/dist/ast/index.js.map +1 -0
  37. package/dist/ast/language-config.d.ts +70 -0
  38. package/dist/ast/language-config.d.ts.map +1 -0
  39. package/dist/ast/language-config.js +192 -0
  40. package/dist/ast/language-config.js.map +1 -0
  41. package/dist/ast/tree-sitter-engine.d.ts +133 -0
  42. package/dist/ast/tree-sitter-engine.d.ts.map +1 -0
  43. package/dist/ast/tree-sitter-engine.js +270 -0
  44. package/dist/ast/tree-sitter-engine.js.map +1 -0
  45. package/dist/config.d.ts +57 -0
  46. package/dist/config.d.ts.map +1 -0
  47. package/dist/config.js +78 -0
  48. package/dist/config.js.map +1 -0
  49. package/dist/crap-config.d.ts +97 -0
  50. package/dist/crap-config.d.ts.map +1 -0
  51. package/dist/crap-config.js +144 -0
  52. package/dist/crap-config.js.map +1 -0
  53. package/dist/dashboard/server.d.ts +65 -0
  54. package/dist/dashboard/server.d.ts.map +1 -0
  55. package/dist/dashboard/server.js +147 -0
  56. package/dist/dashboard/server.js.map +1 -0
  57. package/dist/index.d.ts +32 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +574 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/metrics/crap.d.ts +71 -0
  62. package/dist/metrics/crap.d.ts.map +1 -0
  63. package/dist/metrics/crap.js +67 -0
  64. package/dist/metrics/crap.js.map +1 -0
  65. package/dist/metrics/index.d.ts +31 -0
  66. package/dist/metrics/index.d.ts.map +1 -0
  67. package/dist/metrics/index.js +27 -0
  68. package/dist/metrics/index.js.map +1 -0
  69. package/dist/metrics/score.d.ts +143 -0
  70. package/dist/metrics/score.d.ts.map +1 -0
  71. package/dist/metrics/score.js +224 -0
  72. package/dist/metrics/score.js.map +1 -0
  73. package/dist/metrics/tdr.d.ts +106 -0
  74. package/dist/metrics/tdr.d.ts.map +1 -0
  75. package/dist/metrics/tdr.js +117 -0
  76. package/dist/metrics/tdr.js.map +1 -0
  77. package/dist/metrics/workspace-walker.d.ts +43 -0
  78. package/dist/metrics/workspace-walker.d.ts.map +1 -0
  79. package/dist/metrics/workspace-walker.js +137 -0
  80. package/dist/metrics/workspace-walker.js.map +1 -0
  81. package/dist/sarif/index.d.ts +21 -0
  82. package/dist/sarif/index.d.ts.map +1 -0
  83. package/dist/sarif/index.js +19 -0
  84. package/dist/sarif/index.js.map +1 -0
  85. package/dist/sarif/sarif-builder.d.ts +128 -0
  86. package/dist/sarif/sarif-builder.d.ts.map +1 -0
  87. package/dist/sarif/sarif-builder.js +79 -0
  88. package/dist/sarif/sarif-builder.js.map +1 -0
  89. package/dist/sarif/sarif-store.d.ts +205 -0
  90. package/dist/sarif/sarif-store.d.ts.map +1 -0
  91. package/dist/sarif/sarif-store.js +246 -0
  92. package/dist/sarif/sarif-store.js.map +1 -0
  93. package/dist/sarif/sarif-validator.d.ts +45 -0
  94. package/dist/sarif/sarif-validator.d.ts.map +1 -0
  95. package/dist/sarif/sarif-validator.js +138 -0
  96. package/dist/sarif/sarif-validator.js.map +1 -0
  97. package/dist/schemas/tool-schemas.d.ts +216 -0
  98. package/dist/schemas/tool-schemas.d.ts.map +1 -0
  99. package/dist/schemas/tool-schemas.js +208 -0
  100. package/dist/schemas/tool-schemas.js.map +1 -0
  101. package/dist/sdk.d.ts +45 -0
  102. package/dist/sdk.d.ts.map +1 -0
  103. package/dist/sdk.js +44 -0
  104. package/dist/sdk.js.map +1 -0
  105. package/dist/tools/index.d.ts +24 -0
  106. package/dist/tools/index.d.ts.map +1 -0
  107. package/dist/tools/index.js +23 -0
  108. package/dist/tools/index.js.map +1 -0
  109. package/dist/tools/test-harness.d.ts +75 -0
  110. package/dist/tools/test-harness.d.ts.map +1 -0
  111. package/dist/tools/test-harness.js +137 -0
  112. package/dist/tools/test-harness.js.map +1 -0
  113. package/dist/workspace-guard.d.ts +53 -0
  114. package/dist/workspace-guard.d.ts.map +1 -0
  115. package/dist/workspace-guard.js +61 -0
  116. package/dist/workspace-guard.js.map +1 -0
  117. package/package.json +133 -0
  118. package/plugin/.claude-plugin/plugin.json +29 -0
  119. package/plugin/.mcp.json +18 -0
  120. package/plugin/CLAUDE.md +143 -0
  121. package/plugin/bundle/dashboard/public/index.html +368 -0
  122. package/plugin/bundle/dashboard/public/vendor/vue.global.prod.js +9 -0
  123. package/plugin/bundle/mcp-server.mjs +8718 -0
  124. package/plugin/bundle/mcp-server.mjs.map +7 -0
  125. package/plugin/bundle/tdr-engine.mjs +50 -0
  126. package/plugin/bundle/tdr-engine.mjs.map +7 -0
  127. package/plugin/hooks/hooks.json +62 -0
  128. package/plugin/hooks/lib/crap-config.mjs +152 -0
  129. package/plugin/hooks/lib/gatekeeper-rules.mjs +257 -0
  130. package/plugin/hooks/lib/hook-io.mjs +151 -0
  131. package/plugin/hooks/lib/quality-gate.mjs +329 -0
  132. package/plugin/hooks/lib/test-harness.mjs +152 -0
  133. package/plugin/hooks/post-tool-use.mjs +245 -0
  134. package/plugin/hooks/pre-tool-use.mjs +290 -0
  135. package/plugin/hooks/session-start.mjs +109 -0
  136. package/plugin/hooks/stop-quality-gate.mjs +226 -0
  137. package/plugin/package.json +18 -0
  138. package/plugin/skills/adopt/SKILL.md +74 -0
  139. package/plugin/skills/analyze/SKILL.md +77 -0
  140. package/plugin/skills/check-test/SKILL.md +50 -0
  141. package/plugin/skills/score/SKILL.md +31 -0
  142. package/scripts/bug-report.mjs +328 -0
  143. package/scripts/build-fast.mjs +130 -0
  144. package/scripts/bundle-plugin.mjs +74 -0
  145. package/scripts/doctor.mjs +320 -0
  146. package/scripts/install.mjs +192 -0
  147. package/scripts/lib/cli-ui.mjs +122 -0
  148. package/scripts/postinstall.mjs +127 -0
  149. package/scripts/run-tests.mjs +95 -0
  150. package/scripts/status.mjs +110 -0
  151. package/scripts/uninstall.mjs +72 -0
  152. package/src/adapters/bandit.ts +191 -0
  153. package/src/adapters/common.ts +133 -0
  154. package/src/adapters/eslint.ts +187 -0
  155. package/src/adapters/index.ts +78 -0
  156. package/src/adapters/semgrep.ts +150 -0
  157. package/src/adapters/stryker.ts +218 -0
  158. package/src/ast/cyclomatic.ts +131 -0
  159. package/src/ast/index.ts +33 -0
  160. package/src/ast/language-config.ts +231 -0
  161. package/src/ast/tree-sitter-engine.ts +385 -0
  162. package/src/config.ts +109 -0
  163. package/src/crap-config.ts +196 -0
  164. package/src/dashboard/public/index.html +368 -0
  165. package/src/dashboard/public/vendor/vue.global.prod.js +9 -0
  166. package/src/dashboard/server.ts +205 -0
  167. package/src/index.ts +696 -0
  168. package/src/metrics/crap.ts +101 -0
  169. package/src/metrics/index.ts +51 -0
  170. package/src/metrics/score.ts +329 -0
  171. package/src/metrics/tdr.ts +155 -0
  172. package/src/metrics/workspace-walker.ts +146 -0
  173. package/src/sarif/index.ts +31 -0
  174. package/src/sarif/sarif-builder.ts +139 -0
  175. package/src/sarif/sarif-store.ts +347 -0
  176. package/src/sarif/sarif-validator.ts +145 -0
  177. package/src/schemas/tool-schemas.ts +225 -0
  178. package/src/sdk.ts +110 -0
  179. package/src/tests/adapters/bandit.test.ts +111 -0
  180. package/src/tests/adapters/dispatch.test.ts +100 -0
  181. package/src/tests/adapters/eslint.test.ts +138 -0
  182. package/src/tests/adapters/semgrep.test.ts +125 -0
  183. package/src/tests/adapters/stryker.test.ts +103 -0
  184. package/src/tests/crap-config.test.ts +228 -0
  185. package/src/tests/crap.test.ts +59 -0
  186. package/src/tests/cyclomatic.test.ts +87 -0
  187. package/src/tests/dashboard-http.test.ts +108 -0
  188. package/src/tests/dashboard-integrity.test.ts +128 -0
  189. package/src/tests/integration/mcp-server.integration.test.ts +352 -0
  190. package/src/tests/pre-tool-use-hook.test.ts +178 -0
  191. package/src/tests/sarif-store.test.ts +241 -0
  192. package/src/tests/sarif-validator.test.ts +164 -0
  193. package/src/tests/score.test.ts +260 -0
  194. package/src/tests/skills-frontmatter.test.ts +172 -0
  195. package/src/tests/stop-quality-gate-strictness.test.ts +243 -0
  196. package/src/tests/tdr.test.ts +86 -0
  197. package/src/tests/test-harness.test.ts +153 -0
  198. package/src/tests/workspace-guard.test.ts +111 -0
  199. package/src/tools/index.ts +24 -0
  200. package/src/tools/test-harness.ts +158 -0
  201. package/src/workspace-guard.ts +64 -0
  202. package/tsconfig.json +27 -0
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Stryker (JavaScript mutation testing) adapter.
3
+ *
4
+ * Stryker emits a JSON report under `reports/mutation/mutation.json`.
5
+ * The report shape is documented at
6
+ * https://stryker-mutator.io/docs/mutation-testing-elements/mutant-result-schema/ :
7
+ *
8
+ * {
9
+ * "schemaVersion": "1.0",
10
+ * "thresholds": { ... },
11
+ * "files": {
12
+ * "src/foo.ts": {
13
+ * "language": "typescript",
14
+ * "source": "...",
15
+ * "mutants": [
16
+ * {
17
+ * "id": "1",
18
+ * "mutatorName": "ConditionalExpression",
19
+ * "replacement": "false",
20
+ * "location": {
21
+ * "start": { "line": 10, "column": 5 },
22
+ * "end": { "line": 10, "column": 15 }
23
+ * },
24
+ * "status": "Survived", // Killed | Survived | Timeout | NoCoverage | RuntimeError | CompileError | Ignored
25
+ * "statusReason": "..."
26
+ * }
27
+ * ]
28
+ * }
29
+ * }
30
+ * }
31
+ *
32
+ * This adapter treats every **surviving mutant** as a SARIF
33
+ * `error`-level finding — surviving mutants are exactly the ones the
34
+ * Golden Rule forbids, because they prove the test suite does not
35
+ * pin the code's behavior tightly enough to notice a change.
36
+ *
37
+ * Mutants with status `NoCoverage` become `warning`-level findings
38
+ * (not blocking the Stop gate by themselves, but still ingested so
39
+ * the dashboard can surface uncovered lines). All other statuses are
40
+ * ignored — they do not represent defects.
41
+ *
42
+ * @module adapters/stryker
43
+ */
44
+
45
+ import type { SarifLevel } from "../sarif/sarif-builder.js";
46
+ import {
47
+ estimateEffortMinutes,
48
+ wrapResultsInSarif,
49
+ type AdapterResult,
50
+ type KnownScanner,
51
+ } from "./common.js";
52
+
53
+ const STRYKER: KnownScanner = "stryker";
54
+
55
+ interface StrykerReport {
56
+ readonly schemaVersion?: string;
57
+ readonly files?: Record<string, StrykerFileReport>;
58
+ }
59
+
60
+ interface StrykerFileReport {
61
+ readonly language?: string;
62
+ readonly mutants?: ReadonlyArray<StrykerMutant>;
63
+ }
64
+
65
+ interface StrykerMutant {
66
+ readonly id?: string;
67
+ readonly mutatorName?: string;
68
+ readonly replacement?: string;
69
+ readonly location?: {
70
+ readonly start?: { readonly line?: number; readonly column?: number };
71
+ readonly end?: { readonly line?: number; readonly column?: number };
72
+ };
73
+ readonly status?: string;
74
+ readonly statusReason?: string;
75
+ }
76
+
77
+ /**
78
+ * Accept a Stryker JSON mutation report and return a normalized
79
+ * `PersistedSarif` document with one finding per surviving mutant
80
+ * and per uncovered mutant.
81
+ *
82
+ * @param input Raw Stryker report (string or parsed object).
83
+ * @returns Adapter result.
84
+ * @throws When the input is not a Stryker mutation report.
85
+ */
86
+ export function adaptStryker(input: unknown): AdapterResult {
87
+ const parsed = typeof input === "string" ? (JSON.parse(input) as unknown) : input;
88
+ if (!parsed || typeof parsed !== "object") {
89
+ throw new Error(`[adapter:stryker] expected a JSON object`);
90
+ }
91
+ const report = parsed as StrykerReport;
92
+ if (!report.files || typeof report.files !== "object") {
93
+ throw new Error(`[adapter:stryker] report is missing a files{} map`);
94
+ }
95
+
96
+ const results: Array<ReturnType<typeof buildSarifResult>> = [];
97
+ let totalEffortMinutes = 0;
98
+
99
+ for (const [filename, fileReport] of Object.entries(report.files)) {
100
+ const mutants = Array.isArray(fileReport?.mutants) ? fileReport.mutants : [];
101
+ for (const mutant of mutants) {
102
+ const level = classifyMutant(mutant.status);
103
+ if (level === null) continue; // Killed / Ignored / CompileError — not a defect
104
+ const startLine = mutant.location?.start?.line ?? 1;
105
+ const startColumn = mutant.location?.start?.column ?? 1;
106
+ const endLine = mutant.location?.end?.line;
107
+ const endColumn = mutant.location?.end?.column;
108
+
109
+ // Surviving mutants cost more to fix than the default error
110
+ // budget because the agent has to first write a killing test,
111
+ // THEN possibly rewrite the code.
112
+ const effortOverride = level === "error" ? 45 : 15;
113
+ const effort = estimateEffortMinutes(level, effortOverride);
114
+ totalEffortMinutes += effort;
115
+
116
+ const mutator = mutant.mutatorName ?? "Unknown";
117
+ const ruleId = `stryker.${mutator}`;
118
+ const statusText = mutant.status ?? "Unknown";
119
+ const message =
120
+ `${statusText}: ${mutator} mutant on '${mutant.replacement ?? "?"}'` +
121
+ (mutant.statusReason ? ` — ${mutant.statusReason}` : "");
122
+
123
+ results.push(
124
+ buildSarifResult({
125
+ ruleId,
126
+ level,
127
+ message,
128
+ uri: filename,
129
+ startLine,
130
+ startColumn,
131
+ endLine: typeof endLine === "number" ? endLine : undefined,
132
+ endColumn: typeof endColumn === "number" ? endColumn : undefined,
133
+ effortMinutes: effort,
134
+ mutantId: mutant.id,
135
+ mutator,
136
+ mutantStatus: statusText,
137
+ }),
138
+ );
139
+ }
140
+ }
141
+
142
+ return {
143
+ document: wrapResultsInSarif(STRYKER, String(report.schemaVersion ?? "unknown"), results),
144
+ sourceTool: STRYKER,
145
+ findingCount: results.length,
146
+ totalEffortMinutes,
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Classify a Stryker mutant status into a SARIF level, or `null` when
152
+ * the status represents a mutant that was correctly handled by the
153
+ * test suite and should not produce a finding.
154
+ */
155
+ function classifyMutant(status: string | undefined): SarifLevel | null {
156
+ switch ((status ?? "").toLowerCase()) {
157
+ case "survived":
158
+ return "error";
159
+ case "nocoverage":
160
+ return "warning";
161
+ case "timeout":
162
+ // Timeout mutants are suspicious but not proof of defect —
163
+ // they deserve a note so the dashboard highlights them.
164
+ return "note";
165
+ case "killed":
166
+ case "ignored":
167
+ case "compileerror":
168
+ case "runtimeerror":
169
+ default:
170
+ return null;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Build the SARIF `result` object for a single Stryker mutant. We
176
+ * propagate the mutant id, mutator name, and raw status into
177
+ * `properties` so the dashboard can display them as tags.
178
+ */
179
+ function buildSarifResult(opts: {
180
+ ruleId: string;
181
+ level: SarifLevel;
182
+ message: string;
183
+ uri: string;
184
+ startLine: number;
185
+ startColumn: number;
186
+ endLine?: number | undefined;
187
+ endColumn?: number | undefined;
188
+ effortMinutes: number;
189
+ mutantId?: string | undefined;
190
+ mutator?: string | undefined;
191
+ mutantStatus?: string | undefined;
192
+ }) {
193
+ return {
194
+ ruleId: opts.ruleId,
195
+ level: opts.level,
196
+ message: { text: opts.message },
197
+ locations: [
198
+ {
199
+ physicalLocation: {
200
+ artifactLocation: { uri: opts.uri },
201
+ region: {
202
+ startLine: opts.startLine,
203
+ startColumn: opts.startColumn,
204
+ ...(opts.endLine !== undefined ? { endLine: opts.endLine } : {}),
205
+ ...(opts.endColumn !== undefined ? { endColumn: opts.endColumn } : {}),
206
+ },
207
+ },
208
+ },
209
+ ],
210
+ properties: {
211
+ sourceTool: STRYKER,
212
+ effortMinutes: opts.effortMinutes,
213
+ ...(opts.mutantId ? { mutantId: opts.mutantId } : {}),
214
+ ...(opts.mutator ? { mutator: opts.mutator } : {}),
215
+ ...(opts.mutantStatus ? { mutantStatus: opts.mutantStatus } : {}),
216
+ },
217
+ };
218
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Deterministic cyclomatic complexity walker.
3
+ *
4
+ * Given a tree-sitter subtree rooted at a function node, this module walks
5
+ * the tree and returns the McCabe cyclomatic complexity number. The
6
+ * algorithm is the standard "1 + (branching nodes + short-circuit operators)"
7
+ * formulation:
8
+ *
9
+ * CC = 1
10
+ * + count of branching-statement nodes (if, while, for, case, catch, ...)
11
+ * + count of short-circuit operators (&&, ||, ??, `and`, `or`)
12
+ * + count of ternary expressions (counted as branching nodes)
13
+ *
14
+ * The baseline of 1 represents the straight-line path through the function.
15
+ * Every additional branching point multiplies the set of reachable paths.
16
+ *
17
+ * The walker is deliberately language-agnostic: it consults the
18
+ * {@link LanguageConfig} passed in to decide which node types to count.
19
+ * Nested functions inside the subtree are **skipped** — each function's
20
+ * complexity is reported independently by {@link TreeSitterEngine}.
21
+ *
22
+ * @module ast/cyclomatic
23
+ */
24
+
25
+ import type { LanguageConfig } from "./language-config.js";
26
+
27
+ /**
28
+ * Minimal structural contract of a tree-sitter node. We intentionally
29
+ * avoid importing `web-tree-sitter` types here so this module stays
30
+ * unit-testable with a hand-rolled mock tree.
31
+ */
32
+ export interface AstNode {
33
+ /** Node type name from the grammar (e.g. `"if_statement"`). */
34
+ readonly type: string;
35
+ /** Raw source text for operator detection. May be large — do not log. */
36
+ readonly text: string;
37
+ /** Zero-based child count. */
38
+ readonly childCount: number;
39
+ /** Retrieve a child by index. Returns `null` if out of range. */
40
+ child(index: number): AstNode | null;
41
+ }
42
+
43
+ /**
44
+ * Compute the cyclomatic complexity of a function subtree.
45
+ *
46
+ * @param root Node rooted at the function (method, arrow, lambda, ...).
47
+ * @param languageConfig Language classification tables for node types.
48
+ * @returns The McCabe cyclomatic complexity (always ≥ 1).
49
+ */
50
+ export function computeCyclomaticComplexity(root: AstNode, languageConfig: LanguageConfig): number {
51
+ let complexity = 1;
52
+ walk(root, languageConfig, true, (node) => {
53
+ if (languageConfig.branchingNodeTypes.has(node.type)) {
54
+ complexity += 1;
55
+ return;
56
+ }
57
+ // Boolean / short-circuit operators are usually represented as
58
+ // "binary_expression" nodes with an operator token child. To avoid
59
+ // coupling to a specific grammar's node shape, we inspect the raw
60
+ // text of the node's direct operator child.
61
+ if (isBooleanExpression(node, languageConfig)) {
62
+ complexity += 1;
63
+ return;
64
+ }
65
+ });
66
+ return complexity;
67
+ }
68
+
69
+ /**
70
+ * Depth-first walk that skips any nested function subtree so that its
71
+ * complexity is not attributed to the enclosing function.
72
+ *
73
+ * @param node Current node being visited.
74
+ * @param languageConfig Language tables used to detect nested functions.
75
+ * @param isRoot `true` for the starting node (we do not skip it).
76
+ * @param visit Callback invoked for every non-function descendant.
77
+ */
78
+ function walk(
79
+ node: AstNode,
80
+ languageConfig: LanguageConfig,
81
+ isRoot: boolean,
82
+ visit: (n: AstNode) => void,
83
+ ): void {
84
+ if (!isRoot && languageConfig.functionNodeTypes.has(node.type)) {
85
+ // Nested function — stop the walk here. Its complexity is reported
86
+ // separately when the engine iterates the top-level function list.
87
+ return;
88
+ }
89
+ visit(node);
90
+ for (let i = 0; i < node.childCount; i++) {
91
+ const child = node.child(i);
92
+ if (child) walk(child, languageConfig, false, visit);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Return `true` when `node` is a boolean / short-circuit expression that
98
+ * should add one to the cyclomatic complexity. We inspect the node's
99
+ * immediate children for an operator token whose text matches one of the
100
+ * language's short-circuit operators.
101
+ *
102
+ * This is a heuristic — grammars differ in how they represent operators
103
+ * — but it is stable enough for the five supported languages because:
104
+ *
105
+ * - JavaScript / TypeScript / Java: binary expression with `"&&"` or `"||"` token child.
106
+ * - Python: `boolean_operator` node with `"and"` / `"or"` token child.
107
+ * - C#: binary expression with `"&&"`, `"||"`, or `"??"` token child.
108
+ *
109
+ * @param node Candidate node.
110
+ * @param languageConfig Tables with the language's boolean operator set.
111
+ * @returns `true` when the node is a counted boolean expression.
112
+ */
113
+ function isBooleanExpression(node: AstNode, languageConfig: LanguageConfig): boolean {
114
+ // Common type names across supported grammars. We check the type first
115
+ // to avoid scanning text for every node in the tree.
116
+ if (
117
+ node.type !== "binary_expression" &&
118
+ node.type !== "boolean_operator" &&
119
+ node.type !== "logical_expression"
120
+ ) {
121
+ return false;
122
+ }
123
+ for (let i = 0; i < node.childCount; i++) {
124
+ const child = node.child(i);
125
+ if (!child) continue;
126
+ if (languageConfig.booleanOperators.includes(child.text)) {
127
+ return true;
128
+ }
129
+ }
130
+ return false;
131
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Public SDK entry point for the tree-sitter based AST engine and the
3
+ * cyclomatic complexity walker.
4
+ *
5
+ * Usage:
6
+ *
7
+ * ```ts
8
+ * import {
9
+ * TreeSitterEngine,
10
+ * computeCyclomaticComplexity,
11
+ * detectLanguageFromPath,
12
+ * type FileMetrics,
13
+ * type FunctionMetrics,
14
+ * type SupportedLanguage,
15
+ * } from "claude-crap/ast";
16
+ * ```
17
+ *
18
+ * @module ast
19
+ */
20
+
21
+ export { TreeSitterEngine } from "./tree-sitter-engine.js";
22
+ export type {
23
+ AnalyzeFileRequest,
24
+ FileMetrics,
25
+ FunctionMetrics,
26
+ TreeSitterEngineOptions,
27
+ } from "./tree-sitter-engine.js";
28
+
29
+ export { computeCyclomaticComplexity } from "./cyclomatic.js";
30
+ export type { AstNode } from "./cyclomatic.js";
31
+
32
+ export { LANGUAGE_TABLE, detectLanguageFromPath } from "./language-config.js";
33
+ export type { LanguageConfig, SupportedLanguage } from "./language-config.js";
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Per-language tree-sitter node classification tables.
3
+ *
4
+ * Every language grammar exposes a different set of node type names. To
5
+ * keep the AST engine language-agnostic we encode, for each supported
6
+ * language, three sets:
7
+ *
8
+ * - `functionNodeTypes` — nodes that represent a function/method/lambda.
9
+ * These are the units we report metrics for.
10
+ * - `branchingNodeTypes` — nodes that add one independent path through
11
+ * the function. Used to compute cyclomatic
12
+ * complexity by counting occurrences.
13
+ * - `nameField` — the tree-sitter field name that holds the
14
+ * function's identifier, used to extract the
15
+ * function name for reporting.
16
+ *
17
+ * We also define which WASM grammar file to load per language. The paths
18
+ * are resolved at runtime against the `tree-sitter-wasms` package, but
19
+ * can be overridden via the engine constructor if you want to ship your
20
+ * own grammars.
21
+ *
22
+ * @module ast/language-config
23
+ */
24
+
25
+ /**
26
+ * Languages currently supported by the AST engine. This is the same
27
+ * `enum` that appears in the `analyze_file_ast` tool schema — keep them
28
+ * in sync when adding a new language.
29
+ */
30
+ export type SupportedLanguage = "csharp" | "javascript" | "typescript" | "python" | "java";
31
+
32
+ /**
33
+ * Per-language classification record. Immutable by convention.
34
+ */
35
+ export interface LanguageConfig {
36
+ /** Canonical language identifier (stable across releases). */
37
+ readonly id: SupportedLanguage;
38
+ /** WASM grammar filename inside `tree-sitter-wasms/out/`. */
39
+ readonly wasmName: string;
40
+ /** File extensions that should map to this language. */
41
+ readonly extensions: ReadonlyArray<string>;
42
+ /** Tree-sitter node types that represent callable units. */
43
+ readonly functionNodeTypes: ReadonlySet<string>;
44
+ /** Tree-sitter node types that add +1 to cyclomatic complexity. */
45
+ readonly branchingNodeTypes: ReadonlySet<string>;
46
+ /**
47
+ * Boolean / short-circuit operator node types. These are counted only
48
+ * when the node is an `"&&"`, `"||"`, `"??"` (etc.) operator, so the
49
+ * walker inspects the operator text on top of the node type.
50
+ */
51
+ readonly booleanOperators: ReadonlyArray<string>;
52
+ /**
53
+ * Child-field names we try in order to extract the function name. The
54
+ * walker reads the first non-empty match.
55
+ */
56
+ readonly nameFieldCandidates: ReadonlyArray<string>;
57
+ }
58
+
59
+ // -----------------------------------------------------------------------------
60
+ // C#
61
+ // -----------------------------------------------------------------------------
62
+ // Grammar: https://github.com/tree-sitter/tree-sitter-c-sharp
63
+ const CSHARP: LanguageConfig = {
64
+ id: "csharp",
65
+ wasmName: "tree-sitter-c_sharp.wasm",
66
+ extensions: [".cs"],
67
+ functionNodeTypes: new Set([
68
+ "method_declaration",
69
+ "local_function_statement",
70
+ "lambda_expression",
71
+ "anonymous_method_expression",
72
+ "constructor_declaration",
73
+ "destructor_declaration",
74
+ "operator_declaration",
75
+ "conversion_operator_declaration",
76
+ "accessor_declaration",
77
+ ]),
78
+ branchingNodeTypes: new Set([
79
+ "if_statement",
80
+ "else_clause",
81
+ "while_statement",
82
+ "do_statement",
83
+ "for_statement",
84
+ "for_each_statement",
85
+ "case_switch_label",
86
+ "case_pattern_switch_label",
87
+ "switch_expression_arm",
88
+ "catch_clause",
89
+ "conditional_expression",
90
+ "conditional_access_expression",
91
+ "when_clause",
92
+ ]),
93
+ booleanOperators: ["&&", "||", "??"],
94
+ nameFieldCandidates: ["name"],
95
+ };
96
+
97
+ // -----------------------------------------------------------------------------
98
+ // JavaScript
99
+ // -----------------------------------------------------------------------------
100
+ // Grammar: https://github.com/tree-sitter/tree-sitter-javascript
101
+ const JAVASCRIPT: LanguageConfig = {
102
+ id: "javascript",
103
+ wasmName: "tree-sitter-javascript.wasm",
104
+ extensions: [".js", ".jsx", ".mjs", ".cjs"],
105
+ functionNodeTypes: new Set([
106
+ "function_declaration",
107
+ "function_expression",
108
+ "arrow_function",
109
+ "method_definition",
110
+ "generator_function",
111
+ "generator_function_declaration",
112
+ ]),
113
+ branchingNodeTypes: new Set([
114
+ "if_statement",
115
+ "else_clause",
116
+ "while_statement",
117
+ "do_statement",
118
+ "for_statement",
119
+ "for_in_statement",
120
+ "for_of_statement",
121
+ "switch_case",
122
+ "catch_clause",
123
+ "ternary_expression",
124
+ ]),
125
+ booleanOperators: ["&&", "||", "??"],
126
+ nameFieldCandidates: ["name"],
127
+ };
128
+
129
+ // -----------------------------------------------------------------------------
130
+ // TypeScript
131
+ // -----------------------------------------------------------------------------
132
+ // Grammar: https://github.com/tree-sitter/tree-sitter-typescript
133
+ // The TypeScript grammar inherits most node types from JavaScript, so we
134
+ // extend the JS tables rather than re-declaring them from scratch.
135
+ const TYPESCRIPT: LanguageConfig = {
136
+ id: "typescript",
137
+ wasmName: "tree-sitter-typescript.wasm",
138
+ extensions: [".ts", ".tsx", ".mts", ".cts"],
139
+ functionNodeTypes: new Set([
140
+ ...JAVASCRIPT.functionNodeTypes,
141
+ "function_signature",
142
+ "method_signature",
143
+ "abstract_method_signature",
144
+ ]),
145
+ branchingNodeTypes: new Set([...JAVASCRIPT.branchingNodeTypes]),
146
+ booleanOperators: [...JAVASCRIPT.booleanOperators],
147
+ nameFieldCandidates: ["name"],
148
+ };
149
+
150
+ // -----------------------------------------------------------------------------
151
+ // Python
152
+ // -----------------------------------------------------------------------------
153
+ // Grammar: https://github.com/tree-sitter/tree-sitter-python
154
+ const PYTHON: LanguageConfig = {
155
+ id: "python",
156
+ wasmName: "tree-sitter-python.wasm",
157
+ extensions: [".py", ".pyi"],
158
+ functionNodeTypes: new Set(["function_definition", "lambda"]),
159
+ branchingNodeTypes: new Set([
160
+ "if_statement",
161
+ "elif_clause",
162
+ "else_clause",
163
+ "while_statement",
164
+ "for_statement",
165
+ "try_statement",
166
+ "except_clause",
167
+ "conditional_expression",
168
+ "match_statement",
169
+ "case_clause",
170
+ ]),
171
+ booleanOperators: ["and", "or"],
172
+ nameFieldCandidates: ["name"],
173
+ };
174
+
175
+ // -----------------------------------------------------------------------------
176
+ // Java
177
+ // -----------------------------------------------------------------------------
178
+ // Grammar: https://github.com/tree-sitter/tree-sitter-java
179
+ const JAVA: LanguageConfig = {
180
+ id: "java",
181
+ wasmName: "tree-sitter-java.wasm",
182
+ extensions: [".java"],
183
+ functionNodeTypes: new Set([
184
+ "method_declaration",
185
+ "constructor_declaration",
186
+ "lambda_expression",
187
+ ]),
188
+ branchingNodeTypes: new Set([
189
+ "if_statement",
190
+ "while_statement",
191
+ "do_statement",
192
+ "for_statement",
193
+ "enhanced_for_statement",
194
+ "switch_label",
195
+ "switch_rule",
196
+ "catch_clause",
197
+ "ternary_expression",
198
+ ]),
199
+ booleanOperators: ["&&", "||"],
200
+ nameFieldCandidates: ["name"],
201
+ };
202
+
203
+ /**
204
+ * Complete language table. Look up by {@link SupportedLanguage} identifier.
205
+ */
206
+ export const LANGUAGE_TABLE: Readonly<Record<SupportedLanguage, LanguageConfig>> = {
207
+ csharp: CSHARP,
208
+ javascript: JAVASCRIPT,
209
+ typescript: TYPESCRIPT,
210
+ python: PYTHON,
211
+ java: JAVA,
212
+ };
213
+
214
+ /**
215
+ * Infer a {@link SupportedLanguage} from a file path by matching its
216
+ * extension. Returns `null` when no known language matches. Useful when
217
+ * the caller does not already know the language and wants the engine to
218
+ * pick one automatically.
219
+ *
220
+ * @param filePath File path (absolute or relative).
221
+ * @returns The detected language or `null`.
222
+ */
223
+ export function detectLanguageFromPath(filePath: string): SupportedLanguage | null {
224
+ const lower = filePath.toLowerCase();
225
+ for (const config of Object.values(LANGUAGE_TABLE)) {
226
+ for (const ext of config.extensions) {
227
+ if (lower.endsWith(ext)) return config.id;
228
+ }
229
+ }
230
+ return null;
231
+ }