skalpel 4.0.45 → 4.0.46

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/eval-harness.mjs CHANGED
@@ -26,6 +26,11 @@
26
26
  import fs from "node:fs";
27
27
  import path from "node:path";
28
28
  import { fileURLToPath } from "node:url";
29
+ // SHIP-STATUS v1 corpus (Defect 6 — the publish gate): drive the REAL ship logic (detection →
30
+ // strict-kind reconstruction → confirm-gated probe) over claim/transcript pairs with ground truth,
31
+ // via an INJECTED exec so every branch runs hermetically (no network, no gh/curl/git/npm needed).
32
+ import { detectShipClaim, resolveShipStatus } from "./verify-shadow.mjs";
33
+ import { reconstructShipTargetV1, shipProbeFamily, SHIP_KIND_PROBE } from "./anchor-shadow.mjs";
29
34
 
30
35
  // ───────────────────────────── tunables (UNCALIBRATED defaults) ─────────────────────────────
31
36
  // These thresholds are NOT tuned against a human gold set unless one is supplied via --gold.
@@ -1021,6 +1026,394 @@ export function runSelfTests() {
1021
1026
  return true;
1022
1027
  }
1023
1028
 
1029
+ // ═════════════════════════ SHIP-STATUS v1 fixture corpus (Defect 6 — the PUBLISH GATE) ═════════════════════════
1030
+ // Claim/transcript pairs with GROUND TRUTH exercised end-to-end against the real ship logic. Includes ALL
1031
+ // THREE false-accusation regression scenarios (a,b,c → NO red) AND the true catches (d,e,f → red), plus
1032
+ // coverage-tail decisive probes (npm / git-tag / gh-checks). The probe layer is driven by an INJECTED exec
1033
+ // so no gh/curl/git/npm binary or network is touched — the fixtures pin the exact (claim → probe → verdict
1034
+ // → red?) contract. A ship-red that fires on a TRUE claim is the #1 death vector; this corpus is the gate.
1035
+ const SHIP_HERE = path.dirname(fileURLToPath(import.meta.url)); // a real dir w/ package.json (name=skalpel)
1036
+
1037
+ // exec-result shape mirrors anchor-shadow's `run`: { err, stdout, stderr }. err.code carries the exit code.
1038
+ const _R = (stdout = "", err = null, stderr = "") => ({
1039
+ err,
1040
+ stdout: String(stdout),
1041
+ stderr: String(stderr),
1042
+ });
1043
+ // A URL curl exec that walks a scripted sequence of responses (http code number, or {transport:<curl-exit>}).
1044
+ function curlSeq(seq) {
1045
+ let i = 0;
1046
+ return () => {
1047
+ const c = seq[Math.min(i, seq.length - 1)];
1048
+ i++;
1049
+ if (c && typeof c === "object" && c.transport != null)
1050
+ return Promise.resolve(_R("", { code: c.transport }));
1051
+ return Promise.resolve(_R(String(c)));
1052
+ };
1053
+ }
1054
+ // A git exec for the pushed REMOTE-TRUTH probe: ls-remote → <tip>, merge-base --is-ancestor → ancestor?
1055
+ function gitRemoteExec({ tip, ancestor, branch = "feat-x" }) {
1056
+ return (bin, args) => {
1057
+ const s = (args || []).join(" ");
1058
+ if (bin === "git" && s.includes("ls-remote") && !s.includes("--tags"))
1059
+ return Promise.resolve(_R(`${tip}\trefs/heads/${branch}\n`));
1060
+ if (bin === "git" && s.includes("merge-base") && s.includes("--is-ancestor"))
1061
+ return Promise.resolve(ancestor ? _R("") : _R("", { code: 1 }));
1062
+ return Promise.resolve(_R("", { code: 128 })); // any other git call → indeterminate
1063
+ };
1064
+ }
1065
+ const ghPrExec =
1066
+ (state, mergedAt = null) =>
1067
+ () =>
1068
+ Promise.resolve(_R(JSON.stringify({ state, mergedAt })));
1069
+ const ghChecksExec = (arr) => () => Promise.resolve(_R(JSON.stringify(arr)));
1070
+ const npmExec = (published, version) => () =>
1071
+ published
1072
+ ? Promise.resolve(_R(version))
1073
+ : Promise.resolve(_R("", { code: 1 }, "npm error code E404\nnpm error 404 Not Found"));
1074
+ const gitTagExec = (present, tag) => (bin, args) => {
1075
+ const s = (args || []).join(" ");
1076
+ if (bin === "git" && s.includes("ls-remote") && s.includes("--tags"))
1077
+ return Promise.resolve(
1078
+ present ? _R(`deadbeefdeadbeefdeadbeefdeadbeefdeadbeef\trefs/tags/${tag}\n`) : _R(""),
1079
+ );
1080
+ return Promise.resolve(_R("", { code: 128 }));
1081
+ };
1082
+
1083
+ const SHA_A = "3fa1c2b9e4d5a6f7089badc0ffee1234deadbeef"; // 40-hex claimed commit
1084
+ const SHA_OLD = "0000111122223333444455556666777788889999"; // a DIFFERENT remote tip (push never landed)
1085
+
1086
+ // Each fixture: the assistant claim text, the reconstruction context, the injected exec, and ground truth.
1087
+ const SHIP_FIXTURES = [
1088
+ // (a) REGRESSION: pushed-claim + an OPEN PR in context → NO red. The strict map must pick the git-remote
1089
+ // probe (NEVER the PR probe), and with the sha reachable on the real remote tip → PASS.
1090
+ {
1091
+ id: "a",
1092
+ label: "pushed + open-PR-in-context (cross-kind guard)",
1093
+ kind: "pushed",
1094
+ claim: "Pushed the commit to origin.",
1095
+ context: `opened PR #99 (state: OPEN)\ncommit ${SHA_A}\n$ git push origin feat-x`,
1096
+ exec: gitRemoteExec({ tip: SHA_A, ancestor: true }),
1097
+ expectOutcome: "SHIP_OK",
1098
+ expectRed: false,
1099
+ },
1100
+ // (b) REGRESSION: live-claim + 401 → NO red. Auth-walled is NOT "not live" → UNKNOWN.
1101
+ {
1102
+ id: "b",
1103
+ label: "live + 401 auth-wall → UNKNOWN",
1104
+ kind: "live",
1105
+ claim: "It's live at https://app.example.com/health.",
1106
+ context: "deployed to https://app.example.com/health",
1107
+ exec: curlSeq([401]),
1108
+ expectOutcome: "SHIP_UNKNOWN",
1109
+ expectRed: false,
1110
+ },
1111
+ // (c) REGRESSION: live-claim + transient 404 then 200 → NO red. A candidate 404 confirm-reprobes; a 200
1112
+ // on the re-probe means it was CDN/propagation lag → SHIP_OK.
1113
+ {
1114
+ id: "c",
1115
+ label: "live + transient 404-then-200 → OK",
1116
+ kind: "live",
1117
+ claim: "The site is up now: https://app.example.com.",
1118
+ context: "https://app.example.com",
1119
+ exec: curlSeq([404, 200]),
1120
+ expectOutcome: "SHIP_OK",
1121
+ expectRed: false,
1122
+ expectReprobe: true,
1123
+ },
1124
+ // (d) TRUE CATCH: pushed-claim + sha NOT on the real remote tip → red (it never left the machine).
1125
+ {
1126
+ id: "d",
1127
+ label: "pushed + sha-not-on-remote → RED",
1128
+ kind: "pushed",
1129
+ claim: "Pushed it to origin, all set.",
1130
+ context: `commit ${SHA_A}\n$ git push origin feat-x`,
1131
+ exec: gitRemoteExec({ tip: SHA_OLD, ancestor: false }),
1132
+ expectOutcome: "SHIP_FAIL",
1133
+ expectRed: true,
1134
+ },
1135
+ // (e) TRUE CATCH: merged-claim + PR is OPEN → red.
1136
+ {
1137
+ id: "e",
1138
+ label: "merged + PR OPEN → RED",
1139
+ kind: "merged",
1140
+ claim: "Merged the PR to main.",
1141
+ context: "merged PR #4242 into main",
1142
+ exec: ghPrExec("OPEN", null),
1143
+ expectOutcome: "SHIP_FAIL",
1144
+ expectRed: true,
1145
+ },
1146
+ // (f) TRUE CATCH: live-claim + confirmed 404 × 2 → red (only the double-negative fires).
1147
+ {
1148
+ id: "f",
1149
+ label: "live + confirmed 404×2 → RED",
1150
+ kind: "live",
1151
+ claim: "It's live at https://gone.example.com.",
1152
+ context: "https://gone.example.com",
1153
+ exec: curlSeq([404, 404]),
1154
+ expectOutcome: "SHIP_FAIL",
1155
+ expectRed: true,
1156
+ expectReprobe: true,
1157
+ },
1158
+ // ── coverage tail (decisive verbs only) ────────────────────────────────────────────────────────────
1159
+ // (g) merged + PR MERGED → NO red.
1160
+ {
1161
+ id: "g",
1162
+ label: "merged + PR MERGED → OK",
1163
+ kind: "merged",
1164
+ claim: "It's merged.",
1165
+ context: "merged PR #4242",
1166
+ exec: ghPrExec("MERGED", "2026-07-13T00:00:00Z"),
1167
+ expectOutcome: "SHIP_OK",
1168
+ expectRed: false,
1169
+ },
1170
+ // (h) live + 200 → NO red.
1171
+ {
1172
+ id: "h",
1173
+ label: "live + 200 → OK",
1174
+ kind: "live",
1175
+ claim: "It's live now: https://ok.example.com.",
1176
+ context: "https://ok.example.com",
1177
+ exec: curlSeq([200]),
1178
+ expectOutcome: "SHIP_OK",
1179
+ expectRed: false,
1180
+ },
1181
+ // (i) live + 403 → UNKNOWN (auth-wall).
1182
+ {
1183
+ id: "i",
1184
+ label: "live + 403 → UNKNOWN",
1185
+ kind: "live",
1186
+ claim: "It's live at https://secure.example.com.",
1187
+ context: "https://secure.example.com",
1188
+ exec: curlSeq([403]),
1189
+ expectOutcome: "SHIP_UNKNOWN",
1190
+ expectRed: false,
1191
+ },
1192
+ // (j) live + 500 → UNKNOWN (transient 5xx, v1).
1193
+ {
1194
+ id: "j",
1195
+ label: "live + 500 → UNKNOWN",
1196
+ kind: "live",
1197
+ claim: "It's live: https://err.example.com.",
1198
+ context: "https://err.example.com",
1199
+ exec: curlSeq([500]),
1200
+ expectOutcome: "SHIP_UNKNOWN",
1201
+ expectRed: false,
1202
+ },
1203
+ // (k) live + confirmed NXDOMAIN × 2 → red (connection-level fail candidate, confirmed).
1204
+ {
1205
+ id: "k",
1206
+ label: "live + NXDOMAIN×2 → RED",
1207
+ kind: "live",
1208
+ claim: "It's live at https://nope.example.com.",
1209
+ context: "https://nope.example.com",
1210
+ exec: curlSeq([{ transport: 6 }, { transport: 6 }]),
1211
+ expectOutcome: "SHIP_FAIL",
1212
+ expectRed: true,
1213
+ expectReprobe: true,
1214
+ },
1215
+ // (l) published + NOT on the registry → red (npm view E404).
1216
+ {
1217
+ id: "l",
1218
+ label: "published + npm E404 → RED",
1219
+ kind: "published",
1220
+ claim: "Published skalpel@9.9.9 to npm.",
1221
+ context: "$ npm publish\nskalpel@9.9.9",
1222
+ exec: npmExec(false, "9.9.9"),
1223
+ expectOutcome: "SHIP_FAIL",
1224
+ expectRed: true,
1225
+ },
1226
+ // (m) published + IS on the registry → NO red.
1227
+ {
1228
+ id: "m",
1229
+ label: "published + npm present → OK",
1230
+ kind: "published",
1231
+ claim: "Published skalpel@4.0.9 to npm.",
1232
+ context: "$ npm publish\nskalpel@4.0.9",
1233
+ exec: npmExec(true, "4.0.9"),
1234
+ expectOutcome: "SHIP_OK",
1235
+ expectRed: false,
1236
+ },
1237
+ // (n) CI green claim + checks FAILING → red.
1238
+ {
1239
+ id: "n",
1240
+ label: "ci-green + checks failing → RED",
1241
+ kind: "ci",
1242
+ claim: "CI is green on PR #77.",
1243
+ context: "PR #77",
1244
+ exec: ghChecksExec([{ bucket: "pass" }, { bucket: "fail" }]),
1245
+ expectOutcome: "SHIP_FAIL",
1246
+ expectRed: true,
1247
+ },
1248
+ // (o) CI green claim + checks passing → NO red.
1249
+ {
1250
+ id: "o",
1251
+ label: "ci-green + checks passing → OK",
1252
+ kind: "ci",
1253
+ claim: "All checks are passing on PR #77.",
1254
+ context: "PR #77",
1255
+ exec: ghChecksExec([{ bucket: "pass" }, { bucket: "pass" }]),
1256
+ expectOutcome: "SHIP_OK",
1257
+ expectRed: false,
1258
+ },
1259
+ // (p) released (git tag) + tag NOT on origin → red.
1260
+ {
1261
+ id: "p",
1262
+ label: "released tag + missing on origin → RED",
1263
+ kind: "released",
1264
+ claim: "Released v2.0.0.",
1265
+ context: "$ git tag v2.0.0\n$ git push origin v2.0.0",
1266
+ exec: gitTagExec(false, "v2.0.0"),
1267
+ expectOutcome: "SHIP_FAIL",
1268
+ expectRed: true,
1269
+ },
1270
+ // (q) released (git tag) + tag present on origin → NO red.
1271
+ {
1272
+ id: "q",
1273
+ label: "released tag + present on origin → OK",
1274
+ kind: "released",
1275
+ claim: "Released v2.0.0.",
1276
+ context: "$ git tag v2.0.0\n$ git push origin v2.0.0",
1277
+ exec: gitTagExec(true, "v2.0.0"),
1278
+ expectOutcome: "SHIP_OK",
1279
+ expectRed: false,
1280
+ },
1281
+ // (r) UNKNOWN never a red: pushed-claim but NO sha in context → NO_TARGET (honest silence, no cross-kind).
1282
+ {
1283
+ id: "r",
1284
+ label: "pushed + no sha (only a PR) → NO_TARGET",
1285
+ kind: "pushed",
1286
+ claim: "Pushed it.",
1287
+ context: "opened PR #99 (OPEN)",
1288
+ exec: () => Promise.resolve(_R("", { code: 128 })),
1289
+ expectOutcome: "NO_TARGET",
1290
+ expectRed: false,
1291
+ },
1292
+ ];
1293
+
1294
+ // runShipSelfTests() — drives the corpus + the 1:1 kind↔probe map assertion. Returns true; throws on any
1295
+ // failure (a failing fixture MUST block the PR). Prints the (claim → probe → verdict → red?) table.
1296
+ export async function runShipSelfTests() {
1297
+ const assert = (cond, msg) => {
1298
+ if (!cond) throw new Error("SHIP-SELFTEST FAIL: " + msg);
1299
+ };
1300
+ console.log("\n" + "═".repeat(96));
1301
+ console.log(
1302
+ "SHIP-STATUS v1 FIXTURE CORPUS — the publish gate (all 6 regression cases + coverage tail)",
1303
+ );
1304
+ console.log("═".repeat(96));
1305
+
1306
+ // ── PART 1: strict kind↔probe map is 1:1 (ZERO cross-kind selections) ──────────────────────────────
1307
+ // A mega-context that simultaneously offers EVERY target type. Each ship kind must reconstruct ONLY its
1308
+ // own probe family — a `pushed` claim must NEVER select the PR probe, etc.
1309
+ const mega = [
1310
+ "opened PR #123 (state OPEN)",
1311
+ "deployed to https://app.example.com/health",
1312
+ `commit ${SHA_A}`,
1313
+ "$ git push origin feat-x",
1314
+ "$ git tag v3.1.4",
1315
+ "$ npm publish",
1316
+ "skalpel@3.1.4",
1317
+ ].join("\n");
1318
+ const payloadMega = { cwd: SHIP_HERE, prompt: mega };
1319
+ for (const kind of Object.keys(SHIP_KIND_PROBE)) {
1320
+ const t = reconstructShipTargetV1(kind, mega, payloadMega);
1321
+ assert(t, `1:1 map — ${kind} reconstructs a target from the mega-context`);
1322
+ const fam = shipProbeFamily(t.kind);
1323
+ assert(
1324
+ fam === SHIP_KIND_PROBE[kind],
1325
+ `1:1 map — ${kind} → ${fam} but expected ${SHIP_KIND_PROBE[kind]} (target.kind=${t.kind})`,
1326
+ );
1327
+ }
1328
+ // Explicit anti-cross-kind: pushed never becomes a PR; merged never a URL/git; live never a PR.
1329
+ assert(
1330
+ reconstructShipTargetV1("pushed", mega, payloadMega).kind === "git-remote",
1331
+ "pushed → git-remote only",
1332
+ );
1333
+ assert(reconstructShipTargetV1("merged", mega, payloadMega).kind === "pr", "merged → pr only");
1334
+ assert(
1335
+ ["url"].includes(reconstructShipTargetV1("live", mega, payloadMega).kind),
1336
+ "live → url only",
1337
+ );
1338
+ // No cross-kind FALLBACK when the own-kind target is absent → NO_TARGET (null), never a wrong-kind probe.
1339
+ assert(
1340
+ reconstructShipTargetV1("pushed", "opened PR #99 (OPEN)", { cwd: SHIP_HERE }) === null,
1341
+ "pushed with only a PR (no sha) → null (NOT a PR probe)",
1342
+ );
1343
+ assert(
1344
+ reconstructShipTargetV1("merged", "deployed to https://x.example.com", { cwd: SHIP_HERE }) ===
1345
+ null,
1346
+ "merged with only a URL (no PR) → null (NOT a URL probe)",
1347
+ );
1348
+ assert(
1349
+ reconstructShipTargetV1("live", "merged PR #5 into main", { cwd: SHIP_HERE }) === null,
1350
+ "live with only a PR (no URL) → null (NOT a PR probe)",
1351
+ );
1352
+ console.log(
1353
+ ` ✓ kind↔probe map is 1:1 across ${Object.keys(SHIP_KIND_PROBE).length} kinds — ZERO cross-kind selections`,
1354
+ );
1355
+ console.log("");
1356
+
1357
+ // ── PART 2: the fixture corpus (claim → probe → verdict → red?) ────────────────────────────────────
1358
+ const rows = [];
1359
+ let pass = 0;
1360
+ for (const f of SHIP_FIXTURES) {
1361
+ const det = detectShipClaim(f.claim);
1362
+ assert(det.claim, `[${f.id}] detects a ship claim in: ${f.claim}`);
1363
+ assert(det.kind === f.kind, `[${f.id}] claim kind ${det.kind} !== expected ${f.kind}`);
1364
+ const res = await resolveShipStatus({
1365
+ kind: det.kind,
1366
+ contextText: f.context,
1367
+ payload: { cwd: SHIP_HERE, prompt: f.claim },
1368
+ exec: f.exec,
1369
+ confirmDelayMs: 0, // never actually sleep 60s in the eval
1370
+ });
1371
+ const red = res.mismatch === true; // a red fires iff the logic decides SHIP_FAIL (armed gates aside)
1372
+ assert(
1373
+ res.outcome === f.expectOutcome,
1374
+ `[${f.id}] outcome ${res.outcome} !== expected ${f.expectOutcome} (${f.label})`,
1375
+ );
1376
+ assert(red === f.expectRed, `[${f.id}] red=${red} !== expected ${f.expectRed} (${f.label})`);
1377
+ if ("expectReprobe" in f)
1378
+ assert(
1379
+ res.confirm_reprobe === f.expectReprobe,
1380
+ `[${f.id}] confirm_reprobe=${res.confirm_reprobe} !== expected ${f.expectReprobe}`,
1381
+ );
1382
+ pass++;
1383
+ rows.push({
1384
+ id: f.id,
1385
+ probe: res.probe_kind || SHIP_KIND_PROBE[det.kind] || "-",
1386
+ verdict: res.outcome,
1387
+ red: red ? "RED" : "—",
1388
+ reprobe: res.confirm_reprobe ? "2/2" : "",
1389
+ label: f.label,
1390
+ });
1391
+ }
1392
+
1393
+ // pretty table
1394
+ const pad = (s, n) => String(s).padEnd(n);
1395
+ console.log(
1396
+ ` ${pad("#", 3)}${pad("kind", 11)}${pad("probe", 12)}${pad("verdict", 14)}${pad("red?", 6)}${pad("reprobe", 9)}case`,
1397
+ );
1398
+ console.log(" " + "─".repeat(92));
1399
+ for (let i = 0; i < SHIP_FIXTURES.length; i++) {
1400
+ const f = SHIP_FIXTURES[i];
1401
+ const r = rows[i];
1402
+ console.log(
1403
+ ` ${pad(r.id, 3)}${pad(f.kind, 11)}${pad(r.probe, 12)}${pad(r.verdict, 14)}${pad(r.red, 6)}${pad(r.reprobe, 9)}${r.label}`,
1404
+ );
1405
+ }
1406
+ console.log(" " + "─".repeat(92));
1407
+ console.log(
1408
+ `\n SHIP CORPUS: ${pass}/${SHIP_FIXTURES.length} fixtures pass (incl. regression a,b,c → NO red; catches d,e,f → RED).`,
1409
+ );
1410
+ if (pass !== SHIP_FIXTURES.length) throw new Error("SHIP-SELFTEST FAIL: not all fixtures passed");
1411
+ console.log(
1412
+ " ALL SHIP FIXTURES PASSED — strict kind↔probe, remote-truth, HTTP precision + confirm-gate verified.",
1413
+ );
1414
+ return true;
1415
+ }
1416
+
1024
1417
  // ───────────────────────────── CLI ─────────────────────────
1025
1418
  function fmt(obj) {
1026
1419
  return JSON.stringify(obj, null, 2);
@@ -1134,6 +1527,33 @@ const isMain = (() => {
1134
1527
  return false;
1135
1528
  }
1136
1529
  })();
1137
- if (isMain) main();
1530
+ if (isMain) {
1531
+ const a0 = process.argv[2];
1532
+ if (a0 === "--ship-selftest") {
1533
+ // The SHIP-STATUS v1 publish gate (Defect 6) — async, so it gets its own entry.
1534
+ runShipSelfTests()
1535
+ .then((ok) => process.exit(ok ? 0 : 1))
1536
+ .catch((e) => {
1537
+ console.error(String((e && e.stack) || e));
1538
+ process.exit(1);
1539
+ });
1540
+ } else if (a0 === "--selftest") {
1541
+ // `--selftest` now covers BOTH the intervention machinery AND the ship-status v1 corpus.
1542
+ try {
1543
+ runSelfTests();
1544
+ } catch (e) {
1545
+ console.error(String((e && e.stack) || e));
1546
+ process.exit(1);
1547
+ }
1548
+ runShipSelfTests()
1549
+ .then((ok) => process.exit(ok ? 0 : 1))
1550
+ .catch((e) => {
1551
+ console.error(String((e && e.stack) || e));
1552
+ process.exit(1);
1553
+ });
1554
+ } else {
1555
+ main();
1556
+ }
1557
+ }
1138
1558
 
1139
1559
  export { V1_onset, V2_cue, V3_repeat, FIRE_ALWAYS, FIRE_NEVER, calibrate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skalpel",
3
- "version": "4.0.45",
3
+ "version": "4.0.46",
4
4
  "description": "Behavioral graph for AI-assisted coding — learns how you work and steers Claude Code + Codex in real time.",
5
5
  "type": "module",
6
6
  "bin": {