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,145 @@
1
+ /**
2
+ * AJV-backed minimal SARIF 2.1.0 document validator.
3
+ *
4
+ * F-A05-01: the `ingest_sarif` MCP tool accepts a caller-supplied
5
+ * `sarifDocument` object and, before this module existed, only
6
+ * checked `version === "2.1.0"`. That was enough for tool-call
7
+ * dispatch but not for the payload itself — a SARIF with a missing
8
+ * `runs[]`, a `results` array of wrong-type entries, or a result
9
+ * without a `ruleId` would still be accepted by the MCP tool and
10
+ * flow through to the store, the dashboard, and any downstream
11
+ * consumer that uploads claude-crap's SARIF to GitHub code-scanning
12
+ * or an IDE viewer.
13
+ *
14
+ * This module uses the `ajv` dependency (already in package.json) to
15
+ * compile a minimal JSON Schema that covers exactly the fields
16
+ * claude-crap reads: `version`, `runs`, `runs[].tool.driver.name`,
17
+ * and the per-result shape. Everything else (tool metadata, rule
18
+ * definitions, snippets, etc.) is passthrough — we do not enforce
19
+ * the full SARIF 2.1.0 spec because claude-crap does not consume
20
+ * those fields.
21
+ *
22
+ * The compiled validator is cached so the ~5 ms AJV compile cost is
23
+ * paid once per MCP server process, not once per ingestion.
24
+ *
25
+ * @module sarif/sarif-validator
26
+ */
27
+
28
+ import { Ajv, type ValidateFunction } from "ajv";
29
+
30
+ /**
31
+ * Minimal JSON Schema covering every field claude-crap reads from a
32
+ * SARIF 2.1.0 document. Passthrough fields are allowed because
33
+ * `additionalProperties` is left at the default (`true`).
34
+ *
35
+ * Keep this schema in sync with `hydrateFindingFromResult` in
36
+ * `src/sarif/sarif-store.ts` — anything the store reads MUST be
37
+ * covered here, and nothing else should be enforced.
38
+ */
39
+ const SARIF_MINIMAL_SCHEMA = {
40
+ type: "object",
41
+ properties: {
42
+ version: { type: "string", enum: ["2.1.0"] },
43
+ $schema: { type: "string" },
44
+ runs: {
45
+ type: "array",
46
+ items: {
47
+ type: "object",
48
+ properties: {
49
+ tool: {
50
+ type: "object",
51
+ properties: {
52
+ driver: {
53
+ type: "object",
54
+ properties: {
55
+ name: { type: "string", minLength: 1 },
56
+ version: { type: "string" },
57
+ },
58
+ required: ["name"],
59
+ },
60
+ },
61
+ required: ["driver"],
62
+ },
63
+ results: {
64
+ type: "array",
65
+ items: {
66
+ type: "object",
67
+ properties: {
68
+ ruleId: { type: "string", minLength: 1 },
69
+ level: { type: "string", enum: ["none", "note", "warning", "error"] },
70
+ message: {
71
+ type: "object",
72
+ properties: { text: { type: "string", minLength: 1 } },
73
+ required: ["text"],
74
+ },
75
+ locations: { type: "array" },
76
+ properties: { type: "object" },
77
+ },
78
+ required: ["ruleId", "message"],
79
+ },
80
+ },
81
+ },
82
+ required: ["tool", "results"],
83
+ },
84
+ },
85
+ },
86
+ required: ["version", "runs"],
87
+ } as const;
88
+
89
+ /**
90
+ * Lazily-compiled validator instance. `null` until the first call to
91
+ * {@link validateSarifDocument}, then reused for the lifetime of the
92
+ * process.
93
+ */
94
+ let cachedValidator: ValidateFunction | null = null;
95
+
96
+ /**
97
+ * Returned by {@link validateSarifDocument} when the document fails
98
+ * schema validation. Includes the full AJV error array for callers
99
+ * that want to surface structured diagnostics.
100
+ */
101
+ export class SarifValidationError extends Error {
102
+ public readonly errors: unknown;
103
+
104
+ constructor(message: string, errors: unknown) {
105
+ super(message);
106
+ this.name = "SarifValidationError";
107
+ this.errors = errors;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Obtain the compiled AJV validator, compiling on first use.
113
+ *
114
+ * The schema above intentionally allows passthrough fields on every
115
+ * object (AJV's default `additionalProperties: true`). We disable
116
+ * `strict` so AJV does not warn about benign constructs like the
117
+ * `format`/`enum` combination.
118
+ */
119
+ function getValidator(): ValidateFunction {
120
+ if (cachedValidator) return cachedValidator;
121
+ const ajv = new Ajv({ allErrors: false, strict: false });
122
+ const validator = ajv.compile(SARIF_MINIMAL_SCHEMA);
123
+ cachedValidator = validator;
124
+ return validator;
125
+ }
126
+
127
+ /**
128
+ * Validate a SARIF 2.1.0 document against the minimal schema. Throws
129
+ * {@link SarifValidationError} when the document does not match.
130
+ *
131
+ * @param doc Document to validate. May be any value — the validator
132
+ * treats non-object inputs as a schema violation.
133
+ * @throws {@link SarifValidationError} on any validation failure.
134
+ */
135
+ export function validateSarifDocument(doc: unknown): void {
136
+ const validator = getValidator();
137
+ if (validator(doc)) return;
138
+ const first = validator.errors?.[0];
139
+ const path = first?.instancePath?.length ? first.instancePath : "<root>";
140
+ const message = first?.message ?? "unknown validation error";
141
+ throw new SarifValidationError(
142
+ `[sarif-validator] SARIF document is not valid 2.1.0: ${path} ${message}`,
143
+ validator.errors ?? null,
144
+ );
145
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * JSON Schema (Draft-07) definitions for every tool exposed by the MCP server.
3
+ *
4
+ * Each schema uses `enum`, `pattern`, `minimum`, `maximum`, `oneOf` and
5
+ * `additionalProperties: false` to eliminate schema hallucinations from the
6
+ * LLM. The MCP SDK automatically validates tool-call inputs against these
7
+ * schemas before invoking the handler — any drift produces a deterministic
8
+ * error that the agent can consume and correct.
9
+ *
10
+ * These `description` fields are read by the LLM at tool-listing time and
11
+ * become part of the agent's context, so they must be precise, imperative,
12
+ * and never speculative. Keep them short but actionable.
13
+ *
14
+ * @module schemas/tool-schemas
15
+ */
16
+
17
+ // The MCP SDK consumes these as the `inputSchema` field of a Tool. We type
18
+ // them with `as const` so TypeScript infers literal types and the MCP SDK
19
+ // accepts them without runtime casting.
20
+
21
+ /**
22
+ * Schema for the `compute_crap` tool. Returns a CRAP score for a single
23
+ * function and a block decision against the configured threshold.
24
+ */
25
+ export const computeCrapSchema = {
26
+ type: "object",
27
+ description:
28
+ "Compute the CRAP (Change Risk Anti-Patterns) index for a single function. Returns the score and whether it exceeds the configured threshold. A blocked result means the function must be decomposed or covered by more tests before the Stop quality gate will pass.",
29
+ properties: {
30
+ cyclomaticComplexity: {
31
+ type: "integer",
32
+ minimum: 1,
33
+ maximum: 1000,
34
+ description: "Cyclomatic complexity of the function (number of linearly independent paths).",
35
+ },
36
+ coveragePercent: {
37
+ type: "number",
38
+ minimum: 0,
39
+ maximum: 100,
40
+ description: "Test coverage percentage for the function, in the range [0, 100].",
41
+ },
42
+ functionName: {
43
+ type: "string",
44
+ pattern: "^[A-Za-z_$][A-Za-z0-9_$.:<>]*$",
45
+ minLength: 1,
46
+ maxLength: 256,
47
+ description: "Fully qualified name of the function under analysis, used for SARIF traceability.",
48
+ },
49
+ filePath: {
50
+ type: "string",
51
+ minLength: 1,
52
+ maxLength: 4096,
53
+ description: "Absolute or workspace-relative path to the source file that contains the function.",
54
+ },
55
+ },
56
+ required: ["cyclomaticComplexity", "coveragePercent", "functionName", "filePath"],
57
+ additionalProperties: false,
58
+ } as const;
59
+
60
+ /**
61
+ * Schema for the `compute_tdr` tool. Returns a Technical Debt Ratio and a
62
+ * maintainability letter rating for a scope (project, module, or file).
63
+ */
64
+ export const computeTdrSchema = {
65
+ type: "object",
66
+ description:
67
+ "Compute the Technical Debt Ratio (TDR) for a scope and return the maintainability letter rating (A..E). Rating E always halts the workflow regardless of the configured tolerance. Use this after aggregating remediation estimates from SARIF findings.",
68
+ properties: {
69
+ remediationMinutes: {
70
+ type: "number",
71
+ minimum: 0,
72
+ maximum: 10_000_000,
73
+ description: "Total estimated remediation effort in minutes, summed across every finding in the scope.",
74
+ },
75
+ totalLinesOfCode: {
76
+ type: "integer",
77
+ minimum: 1,
78
+ maximum: 100_000_000,
79
+ description: "Physical lines of code in the scope (project, module, or file).",
80
+ },
81
+ scope: {
82
+ type: "string",
83
+ enum: ["project", "module", "file"],
84
+ description: "Aggregation scope for the TDR computation.",
85
+ },
86
+ },
87
+ required: ["remediationMinutes", "totalLinesOfCode", "scope"],
88
+ additionalProperties: false,
89
+ } as const;
90
+
91
+ /**
92
+ * Schema for the `analyze_file_ast` tool. Returns deterministic AST
93
+ * metrics (LOC, cyclomatic complexity, node counts) for a source file.
94
+ */
95
+ export const analyzeFileAstSchema = {
96
+ type: "object",
97
+ description:
98
+ "Parse a source file with tree-sitter and return deterministic metrics (lines of code, cyclomatic complexity per function, top-level node counts). Prefer this tool over reading the file directly — it is faster and will not bloat the agent context.",
99
+ properties: {
100
+ filePath: {
101
+ type: "string",
102
+ minLength: 1,
103
+ maxLength: 4096,
104
+ // The lookahead pattern rejects any path traversal (`../`) to prevent
105
+ // the LLM from reading files outside the workspace. Any absolute path
106
+ // that does not contain `../` is still allowed.
107
+ pattern: "^(?!.*\\.\\./).*$",
108
+ description: "Path to the file to analyze. Paths containing `../` are rejected to prevent workspace escape.",
109
+ },
110
+ language: {
111
+ type: "string",
112
+ enum: ["csharp", "javascript", "typescript", "python", "java"],
113
+ description: "Source language of the file. Determines which tree-sitter grammar to load.",
114
+ },
115
+ },
116
+ required: ["filePath", "language"],
117
+ additionalProperties: false,
118
+ } as const;
119
+
120
+ /**
121
+ * Schema for the `score_project` tool. Aggregates the latest SARIF
122
+ * report and the workspace size into a single project score with
123
+ * Maintainability / Reliability / Security letter grades, an overall
124
+ * grade, the dashboard URL (when running), and the SARIF report path.
125
+ */
126
+ export const scoreProjectSchema = {
127
+ type: "object",
128
+ description:
129
+ "Compute the aggregate project score (Maintainability / Reliability / Security / Overall A..E), and return both a chat-friendly Markdown summary and a structured JSON snapshot. Includes the local dashboard URL and the consolidated SARIF report path so the user can drill in without opening any extra tooling.",
130
+ properties: {
131
+ format: {
132
+ type: "string",
133
+ enum: ["markdown", "json", "both"],
134
+ description:
135
+ "Output format. `markdown` returns only the chat summary, `json` returns only the structured snapshot, `both` (default) returns both as separate content blocks.",
136
+ },
137
+ },
138
+ required: [],
139
+ additionalProperties: false,
140
+ } as const;
141
+
142
+ /**
143
+ * Schema for the `require_test_harness` tool. Checks whether a production
144
+ * source file has an accompanying test file in any of the conventional
145
+ * locations the resolver supports (sibling `.test.`, `__tests__/`, mirror
146
+ * tree, Python `test_` prefix).
147
+ */
148
+ export const requireTestHarnessSchema = {
149
+ type: "object",
150
+ description:
151
+ "Check whether a production source file has a matching test file. Returns the first existing test path, or the full list of paths the resolver probed when none exists. Use this BEFORE writing any functional code — the CLAUDE.md Golden Rule requires a test harness to exist first.",
152
+ properties: {
153
+ filePath: {
154
+ type: "string",
155
+ minLength: 1,
156
+ maxLength: 4096,
157
+ pattern: "^(?!.*\\.\\./).*$",
158
+ description:
159
+ "Path to the production file. Paths containing `../` are rejected to prevent workspace escape.",
160
+ },
161
+ },
162
+ required: ["filePath"],
163
+ additionalProperties: false,
164
+ } as const;
165
+
166
+ /**
167
+ * Schema for the `ingest_scanner_output` tool. Accepts a scanner
168
+ * identifier (Semgrep, ESLint, Bandit, Stryker) plus that scanner's
169
+ * native output (SARIF or JSON), routes the input through the
170
+ * matching adapter in `src/adapters/`, and persists the normalized
171
+ * SARIF 2.1.0 document in the store.
172
+ *
173
+ * This tool is the preferred path for ingesting scanner output that
174
+ * is not already SARIF — `ingest_sarif` remains the right choice
175
+ * when you already have a SARIF document and just need deduplication.
176
+ */
177
+ export const ingestScannerOutputSchema = {
178
+ type: "object",
179
+ description:
180
+ "Ingest a scanner's native output (Semgrep SARIF, ESLint JSON, Bandit JSON, or Stryker JSON), route it through the matching adapter, enrich every finding with an effort estimate, and persist the normalized SARIF 2.1.0 document. Prefer this tool over `ingest_sarif` whenever the scanner does not emit SARIF natively.",
181
+ properties: {
182
+ scanner: {
183
+ type: "string",
184
+ enum: ["semgrep", "eslint", "bandit", "stryker"],
185
+ description: "Identifier of the producing scanner.",
186
+ },
187
+ rawOutput: {
188
+ description:
189
+ "The scanner's native output. Accepts either a JSON string (as produced by the scanner's CLI) or a pre-parsed JSON object / array.",
190
+ oneOf: [{ type: "string" }, { type: "object" }, { type: "array" }],
191
+ },
192
+ },
193
+ required: ["scanner", "rawOutput"],
194
+ additionalProperties: false,
195
+ } as const;
196
+
197
+ /**
198
+ * Schema for the `ingest_sarif` tool. Accepts a raw SARIF 2.1.0 document
199
+ * from an external scanner, deduplicates against the internal store, and
200
+ * normalizes the output into claude-crap's canonical format.
201
+ */
202
+ export const ingestSarifSchema = {
203
+ type: "object",
204
+ description:
205
+ "Ingest a raw SARIF 2.1.0 report produced by an external scanner (Semgrep, ESLint, Bandit, Stryker, etc.), deduplicate it against the internal store, and return the normalized document. The agent should call this once per scanner invocation, not once per finding.",
206
+ properties: {
207
+ sarifDocument: {
208
+ type: "object",
209
+ description: "A full SARIF 2.1.0 document with `version` and `runs` keys.",
210
+ properties: {
211
+ version: { type: "string", enum: ["2.1.0"] },
212
+ $schema: { type: "string" },
213
+ runs: { type: "array", minItems: 1 },
214
+ },
215
+ required: ["version", "runs"],
216
+ },
217
+ sourceTool: {
218
+ type: "string",
219
+ pattern: "^[a-zA-Z0-9._-]{1,64}$",
220
+ description: "Stable identifier of the tool that produced the report (`semgrep`, `eslint`, `bandit`, ...).",
221
+ },
222
+ },
223
+ required: ["sarifDocument", "sourceTool"],
224
+ additionalProperties: false,
225
+ } as const;
package/src/sdk.ts ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Root public SDK for `claude-crap`.
3
+ *
4
+ * This is the module you get when you do
5
+ * `import ... from "claude-crap"`. It is intentionally
6
+ * **side-effect-free**: importing this file does NOT start the MCP
7
+ * server, does NOT open the dashboard, does NOT touch the filesystem.
8
+ * Only the executable entrypoint in `dist/index.js` boots the
9
+ * server — that file is invoked by the `.mcp.json` command and the
10
+ * CLI bin, never as a library.
11
+ *
12
+ * Structure:
13
+ *
14
+ * - `./metrics` — CRAP, TDR, project score, workspace walker
15
+ * - `./sarif` — SARIF 2.1.0 builder and on-disk store
16
+ * - `./ast` — tree-sitter engine, cyclomatic complexity, language config
17
+ * - `./tools` — test-harness resolver used by `require_test_harness`
18
+ *
19
+ * Prefer deep imports
20
+ * (`import { computeCrap } from "claude-crap/metrics"`) over
21
+ * pulling everything through the root — they give TypeScript more
22
+ * precise type information and help tree-shakers drop unused modules.
23
+ *
24
+ * The symbols re-exported here are the ones most code paths need:
25
+ *
26
+ * - `computeCrap`, `computeTdr`, `computeProjectScore`
27
+ * - `renderProjectScoreMarkdown`
28
+ * - `classifyTdr`, `ratingIsWorseThan`
29
+ * - `SarifStore`, `buildSarifDocument`
30
+ * - `TreeSitterEngine`
31
+ *
32
+ * @module claude-crap
33
+ */
34
+
35
+ // --- metrics ---------------------------------------------------------------
36
+ export {
37
+ computeCrap,
38
+ computeTdr,
39
+ classifyTdr,
40
+ ratingIsWorseThan,
41
+ ratingToRank,
42
+ computeProjectScore,
43
+ renderProjectScoreMarkdown,
44
+ estimateWorkspaceLoc,
45
+ } from "./metrics/index.js";
46
+ export type {
47
+ CrapInput,
48
+ CrapResult,
49
+ TdrInput,
50
+ TdrResult,
51
+ ComputeProjectScoreInput,
52
+ DimensionScore,
53
+ FindingsSummary,
54
+ MaintainabilityScore,
55
+ ProjectScore,
56
+ ScoreLocation,
57
+ SeverityRating,
58
+ WorkspaceStats,
59
+ WorkspaceWalkResult,
60
+ } from "./metrics/index.js";
61
+
62
+ // --- sarif -----------------------------------------------------------------
63
+ export { SarifStore, buildSarifDocument } from "./sarif/index.js";
64
+ export type {
65
+ IngestedFinding,
66
+ PersistedSarif,
67
+ SarifFinding,
68
+ SarifLevel,
69
+ SarifLocation,
70
+ SarifStoreOptions,
71
+ SarifToolInfo,
72
+ } from "./sarif/index.js";
73
+
74
+ // --- ast -------------------------------------------------------------------
75
+ export {
76
+ TreeSitterEngine,
77
+ computeCyclomaticComplexity,
78
+ detectLanguageFromPath,
79
+ LANGUAGE_TABLE,
80
+ } from "./ast/index.js";
81
+ export type {
82
+ AnalyzeFileRequest,
83
+ AstNode,
84
+ FileMetrics,
85
+ FunctionMetrics,
86
+ LanguageConfig,
87
+ SupportedLanguage,
88
+ TreeSitterEngineOptions,
89
+ } from "./ast/index.js";
90
+
91
+ // --- tools -----------------------------------------------------------------
92
+ export { findTestFile, isTestFile, candidatePaths } from "./tools/index.js";
93
+ export type { TestFileResolution } from "./tools/index.js";
94
+
95
+ // --- adapters --------------------------------------------------------------
96
+ export {
97
+ adaptScannerOutput,
98
+ adaptSemgrep,
99
+ adaptEslint,
100
+ adaptBandit,
101
+ adaptStryker,
102
+ KNOWN_SCANNERS,
103
+ } from "./adapters/index.js";
104
+ export type { AdapterResult, KnownScanner } from "./adapters/index.js";
105
+
106
+ // --- configuration types ---------------------------------------------------
107
+ // We re-export the config type (not `loadConfig`) so consumers can type
108
+ // their own configs without importing the loader, which would read
109
+ // `process.env` eagerly.
110
+ export type { MaintainabilityRating, CrapConfig } from "./config.js";
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Unit tests for the Bandit adapter.
3
+ *
4
+ * Bandit's `-f json` output has a `results[]` array of findings with
5
+ * `issue_severity` strings HIGH / MEDIUM / LOW that map to SARIF
6
+ * error / warning / note.
7
+ *
8
+ * @module tests/adapters/bandit.test
9
+ */
10
+
11
+ import { describe, it } from "node:test";
12
+ import assert from "node:assert/strict";
13
+
14
+ import { adaptBandit } from "../../adapters/bandit.js";
15
+
16
+ describe("adaptBandit", () => {
17
+ it("maps HIGH severity to error", () => {
18
+ const raw = {
19
+ results: [
20
+ {
21
+ filename: "app.py",
22
+ line_number: 12,
23
+ col_offset: 4,
24
+ test_id: "B608",
25
+ test_name: "hardcoded_sql_expressions",
26
+ issue_severity: "HIGH",
27
+ issue_confidence: "HIGH",
28
+ issue_text: "Possible SQL injection",
29
+ issue_cwe: { id: 89 },
30
+ },
31
+ ],
32
+ };
33
+ const result = adaptBandit(raw);
34
+ assert.equal(result.sourceTool, "bandit");
35
+ const finding = result.document.runs[0]?.results?.[0] as {
36
+ level?: string;
37
+ ruleId?: string;
38
+ properties?: { cwe?: number; confidence?: string };
39
+ };
40
+ assert.equal(finding?.level, "error");
41
+ assert.equal(finding?.ruleId, "bandit.B608");
42
+ assert.equal(finding?.properties?.cwe, 89);
43
+ assert.equal(finding?.properties?.confidence, "HIGH");
44
+ });
45
+
46
+ it("maps MEDIUM severity to warning and LOW severity to note", () => {
47
+ const raw = {
48
+ results: [
49
+ {
50
+ filename: "a.py",
51
+ line_number: 1,
52
+ col_offset: 0,
53
+ test_id: "B101",
54
+ issue_severity: "MEDIUM",
55
+ },
56
+ {
57
+ filename: "b.py",
58
+ line_number: 2,
59
+ col_offset: 0,
60
+ test_id: "B102",
61
+ issue_severity: "LOW",
62
+ },
63
+ ],
64
+ };
65
+ const result = adaptBandit(raw);
66
+ const levels = (result.document.runs[0]?.results ?? [])
67
+ .map((r) => (r as { level?: string }).level)
68
+ .sort();
69
+ assert.deepEqual(levels, ["note", "warning"]);
70
+ });
71
+
72
+ it("converts 0-based col_offset to 1-based SARIF startColumn", () => {
73
+ const raw = {
74
+ results: [
75
+ { filename: "a.py", line_number: 5, col_offset: 0, test_id: "B101", issue_severity: "LOW" },
76
+ ],
77
+ };
78
+ const result = adaptBandit(raw);
79
+ const finding = result.document.runs[0]?.results?.[0] as {
80
+ locations?: Array<{ physicalLocation?: { region?: { startColumn?: number } } }>;
81
+ };
82
+ assert.equal(finding?.locations?.[0]?.physicalLocation?.region?.startColumn, 1);
83
+ });
84
+
85
+ it("assigns a high effort budget to HIGH severity findings", () => {
86
+ const high = adaptBandit({
87
+ results: [
88
+ { filename: "a.py", line_number: 1, col_offset: 0, test_id: "B1", issue_severity: "HIGH" },
89
+ ],
90
+ });
91
+ const low = adaptBandit({
92
+ results: [
93
+ { filename: "a.py", line_number: 1, col_offset: 0, test_id: "B1", issue_severity: "LOW" },
94
+ ],
95
+ });
96
+ assert.ok(high.totalEffortMinutes > low.totalEffortMinutes * 2);
97
+ });
98
+
99
+ it("accepts a JSON string", () => {
100
+ const raw = JSON.stringify({
101
+ results: [
102
+ { filename: "x.py", line_number: 1, col_offset: 0, test_id: "B101", issue_severity: "LOW" },
103
+ ],
104
+ });
105
+ assert.equal(adaptBandit(raw).findingCount, 1);
106
+ });
107
+
108
+ it("throws on inputs missing the results[] array", () => {
109
+ assert.throws(() => adaptBandit({ not: "a bandit report" }));
110
+ });
111
+ });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Unit tests for the `adaptScannerOutput` dispatcher.
3
+ *
4
+ * The dispatcher is a thin switch that picks the right adapter based
5
+ * on the `scanner` argument. These tests pin the happy path per
6
+ * scanner and the error path for unknown names.
7
+ *
8
+ * @module tests/adapters/dispatch.test
9
+ */
10
+
11
+ import { describe, it } from "node:test";
12
+ import assert from "node:assert/strict";
13
+
14
+ import { adaptScannerOutput, KNOWN_SCANNERS } from "../../adapters/index.js";
15
+
16
+ describe("adaptScannerOutput", () => {
17
+ it("routes semgrep input through the semgrep adapter", () => {
18
+ const result = adaptScannerOutput("semgrep", {
19
+ version: "2.1.0",
20
+ runs: [
21
+ {
22
+ tool: { driver: { name: "semgrep", version: "1" } },
23
+ results: [
24
+ {
25
+ ruleId: "r1",
26
+ level: "error",
27
+ message: { text: "m" },
28
+ locations: [
29
+ {
30
+ physicalLocation: {
31
+ artifactLocation: { uri: "a.py" },
32
+ region: { startLine: 1, startColumn: 1 },
33
+ },
34
+ },
35
+ ],
36
+ },
37
+ ],
38
+ },
39
+ ],
40
+ });
41
+ assert.equal(result.sourceTool, "semgrep");
42
+ assert.equal(result.findingCount, 1);
43
+ });
44
+
45
+ it("routes eslint input through the eslint adapter", () => {
46
+ const result = adaptScannerOutput("eslint", [
47
+ {
48
+ filePath: "/a.js",
49
+ messages: [{ ruleId: "no-undef", severity: 2, message: "m", line: 1, column: 1 }],
50
+ },
51
+ ]);
52
+ assert.equal(result.sourceTool, "eslint");
53
+ assert.equal(result.findingCount, 1);
54
+ });
55
+
56
+ it("routes bandit input through the bandit adapter", () => {
57
+ const result = adaptScannerOutput("bandit", {
58
+ results: [
59
+ { filename: "a.py", line_number: 1, col_offset: 0, test_id: "B101", issue_severity: "LOW" },
60
+ ],
61
+ });
62
+ assert.equal(result.sourceTool, "bandit");
63
+ assert.equal(result.findingCount, 1);
64
+ });
65
+
66
+ it("routes stryker input through the stryker adapter", () => {
67
+ const result = adaptScannerOutput("stryker", {
68
+ schemaVersion: "1.0",
69
+ files: {
70
+ "src/a.ts": {
71
+ mutants: [
72
+ {
73
+ id: "1",
74
+ mutatorName: "Cond",
75
+ replacement: "!",
76
+ location: {
77
+ start: { line: 1, column: 1 },
78
+ end: { line: 1, column: 5 },
79
+ },
80
+ status: "Survived",
81
+ },
82
+ ],
83
+ },
84
+ },
85
+ });
86
+ assert.equal(result.sourceTool, "stryker");
87
+ assert.equal(result.findingCount, 1);
88
+ });
89
+
90
+ it("throws on unknown scanner names", () => {
91
+ // Cast through unknown → string to keep the test compile-time safe
92
+ // while we exercise the runtime exhaustiveness guard.
93
+ const scanner = "does-not-exist" as unknown as "semgrep";
94
+ assert.throws(() => adaptScannerOutput(scanner, {}));
95
+ });
96
+
97
+ it("KNOWN_SCANNERS is frozen and contains exactly the four supported names", () => {
98
+ assert.deepEqual([...KNOWN_SCANNERS].sort(), ["bandit", "eslint", "semgrep", "stryker"]);
99
+ });
100
+ });