codeblast 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,9 +94,10 @@ Replayed against 50 real commits: 42 correctly stayed silent, 87.5% of comments
94
94
 
95
95
  - **TypeScript at function level: zero missed impact within statically analyzable scope.** Verified by mutation testing:
96
96
  inject mutations into a real repo → run the full test suite to get the ground-truth impact set → compare against predictions.
97
- Current benchmark (tRPC, 950 files): **28/28 mutations, 100% recall**, average precision 0.36 — favoring
98
- false positives over false negatives is a deliberate trade: in a controlled experiment, dropping the conservative edges
99
- raises precision to 0.70 but recall collapses to 14%. Data lives in [`eval/`](eval/).
97
+ Two benchmarks, both hard gates in the weekly acceptance workflow: **tRPC** (vitest, 950 files) **28/28**
98
+ and **graphql-tools** (jest + npm workspaces, 353 files) **10/10** — 100% recall on each, average precision
99
+ 0.33–0.36. Favoring false positives over false negatives is a deliberate trade: in a controlled experiment,
100
+ dropping the conservative edges raises precision to 0.70 but recall collapses to 14%. Data lives in [`eval/`](eval/).
100
101
  - **Blind spots are explicitly flagged.** A blind spot is any call or import that static analysis cannot resolve to an in-repo target — dynamic calls, unresolved calls, failed external-dependency resolution, subprocess boundaries, test-framework globals — not just dynamic calls; each is recorded in `blind_spots` with an "impact may be underestimated" warning, never silently dropped.
101
102
  - **Python is file-level.** Dynamic typing makes function-level zero-miss guarantees impossible in principle, and we don't pretend otherwise.
102
103
 
package/SKILL.md CHANGED
@@ -148,8 +148,9 @@ Replayed over 50 real commits: 42 stayed silent, 87.5% of the comments posted we
148
148
 
149
149
  - TypeScript, function level: **zero missed impact within statically analyzable scope**, checked by
150
150
  mutation testing (inject a fault, run the real test suite, compare failing tests to the prediction).
151
- tRPC benchmark, 950 files: 28/28 mutations recalled, precision 0.36 overall / ≈ 0.70 on the call channel.
152
- The weekly acceptance workflow re-runs this and opens an issue if recall drops below 100%.
151
+ Two benchmarks: tRPC (vitest, 950 files) 28/28, graphql-tools (jest, 353 files) 10/10 — precision
152
+ 0.33–0.36 overall, ≈ 0.70–0.92 on the call channel. Both are hard gates in the weekly acceptance
153
+ workflow, which opens an issue if either drops below 100%.
153
154
  - Conservative edges over-approximate on purpose (an interface method call fans out to every implementer).
154
155
  - Python: file-level; typed calls (`b = Builder(); b.method()`, annotated parameters) are function-level;
155
156
  untyped attribute chains fall back to file level and are recorded as blind spots.
package/dist/bin.js CHANGED
@@ -1058,7 +1058,47 @@ var init_impact = __esm(() => {
1058
1058
 
1059
1059
  // src/impact-cli.ts
1060
1060
  var exports_impact_cli = {};
1061
- var dbPath2, query, maxFlag, maxNodes, db2, targetId, exact, t02, result, ms, byLevel, callItems, fileNamed, fileUnnamed, ordered;
1061
+ function printHuman() {
1062
+ const byLevel = { direct: 0, indirect: 0, tests: 0 };
1063
+ for (const it of result.items)
1064
+ byLevel[it.level]++;
1065
+ const callItems = result.items.filter((it) => it.channel === "call");
1066
+ const fileNamed = result.items.filter((it) => it.channel === "file" && !it.named_miss);
1067
+ const fileUnnamed = result.items.filter((it) => it.channel === "file" && it.named_miss);
1068
+ console.log(`target: ${result.target}`);
1069
+ console.log(`impact: ${result.items.length} nodes (direct=${byLevel.direct} indirect=${byLevel.indirect} tests=${byLevel.tests})${result.truncated ? " [TRUNCATED — wide impact, run the full suite]" : ""}`);
1070
+ console.log(` ├─ call-graph reachable (high confidence): ${callItems.length}`);
1071
+ console.log(` ├─ reachable via named import: ${fileNamed.length}`);
1072
+ console.log(` └─ reachable via unnamed import (execution closure, conservative — do not skip): ${fileUnnamed.length}`);
1073
+ if (result.blind_spot_count > 0)
1074
+ console.log(`blind spots in target file: ${result.blind_spot_count} (impact may be underestimated)`);
1075
+ if (result.co_change_hints.length > 0) {
1076
+ console.log(`co-change hints (no static edge, but historically changed together):`);
1077
+ for (const h of result.co_change_hints)
1078
+ console.log(` ~ ${h.file} (${h.co_commits} co-commits, ${h.evidence})`);
1079
+ }
1080
+ console.log(`query: ${ms}ms
1081
+ `);
1082
+ const testItems = result.items.filter((it) => it.level === "tests");
1083
+ const callNonTest = callItems.filter((it) => it.level !== "tests");
1084
+ const fileNonTest = [...fileNamed, ...fileUnnamed].filter((it) => it.level !== "tests");
1085
+ const shown = showAll ? [...callItems, ...fileNamed, ...fileUnnamed] : [...callNonTest, ...testItems];
1086
+ const LIMIT = 40;
1087
+ for (const it of shown.slice(0, LIMIT)) {
1088
+ const conf = it.confidence === "conservative" ? " ~" : "";
1089
+ const ch = it.channel === "file" ? it.named_miss ? " ·closure" : " ·import" : "";
1090
+ console.log(` [${it.level}${conf}${ch}] ${it.id} (${it.kind}, ${it.hops} hop, via ${it.via_file}:${it.via_line})`);
1091
+ }
1092
+ if (shown.length > LIMIT)
1093
+ console.log(` ... and ${shown.length - LIMIT} more`);
1094
+ if (!showAll && fileNonTest.length > 0) {
1095
+ const files = new Set(fileNonTest.map((it) => it.file)).size;
1096
+ console.log(`
1097
+ + ${fileNonTest.length} conservative items across ${files} files reachable only via imports — --all to list them.`);
1098
+ console.log(` They are part of the no-false-negative promise: run the tests above, do not treat this list as noise.`);
1099
+ }
1100
+ }
1101
+ var dbPath2, query, maxFlag, maxNodes, showAll, db2, targetId, exact, t02, result, ms;
1062
1102
  var init_impact_cli = __esm(() => {
1063
1103
  init_db();
1064
1104
  init_impact();
@@ -1069,6 +1109,7 @@ var init_impact_cli = __esm(() => {
1069
1109
  }
1070
1110
  maxFlag = process.argv.indexOf("--max");
1071
1111
  maxNodes = maxFlag >= 0 ? Number(process.argv[maxFlag + 1]) : 500;
1112
+ showAll = process.argv.includes("--all");
1072
1113
  db2 = openDatabase(dbPath2, { readonly: true });
1073
1114
  targetId = query;
1074
1115
  exact = db2.prepare("SELECT id FROM nodes WHERE id = ?").get(query);
@@ -1090,37 +1131,13 @@ var init_impact_cli = __esm(() => {
1090
1131
  result = impact(db2, targetId, maxNodes);
1091
1132
  ms = (performance.now() - t02).toFixed(0);
1092
1133
  if (process.argv.includes("--json")) {
1093
- console.log(JSON.stringify(result));
1094
- process.exit(0);
1095
- }
1096
- byLevel = { direct: 0, indirect: 0, tests: 0 };
1097
- for (const it of result.items)
1098
- byLevel[it.level]++;
1099
- callItems = result.items.filter((it) => it.channel === "call");
1100
- fileNamed = result.items.filter((it) => it.channel === "file" && !it.named_miss);
1101
- fileUnnamed = result.items.filter((it) => it.channel === "file" && it.named_miss);
1102
- console.log(`target: ${result.target}`);
1103
- console.log(`impact: ${result.items.length} nodes (direct=${byLevel.direct} indirect=${byLevel.indirect} tests=${byLevel.tests})${result.truncated ? " [TRUNCATED — wide impact, run the full suite]" : ""}`);
1104
- console.log(` ├─ call-graph reachable (high confidence): ${callItems.length}`);
1105
- console.log(` ├─ reachable via named import: ${fileNamed.length}`);
1106
- console.log(` └─ reachable via unnamed import (execution closure, conservative — do not skip): ${fileUnnamed.length}`);
1107
- if (result.blind_spot_count > 0)
1108
- console.log(`blind spots in target file: ${result.blind_spot_count} (impact may be underestimated)`);
1109
- if (result.co_change_hints.length > 0) {
1110
- console.log(`co-change hints (no static edge, but historically changed together):`);
1111
- for (const h of result.co_change_hints)
1112
- console.log(` ~ ${h.file} (${h.co_commits} co-commits, ${h.evidence})`);
1113
- }
1114
- console.log(`query: ${ms}ms
1134
+ process.stdout.write(JSON.stringify(result) + `
1115
1135
  `);
1116
- ordered = [...callItems, ...fileNamed, ...fileUnnamed];
1117
- for (const it of ordered.slice(0, 40)) {
1118
- const conf = it.confidence === "conservative" ? " ~" : "";
1119
- const ch = it.channel === "file" ? it.named_miss ? " ·closure" : " ·import" : "";
1120
- console.log(` [${it.level}${conf}${ch}] ${it.id} (${it.kind}, ${it.hops} hop, via ${it.via_file}:${it.via_line})`);
1136
+ db2.close();
1137
+ process.exitCode = 0;
1138
+ } else {
1139
+ printHuman();
1121
1140
  }
1122
- if (ordered.length > 40)
1123
- console.log(` ... and ${ordered.length - 40} more`);
1124
1141
  });
1125
1142
 
1126
1143
  // src/graph-diff.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeblast",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Know what breaks before you merge — mutation-tested code graph with architecture, change & impact maps. Evidence on every edge. For humans and AI agents.",
5
5
  "keywords": [
6
6
  "impact-analysis",