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,133 @@
1
+ /**
2
+ * Shared types and helpers for per-scanner SARIF adapters.
3
+ *
4
+ * Every adapter in this directory converts a scanner's native output
5
+ * into a `PersistedSarif` 2.1.0 document that the `SarifStore` can
6
+ * ingest directly. The adapters also enrich the finding `properties`
7
+ * bag with a stable `effortMinutes` field so the Stop quality gate and
8
+ * the Technical Debt Ratio computation can treat every source tool
9
+ * uniformly.
10
+ *
11
+ * Rule-level effort estimates live in `DEFAULT_EFFORT_BY_SEVERITY`.
12
+ * Individual adapters may override the default per rule id when the
13
+ * scanner attaches a more specific hint.
14
+ *
15
+ * @module adapters/common
16
+ */
17
+
18
+ import type { PersistedSarif } from "../sarif/sarif-store.js";
19
+ import type { SarifLevel } from "../sarif/sarif-builder.js";
20
+
21
+ /**
22
+ * The canonical list of scanners claude-crap understands. The
23
+ * `ingest_scanner_output` MCP tool uses this as its `enum` constraint,
24
+ * so keeping it narrow prevents drift.
25
+ */
26
+ export const KNOWN_SCANNERS = ["semgrep", "eslint", "bandit", "stryker"] as const;
27
+
28
+ /**
29
+ * Union of supported scanner identifiers.
30
+ */
31
+ export type KnownScanner = (typeof KNOWN_SCANNERS)[number];
32
+
33
+ /**
34
+ * Default remediation effort in minutes per SARIF severity level. These
35
+ * numbers are deliberately conservative — real projects should override
36
+ * them per rule via adapter-specific rule maps or via SARIF properties.
37
+ *
38
+ * The mapping follows the common-sense rule that every bug takes at
39
+ * least a test plus a patch, so even a note-level finding costs time.
40
+ */
41
+ export const DEFAULT_EFFORT_BY_SEVERITY: Readonly<Record<SarifLevel, number>> = Object.freeze({
42
+ error: 60,
43
+ warning: 30,
44
+ note: 10,
45
+ none: 5,
46
+ });
47
+
48
+ /**
49
+ * Envelope common to every adapter output. Adapters return a
50
+ * `PersistedSarif` document and a small stats block describing what
51
+ * they saw, so the MCP tool handler can echo those stats back to the
52
+ * LLM even when the SarifStore rejects duplicates.
53
+ */
54
+ export interface AdapterResult {
55
+ /** Normalized SARIF 2.1.0 document ready for `SarifStore.ingestRun`. */
56
+ readonly document: PersistedSarif;
57
+ /** Scanner identifier, propagated into every finding's `properties.sourceTool`. */
58
+ readonly sourceTool: KnownScanner;
59
+ /** Raw number of findings the adapter read from the scanner's native output. */
60
+ readonly findingCount: number;
61
+ /** Total estimated remediation effort across all findings, in minutes. */
62
+ readonly totalEffortMinutes: number;
63
+ }
64
+
65
+ /**
66
+ * Build a `PersistedSarif` document from a flat list of already-mapped
67
+ * result entries. Every adapter produces its results with the same
68
+ * shape and then calls this helper to wrap them in a valid 2.1.0 envelope.
69
+ *
70
+ * @param sourceTool Stable scanner identifier (e.g. `"semgrep"`).
71
+ * @param version Adapter version string stored in `tool.driver.version`.
72
+ * @param results Pre-built SARIF `result` entries.
73
+ */
74
+ export function wrapResultsInSarif(
75
+ sourceTool: KnownScanner,
76
+ version: string,
77
+ results: ReadonlyArray<object>,
78
+ ): PersistedSarif {
79
+ return {
80
+ $schema: "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json",
81
+ version: "2.1.0",
82
+ runs: [
83
+ {
84
+ tool: {
85
+ driver: {
86
+ name: sourceTool,
87
+ version,
88
+ },
89
+ },
90
+ results: results as ReadonlyArray<SarifResultShape>,
91
+ },
92
+ ],
93
+ } as PersistedSarif;
94
+ }
95
+
96
+ /**
97
+ * Narrow structural contract of a SARIF `result` object. We type it
98
+ * loosely so adapters can emit the minimum required fields without
99
+ * importing the full SARIF spec types from `sarif-store.ts`.
100
+ */
101
+ interface SarifResultShape {
102
+ readonly ruleId: string;
103
+ readonly level?: SarifLevel;
104
+ readonly message: { readonly text: string };
105
+ readonly locations?: ReadonlyArray<{
106
+ readonly physicalLocation?: {
107
+ readonly artifactLocation?: { readonly uri?: string };
108
+ readonly region?: {
109
+ readonly startLine?: number;
110
+ readonly startColumn?: number;
111
+ readonly endLine?: number;
112
+ readonly endColumn?: number;
113
+ };
114
+ };
115
+ }>;
116
+ readonly properties?: Record<string, unknown>;
117
+ }
118
+
119
+ /**
120
+ * Estimate remediation effort for a single finding given its severity
121
+ * and an optional rule-specific override. Returns `minutes` clamped to
122
+ * a non-negative integer.
123
+ *
124
+ * @param level SARIF severity level (`"error"`, `"warning"`, ...).
125
+ * @param override Optional rule-specific effort in minutes.
126
+ */
127
+ export function estimateEffortMinutes(level: SarifLevel | undefined, override?: number): number {
128
+ if (typeof override === "number" && Number.isFinite(override) && override >= 0) {
129
+ return Math.round(override);
130
+ }
131
+ const base = DEFAULT_EFFORT_BY_SEVERITY[level ?? "warning"];
132
+ return Math.max(0, Math.round(base ?? 30));
133
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * ESLint adapter.
3
+ *
4
+ * ESLint's default JSON output (`eslint -f json .`) is NOT SARIF.
5
+ * This adapter converts it into a SARIF 2.1.0 document with one
6
+ * `result` per ESLint `messages[]` entry, mapping ESLint's numeric
7
+ * severity to SARIF levels:
8
+ *
9
+ * severity 0 → "note" (parser info / disabled)
10
+ * severity 1 → "warning"
11
+ * severity 2 → "error"
12
+ *
13
+ * ESLint's JSON shape:
14
+ *
15
+ * [
16
+ * {
17
+ * "filePath": "/abs/path/to/foo.js",
18
+ * "messages": [
19
+ * {
20
+ * "ruleId": "no-unused-vars",
21
+ * "severity": 1,
22
+ * "message": "'foo' is defined but never used.",
23
+ * "line": 10,
24
+ * "column": 5,
25
+ * "endLine": 10,
26
+ * "endColumn": 8
27
+ * }
28
+ * ],
29
+ * "errorCount": 0,
30
+ * "warningCount": 1,
31
+ * "fatalErrorCount": 0,
32
+ * "source": "...",
33
+ * "usedDeprecatedRules": []
34
+ * }
35
+ * ]
36
+ *
37
+ * We preserve the full `line`/`column` range when ESLint provides one
38
+ * so the dashboard's hot-spot table can show a precise location.
39
+ *
40
+ * @module adapters/eslint
41
+ */
42
+
43
+ import type { SarifLevel } from "../sarif/sarif-builder.js";
44
+ import {
45
+ estimateEffortMinutes,
46
+ wrapResultsInSarif,
47
+ type AdapterResult,
48
+ type KnownScanner,
49
+ } from "./common.js";
50
+
51
+ const ESLINT: KnownScanner = "eslint";
52
+
53
+ /**
54
+ * ESLint JSON file entry as produced by `eslint -f json`. We type it
55
+ * permissively because many ESLint fields are optional depending on
56
+ * version and plugin.
57
+ */
58
+ interface EslintFileReport {
59
+ readonly filePath?: string;
60
+ readonly messages?: ReadonlyArray<EslintMessage>;
61
+ }
62
+
63
+ interface EslintMessage {
64
+ readonly ruleId?: string | null;
65
+ readonly severity?: number;
66
+ readonly message?: string;
67
+ readonly line?: number;
68
+ readonly column?: number;
69
+ readonly endLine?: number;
70
+ readonly endColumn?: number;
71
+ readonly fatal?: boolean;
72
+ }
73
+
74
+ /**
75
+ * Accept ESLint native JSON output and return a normalized
76
+ * `PersistedSarif` document plus counts.
77
+ *
78
+ * @param input Raw ESLint JSON (string or parsed array).
79
+ * @returns Adapter result.
80
+ * @throws When the input is not a valid ESLint report.
81
+ */
82
+ export function adaptEslint(input: unknown): AdapterResult {
83
+ const parsed = typeof input === "string" ? (JSON.parse(input) as unknown) : input;
84
+ if (!Array.isArray(parsed)) {
85
+ throw new Error(`[adapter:eslint] expected an array of file reports`);
86
+ }
87
+
88
+ const results: Array<ReturnType<typeof buildSarifResult>> = [];
89
+ let totalEffortMinutes = 0;
90
+
91
+ for (const fileReport of parsed as ReadonlyArray<EslintFileReport>) {
92
+ const filePath = fileReport?.filePath;
93
+ if (typeof filePath !== "string" || !filePath) continue;
94
+ const messages = Array.isArray(fileReport.messages) ? fileReport.messages : [];
95
+ for (const msg of messages) {
96
+ const level = mapSeverity(msg.severity);
97
+ const ruleId = typeof msg.ruleId === "string" ? msg.ruleId : "eslint.unknown";
98
+ const line = typeof msg.line === "number" && msg.line > 0 ? msg.line : 1;
99
+ const column = typeof msg.column === "number" && msg.column > 0 ? msg.column : 1;
100
+ const effort = estimateEffortMinutes(level);
101
+ totalEffortMinutes += effort;
102
+ results.push(
103
+ buildSarifResult({
104
+ ruleId,
105
+ level,
106
+ message: msg.message ?? ruleId,
107
+ uri: filePath,
108
+ startLine: line,
109
+ startColumn: column,
110
+ endLine: typeof msg.endLine === "number" ? msg.endLine : undefined,
111
+ endColumn: typeof msg.endColumn === "number" ? msg.endColumn : undefined,
112
+ effortMinutes: effort,
113
+ }),
114
+ );
115
+ }
116
+ }
117
+
118
+ return {
119
+ document: wrapResultsInSarif(ESLINT, "unknown", results),
120
+ sourceTool: ESLINT,
121
+ findingCount: results.length,
122
+ totalEffortMinutes,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Translate ESLint's numeric severity to a SARIF level. ESLint uses:
128
+ *
129
+ * 0 = off / disabled → `"note"` (informational)
130
+ * 1 = warn → `"warning"`
131
+ * 2 = error → `"error"`
132
+ *
133
+ * Unknown values default to `"warning"` so the finding is still
134
+ * visible without being treated as a blocker.
135
+ */
136
+ function mapSeverity(severity: number | undefined): SarifLevel {
137
+ switch (severity) {
138
+ case 2:
139
+ return "error";
140
+ case 1:
141
+ return "warning";
142
+ case 0:
143
+ return "note";
144
+ default:
145
+ return "warning";
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Assemble a SARIF `result` object from the narrow set of fields an
151
+ * ESLint message provides. The shape matches what the SarifStore
152
+ * expects when hydrating a finding from a persisted document.
153
+ */
154
+ function buildSarifResult(opts: {
155
+ ruleId: string;
156
+ level: SarifLevel;
157
+ message: string;
158
+ uri: string;
159
+ startLine: number;
160
+ startColumn: number;
161
+ endLine?: number | undefined;
162
+ endColumn?: number | undefined;
163
+ effortMinutes: number;
164
+ }) {
165
+ return {
166
+ ruleId: opts.ruleId,
167
+ level: opts.level,
168
+ message: { text: opts.message },
169
+ locations: [
170
+ {
171
+ physicalLocation: {
172
+ artifactLocation: { uri: opts.uri },
173
+ region: {
174
+ startLine: opts.startLine,
175
+ startColumn: opts.startColumn,
176
+ ...(opts.endLine !== undefined ? { endLine: opts.endLine } : {}),
177
+ ...(opts.endColumn !== undefined ? { endColumn: opts.endColumn } : {}),
178
+ },
179
+ },
180
+ },
181
+ ],
182
+ properties: {
183
+ sourceTool: ESLINT,
184
+ effortMinutes: opts.effortMinutes,
185
+ },
186
+ };
187
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Public SDK entry point for the per-scanner SARIF adapters.
3
+ *
4
+ * Adapters convert a scanner's native output (SARIF, JSON, or some
5
+ * other structured format) into a normalized `PersistedSarif`
6
+ * document that the `SarifStore` can ingest directly. Every adapter
7
+ * enriches its findings with a stable `effortMinutes` value on the
8
+ * `properties` bag so the Stop quality gate and the project score
9
+ * engine can compute a Technical Debt Ratio.
10
+ *
11
+ * Usage:
12
+ *
13
+ * ```ts
14
+ * import {
15
+ * adaptScannerOutput,
16
+ * adaptSemgrep,
17
+ * adaptEslint,
18
+ * adaptBandit,
19
+ * adaptStryker,
20
+ * } from "claude-crap/adapters";
21
+ *
22
+ * const result = adaptScannerOutput("eslint", rawJsonFromEslint);
23
+ * sarifStore.ingestRun(result.document, result.sourceTool);
24
+ * ```
25
+ *
26
+ * @module adapters
27
+ */
28
+
29
+ export { adaptSemgrep } from "./semgrep.js";
30
+ export { adaptEslint } from "./eslint.js";
31
+ export { adaptBandit } from "./bandit.js";
32
+ export { adaptStryker } from "./stryker.js";
33
+
34
+ export {
35
+ DEFAULT_EFFORT_BY_SEVERITY,
36
+ KNOWN_SCANNERS,
37
+ estimateEffortMinutes,
38
+ wrapResultsInSarif,
39
+ } from "./common.js";
40
+
41
+ export type { AdapterResult, KnownScanner } from "./common.js";
42
+
43
+ import { adaptSemgrep } from "./semgrep.js";
44
+ import { adaptEslint } from "./eslint.js";
45
+ import { adaptBandit } from "./bandit.js";
46
+ import { adaptStryker } from "./stryker.js";
47
+ import type { AdapterResult, KnownScanner } from "./common.js";
48
+
49
+ /**
50
+ * Route a raw scanner output to the correct adapter based on its
51
+ * name. Preferred entry point for the `ingest_scanner_output` MCP
52
+ * tool — the dispatch is a single switch so the compiler can verify
53
+ * every case with `never` exhaustiveness.
54
+ *
55
+ * @param scanner One of the known scanner identifiers.
56
+ * @param rawOutput The scanner's native output (string or parsed).
57
+ * @returns A normalized `AdapterResult`.
58
+ * @throws When `scanner` is unknown or the raw output is malformed.
59
+ */
60
+ export function adaptScannerOutput(
61
+ scanner: KnownScanner,
62
+ rawOutput: unknown,
63
+ ): AdapterResult {
64
+ switch (scanner) {
65
+ case "semgrep":
66
+ return adaptSemgrep(rawOutput);
67
+ case "eslint":
68
+ return adaptEslint(rawOutput);
69
+ case "bandit":
70
+ return adaptBandit(rawOutput);
71
+ case "stryker":
72
+ return adaptStryker(rawOutput);
73
+ default: {
74
+ const exhaustive: never = scanner;
75
+ throw new Error(`[adapters] Unknown scanner: ${String(exhaustive)}`);
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Semgrep adapter.
3
+ *
4
+ * Semgrep already emits SARIF 2.1.0 natively when invoked with
5
+ * `--sarif`, so this adapter's job is not translation but
6
+ * **enrichment**: we walk every `result` entry and stamp a
7
+ * `properties.effortMinutes` value so the Stop quality gate can
8
+ * compute a Technical Debt Ratio, plus we normalize the
9
+ * `properties.sourceTool` field so downstream consumers always know
10
+ * the finding came from Semgrep.
11
+ *
12
+ * If the caller passes a string, we parse it as JSON. If they pass an
13
+ * object that already matches the SARIF 2.1.0 envelope, we use it
14
+ * directly. Anything else throws a descriptive error that the MCP
15
+ * tool handler surfaces back to the LLM.
16
+ *
17
+ * @module adapters/semgrep
18
+ */
19
+
20
+ import type { PersistedSarif } from "../sarif/sarif-store.js";
21
+ import type { SarifLevel } from "../sarif/sarif-builder.js";
22
+ import {
23
+ estimateEffortMinutes,
24
+ type AdapterResult,
25
+ type KnownScanner,
26
+ } from "./common.js";
27
+
28
+ const SEMGREP: KnownScanner = "semgrep";
29
+
30
+ /**
31
+ * Rule-id effort overrides. Semgrep emits lots of stylistic rules
32
+ * that take less than a minute to fix and a handful of deep security
33
+ * rules that deserve more budget than the default warning tier. The
34
+ * list below is intentionally short — teams should extend it.
35
+ */
36
+ const SEMGREP_EFFORT_OVERRIDES: ReadonlyMap<RegExp, number> = new Map([
37
+ [/security\./i, 90],
38
+ [/sqli|xss|ssrf|rce|deserial|crypto/i, 120],
39
+ [/style\./i, 5],
40
+ [/formatting\./i, 3],
41
+ ]);
42
+
43
+ /**
44
+ * Accept a Semgrep SARIF document (as a string or object) and return
45
+ * an enriched `PersistedSarif` with effort estimates and a normalized
46
+ * `sourceTool` field.
47
+ *
48
+ * @param input Raw SARIF document from Semgrep (`JSON.stringify`ed or parsed).
49
+ * @returns The enriched document plus per-run stats.
50
+ * @throws When the input is not a SARIF 2.1.0 document.
51
+ */
52
+ export function adaptSemgrep(input: unknown): AdapterResult {
53
+ const doc = coerceToSarif(input);
54
+
55
+ let findingCount = 0;
56
+ let totalEffortMinutes = 0;
57
+
58
+ // We deep-clone the document so callers don't observe a mutation on
59
+ // the value they passed us. JSON round-trip is cheap here; a
60
+ // typical Semgrep SARIF report is well under 1 MB.
61
+ //
62
+ // We operate on the cloned value through a loose `Record`-based view
63
+ // to keep the adapter agnostic to the full SARIF schema — the
64
+ // canonical types live in `sarif-store.ts` and we only care about a
65
+ // handful of fields here. The final return casts through `unknown`
66
+ // because the JSON round-trip is shape-preserving by construction.
67
+ const cloned = JSON.parse(JSON.stringify(doc)) as {
68
+ runs?: Array<Record<string, unknown>>;
69
+ };
70
+ const runs = Array.isArray(cloned.runs) ? cloned.runs : [];
71
+
72
+ for (const run of runs) {
73
+ const rawResults = run["results"];
74
+ const results = Array.isArray(rawResults) ? (rawResults as Array<Record<string, unknown>>) : [];
75
+ for (const result of results) {
76
+ findingCount += 1;
77
+ const ruleId = typeof result["ruleId"] === "string" ? (result["ruleId"] as string) : "";
78
+ const level = result["level"] as SarifLevel | undefined;
79
+ const override = matchOverride(ruleId);
80
+ const effort = estimateEffortMinutes(level, override);
81
+ totalEffortMinutes += effort;
82
+ const existingProps =
83
+ result["properties"] && typeof result["properties"] === "object"
84
+ ? (result["properties"] as Record<string, unknown>)
85
+ : {};
86
+ result["properties"] = {
87
+ ...existingProps,
88
+ sourceTool: SEMGREP,
89
+ effortMinutes: effort,
90
+ };
91
+ }
92
+
93
+ // Overwrite tool.driver.name so store-level filters always match
94
+ // `"semgrep"` regardless of the label Semgrep reported. Preserve
95
+ // the existing version and rules[] array when present.
96
+ const existingTool = (run["tool"] as Record<string, unknown> | undefined) ?? {};
97
+ const existingDriver = (existingTool["driver"] as Record<string, unknown> | undefined) ?? {};
98
+ const driverOut: Record<string, unknown> = {
99
+ name: SEMGREP,
100
+ version: typeof existingDriver["version"] === "string" ? existingDriver["version"] : "unknown",
101
+ };
102
+ if (Array.isArray(existingDriver["rules"])) {
103
+ driverOut["rules"] = existingDriver["rules"];
104
+ }
105
+ run["tool"] = { driver: driverOut };
106
+ }
107
+
108
+ return {
109
+ document: cloned as unknown as PersistedSarif,
110
+ sourceTool: SEMGREP,
111
+ findingCount,
112
+ totalEffortMinutes,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Accept either a pre-parsed SARIF object or a JSON string and return
118
+ * a strongly-typed `PersistedSarif`. Throws on malformed input.
119
+ *
120
+ * @param input Raw caller-provided value.
121
+ */
122
+ function coerceToSarif(input: unknown): PersistedSarif {
123
+ const parsed = typeof input === "string" ? (JSON.parse(input) as unknown) : input;
124
+ if (!parsed || typeof parsed !== "object") {
125
+ throw new Error(`[adapter:semgrep] input is not a SARIF object`);
126
+ }
127
+ const doc = parsed as { version?: unknown; runs?: unknown };
128
+ if (doc.version !== "2.1.0") {
129
+ throw new Error(
130
+ `[adapter:semgrep] expected SARIF version 2.1.0, got ${String(doc.version)}`,
131
+ );
132
+ }
133
+ if (!Array.isArray(doc.runs)) {
134
+ throw new Error(`[adapter:semgrep] document is missing a runs[] array`);
135
+ }
136
+ return parsed as PersistedSarif;
137
+ }
138
+
139
+ /**
140
+ * Return the effort override (in minutes) matching the first pattern
141
+ * that matches the given rule id, or `undefined` when none match.
142
+ *
143
+ * @param ruleId Semgrep rule identifier.
144
+ */
145
+ function matchOverride(ruleId: string): number | undefined {
146
+ for (const [pattern, minutes] of SEMGREP_EFFORT_OVERRIDES) {
147
+ if (pattern.test(ruleId)) return minutes;
148
+ }
149
+ return undefined;
150
+ }