martin-loop 0.1.4 → 1.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/CODE_OF_CONDUCT.md +32 -0
- package/README.md +172 -227
- package/demo/seeded-workspace/README.md +35 -0
- package/demo/seeded-workspace/TASKS.md +29 -0
- package/demo/seeded-workspace/martin.config.yaml +11 -0
- package/demo/seeded-workspace/package.json +8 -0
- package/demo/seeded-workspace/src/invoice-summary.js +11 -0
- package/demo/seeded-workspace/test/invoice-summary.test.js +20 -0
- package/dist/bin/martin-loop.js +0 -0
- package/dist/vendor/adapters/claude-cli.d.ts +19 -4
- package/dist/vendor/adapters/claude-cli.js +55 -24
- package/dist/vendor/adapters/cli-bridge.d.ts +1 -0
- package/dist/vendor/adapters/cli-bridge.js +154 -28
- package/dist/vendor/adapters/counter.d.ts +1 -0
- package/dist/vendor/adapters/counter.js +4 -0
- package/dist/vendor/adapters/git-baseline.d.ts +50 -0
- package/dist/vendor/adapters/git-baseline.js +233 -0
- package/dist/vendor/adapters/index.d.ts +1 -0
- package/dist/vendor/adapters/index.js +1 -0
- package/dist/vendor/adapters/openrouter-adapter.d.ts +15 -0
- package/dist/vendor/adapters/openrouter-adapter.js +302 -0
- package/dist/vendor/adapters/usage.d.ts +48 -0
- package/dist/vendor/adapters/usage.js +66 -0
- package/dist/vendor/adapters/verifier-only.d.ts +7 -0
- package/dist/vendor/adapters/verifier-only.js +57 -0
- package/dist/vendor/cli/bin/exit.d.ts +12 -0
- package/dist/vendor/cli/bin/exit.js +28 -0
- package/dist/vendor/cli/commands/analyze.d.ts +5 -0
- package/dist/vendor/cli/commands/analyze.js +58 -0
- package/dist/vendor/cli/commands/audit-log-verify.d.ts +34 -0
- package/dist/vendor/cli/commands/audit-log-verify.js +99 -0
- package/dist/vendor/cli/commands/audit.d.ts +8 -0
- package/dist/vendor/cli/commands/audit.js +199 -0
- package/dist/vendor/cli/commands/corpus.d.ts +5 -0
- package/dist/vendor/cli/commands/corpus.js +60 -0
- package/dist/vendor/cli/commands/doctor.d.ts +8 -0
- package/dist/vendor/cli/commands/doctor.js +219 -0
- package/dist/vendor/cli/commands/explain.d.ts +17 -0
- package/dist/vendor/cli/commands/explain.js +176 -0
- package/dist/vendor/cli/commands/export.d.ts +5 -0
- package/dist/vendor/cli/commands/export.js +60 -0
- package/dist/vendor/cli/commands/governance.d.ts +8 -0
- package/dist/vendor/cli/commands/governance.js +95 -0
- package/dist/vendor/cli/commands/improve.d.ts +18 -0
- package/dist/vendor/cli/commands/improve.js +396 -0
- package/dist/vendor/cli/commands/init.d.ts +8 -0
- package/dist/vendor/cli/commands/init.js +281 -0
- package/dist/vendor/cli/commands/migration.d.ts +8 -0
- package/dist/vendor/cli/commands/migration.js +67 -0
- package/dist/vendor/cli/commands/prior.d.ts +23 -0
- package/dist/vendor/cli/commands/prior.js +145 -0
- package/dist/vendor/cli/commands/resume.d.ts +21 -0
- package/dist/vendor/cli/commands/resume.js +73 -0
- package/dist/vendor/cli/commands/verify.d.ts +6 -0
- package/dist/vendor/cli/commands/verify.js +43 -0
- package/dist/vendor/cli/index.d.ts +6 -1
- package/dist/vendor/cli/index.js +124 -7
- package/dist/vendor/cli/research/public-corpus.d.ts +43 -0
- package/dist/vendor/cli/research/public-corpus.js +151 -0
- package/dist/vendor/cli/ui/error-card.d.ts +38 -0
- package/dist/vendor/cli/ui/error-card.js +103 -0
- package/dist/vendor/cli/ui/mission-brief.d.ts +41 -0
- package/dist/vendor/cli/ui/mission-brief.js +173 -0
- package/dist/vendor/cli/ui/summary-card.d.ts +34 -0
- package/dist/vendor/cli/ui/summary-card.js +102 -0
- package/dist/vendor/contracts/audit.d.ts +46 -0
- package/dist/vendor/contracts/audit.js +360 -0
- package/dist/vendor/contracts/index.d.ts +3 -1
- package/dist/vendor/contracts/post-phase15.d.ts +240 -0
- package/dist/vendor/contracts/post-phase15.js +166 -0
- package/dist/vendor/core/agent/mandates.d.ts +46 -0
- package/dist/vendor/core/agent/mandates.js +178 -0
- package/dist/vendor/core/agent/receipts.d.ts +38 -0
- package/dist/vendor/core/agent/receipts.js +131 -0
- package/dist/vendor/core/agent/signing.d.ts +17 -0
- package/dist/vendor/core/agent/signing.js +91 -0
- package/dist/vendor/core/attestation/sign.d.ts +25 -0
- package/dist/vendor/core/attestation/sign.js +216 -0
- package/dist/vendor/core/autonomy/autonomous-promotion.d.ts +120 -0
- package/dist/vendor/core/autonomy/autonomous-promotion.js +346 -0
- package/dist/vendor/core/autonomy/envelope-v2.d.ts +29 -0
- package/dist/vendor/core/autonomy/envelope-v2.js +60 -0
- package/dist/vendor/core/autonomy/envelope.d.ts +17 -0
- package/dist/vendor/core/autonomy/envelope.js +27 -0
- package/dist/vendor/core/autonomy/escalation-ledger.d.ts +20 -0
- package/dist/vendor/core/autonomy/escalation-ledger.js +18 -0
- package/dist/vendor/core/autonomy/resume.d.ts +15 -0
- package/dist/vendor/core/autonomy/resume.js +23 -0
- package/dist/vendor/core/circuit/circuit-breaker.d.ts +60 -0
- package/dist/vendor/core/circuit/circuit-breaker.js +143 -0
- package/dist/vendor/core/compiler.d.ts +2 -0
- package/dist/vendor/core/compiler.js +10 -4
- package/dist/vendor/core/context-distillation.d.ts +3 -0
- package/dist/vendor/core/context-distillation.js +44 -0
- package/dist/vendor/core/context-flow/compile-context.d.ts +8 -0
- package/dist/vendor/core/context-flow/compile-context.js +111 -0
- package/dist/vendor/core/context-flow/entities.d.ts +2 -0
- package/dist/vendor/core/context-flow/entities.js +44 -0
- package/dist/vendor/core/context-flow/evaluate-policy.d.ts +2 -0
- package/dist/vendor/core/context-flow/evaluate-policy.js +42 -0
- package/dist/vendor/core/context-flow/index.d.ts +11 -0
- package/dist/vendor/core/context-flow/index.js +24 -0
- package/dist/vendor/core/context-flow/labels.d.ts +3 -0
- package/dist/vendor/core/context-flow/labels.js +17 -0
- package/dist/vendor/core/context-flow/normalizer.d.ts +9 -0
- package/dist/vendor/core/context-flow/normalizer.js +69 -0
- package/dist/vendor/core/context-flow/profiles.d.ts +33 -0
- package/dist/vendor/core/context-flow/profiles.js +36 -0
- package/dist/vendor/core/context-flow/redaction.d.ts +1 -0
- package/dist/vendor/core/context-flow/redaction.js +6 -0
- package/dist/vendor/core/context-flow/sensitivity.d.ts +2 -0
- package/dist/vendor/core/context-flow/sensitivity.js +27 -0
- package/dist/vendor/core/context-flow/sync-preview.d.ts +2 -0
- package/dist/vendor/core/context-flow/sync-preview.js +22 -0
- package/dist/vendor/core/context-flow/token-estimator.d.ts +3 -0
- package/dist/vendor/core/context-flow/token-estimator.js +13 -0
- package/dist/vendor/core/context-flow/types.d.ts +91 -0
- package/dist/vendor/core/context-flow/types.js +2 -0
- package/dist/vendor/core/context-integrity.d.ts +26 -0
- package/dist/vendor/core/context-integrity.js +56 -0
- package/dist/vendor/core/context-utility.d.ts +47 -0
- package/dist/vendor/core/context-utility.js +405 -0
- package/dist/vendor/core/cost/pipeline.d.ts +92 -0
- package/dist/vendor/core/cost/pipeline.js +141 -0
- package/dist/vendor/core/cost/tagged-cost.d.ts +27 -0
- package/dist/vendor/core/cost/tagged-cost.js +55 -0
- package/dist/vendor/core/cost-governor.d.ts +2 -0
- package/dist/vendor/core/cost-governor.js +50 -0
- package/dist/vendor/core/cve/cve-check.d.ts +80 -0
- package/dist/vendor/core/cve/cve-check.js +172 -0
- package/dist/vendor/core/digital-twin/index.d.ts +27 -0
- package/dist/vendor/core/digital-twin/index.js +90 -0
- package/dist/vendor/core/drift/drift-graph.d.ts +47 -0
- package/dist/vendor/core/drift/drift-graph.js +100 -0
- package/dist/vendor/core/drift/objective-lock.d.ts +69 -0
- package/dist/vendor/core/drift/objective-lock.js +88 -0
- package/dist/vendor/core/drift/scope.d.ts +46 -0
- package/dist/vendor/core/drift/scope.js +102 -0
- package/dist/vendor/core/drift/signature-lock.d.ts +48 -0
- package/dist/vendor/core/drift/signature-lock.js +202 -0
- package/dist/vendor/core/drift/stale-proof-gate.d.ts +21 -0
- package/dist/vendor/core/drift/stale-proof-gate.js +19 -0
- package/dist/vendor/core/eval/known-bad-world-runner.d.ts +24 -0
- package/dist/vendor/core/eval/known-bad-world-runner.js +256 -0
- package/dist/vendor/core/evidence/claim-audit.d.ts +18 -0
- package/dist/vendor/core/evidence/claim-audit.js +89 -0
- package/dist/vendor/core/exit-intelligence.d.ts +2 -0
- package/dist/vendor/core/exit-intelligence.js +58 -0
- package/dist/vendor/core/explain/formatter.d.ts +42 -0
- package/dist/vendor/core/explain/formatter.js +171 -0
- package/dist/vendor/core/explain/timeline.d.ts +29 -0
- package/dist/vendor/core/explain/timeline.js +213 -0
- package/dist/vendor/core/failure-taxonomy.d.ts +2 -0
- package/dist/vendor/core/failure-taxonomy.js +76 -0
- package/dist/vendor/core/gateway/index.d.ts +10 -0
- package/dist/vendor/core/gateway/index.js +12 -0
- package/dist/vendor/core/gateway/registry.d.ts +40 -0
- package/dist/vendor/core/gateway/registry.js +97 -0
- package/dist/vendor/core/gateway/transport.d.ts +31 -0
- package/dist/vendor/core/gateway/transport.js +82 -0
- package/dist/vendor/core/gateway/vault.d.ts +19 -0
- package/dist/vendor/core/gateway/vault.js +29 -0
- package/dist/vendor/core/graph/adapters.d.ts +43 -0
- package/dist/vendor/core/graph/adapters.js +91 -0
- package/dist/vendor/core/graph/hotspots.d.ts +22 -0
- package/dist/vendor/core/graph/hotspots.js +30 -0
- package/dist/vendor/core/graph/index.d.ts +1 -0
- package/dist/vendor/core/graph/index.js +2 -0
- package/dist/vendor/core/honey/honey-tokens.d.ts +32 -0
- package/dist/vendor/core/honey/honey-tokens.js +44 -0
- package/dist/vendor/core/index.d.ts +7 -4
- package/dist/vendor/core/index.js +222 -64
- package/dist/vendor/core/learning/bayesian-update.d.ts +31 -0
- package/dist/vendor/core/learning/bayesian-update.js +60 -0
- package/dist/vendor/core/learning/prior-sets.d.ts +42 -0
- package/dist/vendor/core/learning/prior-sets.js +111 -0
- package/dist/vendor/core/learning/promotion-gate.d.ts +17 -0
- package/dist/vendor/core/learning/promotion-gate.js +23 -0
- package/dist/vendor/core/leash/blast-radius.d.ts +42 -0
- package/dist/vendor/core/leash/blast-radius.js +156 -0
- package/dist/vendor/core/leash/policy-leash.d.ts +31 -0
- package/dist/vendor/core/leash/policy-leash.js +117 -0
- package/dist/vendor/core/memo/memo.d.ts +63 -0
- package/dist/vendor/core/memo/memo.js +97 -0
- package/dist/vendor/core/memory/learning-pipeline.d.ts +154 -0
- package/dist/vendor/core/memory/learning-pipeline.js +391 -0
- package/dist/vendor/core/memory/palace.d.ts +84 -0
- package/dist/vendor/core/memory/palace.js +379 -0
- package/dist/vendor/core/merge/ast-merge.d.ts +22 -0
- package/dist/vendor/core/merge/ast-merge.js +350 -0
- package/dist/vendor/core/merge/text-merge.d.ts +12 -0
- package/dist/vendor/core/merge/text-merge.js +182 -0
- package/dist/vendor/core/otel/tracer.d.ts +45 -0
- package/dist/vendor/core/otel/tracer.js +116 -0
- package/dist/vendor/core/parallel/parallel-attempts.d.ts +28 -0
- package/dist/vendor/core/parallel/parallel-attempts.js +41 -0
- package/dist/vendor/core/parallel/scorer.d.ts +24 -0
- package/dist/vendor/core/parallel/scorer.js +65 -0
- package/dist/vendor/core/pattern-detection.d.ts +64 -0
- package/dist/vendor/core/pattern-detection.js +108 -0
- package/dist/vendor/core/persistence/checkpoint.d.ts +44 -0
- package/dist/vendor/core/persistence/checkpoint.js +156 -0
- package/dist/vendor/core/persistence/cleanup.d.ts +22 -0
- package/dist/vendor/core/persistence/cleanup.js +131 -0
- package/dist/vendor/core/persistence/index.d.ts +2 -0
- package/dist/vendor/core/persistence/index.js +1 -0
- package/dist/vendor/core/persistence/runs-reader.d.ts +52 -0
- package/dist/vendor/core/persistence/runs-reader.js +84 -0
- package/dist/vendor/core/persistence/store.d.ts +6 -1
- package/dist/vendor/core/persistence/store.js +5 -0
- package/dist/vendor/core/policy/file-touch-quota.d.ts +60 -0
- package/dist/vendor/core/policy/file-touch-quota.js +105 -0
- package/dist/vendor/core/policy/policy-loader.d.ts +30 -0
- package/dist/vendor/core/policy/policy-loader.js +170 -0
- package/dist/vendor/core/policy/policy-schema.d.ts +55 -0
- package/dist/vendor/core/policy/policy-schema.js +78 -0
- package/dist/vendor/core/policy.d.ts +6 -0
- package/dist/vendor/core/probe/probe.d.ts +49 -0
- package/dist/vendor/core/probe/probe.js +115 -0
- package/dist/vendor/core/proof/patch-proof.d.ts +58 -0
- package/dist/vendor/core/proof/patch-proof.js +84 -0
- package/dist/vendor/core/proof/semantic-probe.d.ts +25 -0
- package/dist/vendor/core/proof/semantic-probe.js +82 -0
- package/dist/vendor/core/recovery/failure-mode-runner.d.ts +29 -0
- package/dist/vendor/core/recovery/failure-mode-runner.js +39 -0
- package/dist/vendor/core/red-blue/red-phase.d.ts +64 -0
- package/dist/vendor/core/red-blue/red-phase.js +141 -0
- package/dist/vendor/core/red-blue/risk-tiers.d.ts +22 -0
- package/dist/vendor/core/red-blue/risk-tiers.js +33 -0
- package/dist/vendor/core/replay/replay.d.ts +85 -0
- package/dist/vendor/core/replay/replay.js +109 -0
- package/dist/vendor/core/router/engine.d.ts +54 -0
- package/dist/vendor/core/router/engine.js +131 -0
- package/dist/vendor/core/router/index.d.ts +1 -0
- package/dist/vendor/core/router/index.js +2 -0
- package/dist/vendor/core/router/trust-calibration.d.ts +57 -0
- package/dist/vendor/core/router/trust-calibration.js +127 -0
- package/dist/vendor/core/run-martin.d.ts +2 -0
- package/dist/vendor/core/run-martin.js +287 -0
- package/dist/vendor/core/security/cve-scanner.d.ts +62 -0
- package/dist/vendor/core/security/cve-scanner.js +178 -0
- package/dist/vendor/core/sentinel/efficiency-sentinel.d.ts +29 -0
- package/dist/vendor/core/sentinel/efficiency-sentinel.js +30 -0
- package/dist/vendor/core/sentinel/progress-guard.d.ts +35 -0
- package/dist/vendor/core/sentinel/progress-guard.js +46 -0
- package/dist/vendor/core/siem/siem-emitter.d.ts +49 -0
- package/dist/vendor/core/siem/siem-emitter.js +157 -0
- package/dist/vendor/core/strategy/attempt-brief.d.ts +22 -0
- package/dist/vendor/core/strategy/attempt-brief.js +89 -0
- package/dist/vendor/core/summarize/diff-summary.d.ts +35 -0
- package/dist/vendor/core/summarize/diff-summary.js +204 -0
- package/dist/vendor/core/surface-signals.d.ts +21 -0
- package/dist/vendor/core/surface-signals.js +139 -0
- package/dist/vendor/core/truth/truth-wall.d.ts +51 -0
- package/dist/vendor/core/truth/truth-wall.js +69 -0
- package/dist/vendor/core/truth-spine.d.ts +26 -0
- package/dist/vendor/core/truth-spine.js +62 -0
- package/dist/vendor/core/types.d.ts +115 -0
- package/dist/vendor/core/types.js +2 -0
- package/dist/vendor/core/verification/tiered-verify.d.ts +17 -0
- package/dist/vendor/core/verification/tiered-verify.js +29 -0
- package/dist/vendor/core/verifier-pyramid.d.ts +32 -0
- package/dist/vendor/core/verifier-pyramid.js +111 -0
- package/dist/vendor/core/workflow-artifacts.d.ts +99 -0
- package/dist/vendor/core/workflow-artifacts.js +668 -0
- package/dist/vendor/core/wrap/supervised-run.d.ts +96 -0
- package/dist/vendor/core/wrap/supervised-run.js +178 -0
- package/docs/assets/cli-animated.svg +139 -0
- package/docs/assets/cli-static.svg +34 -0
- package/docs/assets/github-hero-v2.svg +23 -0
- package/docs/assets/martin-raplph.png.jpg +0 -0
- package/docs/assets/martinloop-logo.png +0 -0
- package/docs/assets/nvidia-inception-program-light.png +0 -0
- package/docs/assets/nvidia-inception-program.png +0 -0
- package/docs/assets/phase3c-sidesidebyside-demo.html +228 -0
- package/docs/assets/side-by-side.svg +134 -0
- package/docs/oss/CLAUDE-CODE-WALKTHROUGH.md +142 -0
- package/docs/oss/EXAMPLES.md +9 -1
- package/docs/oss/OSS-BOUNDARY-REPORT.json +109 -113
- package/docs/oss/OSS-BOUNDARY-REPORT.md +48 -48
- package/docs/oss/QUICKSTART.md +39 -4
- package/docs/oss/RALPH-LOOP-SAFETY.md +113 -0
- package/docs/oss/README.md +7 -4
- package/docs/oss/RELEASE-SURFACE-REPORT.json +46 -45
- package/docs/oss/RELEASE-SURFACE-REPORT.md +36 -35
- package/package.json +129 -49
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
// ─── Gate ordering ────────────────────────────────────────────────────────────
|
|
5
|
+
const GATE_ORDER = ["development", "backtest", "shadow", "live"];
|
|
6
|
+
function gateIndex(gate) {
|
|
7
|
+
return GATE_ORDER.indexOf(gate);
|
|
8
|
+
}
|
|
9
|
+
function nextGate(current) {
|
|
10
|
+
const idx = gateIndex(current);
|
|
11
|
+
return idx < GATE_ORDER.length - 1 ? GATE_ORDER[idx + 1] ?? null : null;
|
|
12
|
+
}
|
|
13
|
+
function prevGate(current) {
|
|
14
|
+
const idx = gateIndex(current);
|
|
15
|
+
return idx > 0 ? GATE_ORDER[idx - 1] ?? null : null;
|
|
16
|
+
}
|
|
17
|
+
// ─── Factory ──────────────────────────────────────────────────────────────────
|
|
18
|
+
export function createPriorSet(input) {
|
|
19
|
+
const confidence = Object.keys(input.priors).length > 0
|
|
20
|
+
? Object.values(input.priors).reduce((a, b) => a + b, 0) / Object.keys(input.priors).length
|
|
21
|
+
: 0;
|
|
22
|
+
return {
|
|
23
|
+
priorSetId: input.priorSetId,
|
|
24
|
+
taskClass: input.taskClass,
|
|
25
|
+
version: 1,
|
|
26
|
+
confidence,
|
|
27
|
+
priors: { ...input.priors },
|
|
28
|
+
lastUpdatedAt: new Date().toISOString(),
|
|
29
|
+
promotionGate: "development",
|
|
30
|
+
sampleCount: 0
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// ─── Persistence ─────────────────────────────────────────────────────────────
|
|
34
|
+
const STORE_FILE = "prior-sets.json";
|
|
35
|
+
export async function loadPriorStore(storeDir) {
|
|
36
|
+
const path = join(storeDir, STORE_FILE);
|
|
37
|
+
if (!existsSync(path))
|
|
38
|
+
return { priorSets: [] };
|
|
39
|
+
const raw = await readFile(path, "utf8");
|
|
40
|
+
return JSON.parse(raw);
|
|
41
|
+
}
|
|
42
|
+
export async function savePriorStore(store, storeDir) {
|
|
43
|
+
await writeFile(join(storeDir, STORE_FILE), JSON.stringify(store, null, 2), "utf8");
|
|
44
|
+
}
|
|
45
|
+
// ─── Operations ───────────────────────────────────────────────────────────────
|
|
46
|
+
/**
|
|
47
|
+
* Promotes a PriorSet to the next gate in the sequence.
|
|
48
|
+
* Gates must advance sequentially: development → backtest → shadow → live.
|
|
49
|
+
* Throws if the target gate is not the immediate next gate.
|
|
50
|
+
*/
|
|
51
|
+
export async function promotePriorSet(priorSetId, targetGate, storeDir) {
|
|
52
|
+
const store = await loadPriorStore(storeDir);
|
|
53
|
+
const idx = store.priorSets.findIndex(p => p.priorSetId === priorSetId);
|
|
54
|
+
if (idx === -1)
|
|
55
|
+
throw new Error(`PriorSet not found: ${priorSetId}`);
|
|
56
|
+
const current = store.priorSets[idx];
|
|
57
|
+
const expected = nextGate(current.promotionGate);
|
|
58
|
+
if (expected === null || targetGate !== expected) {
|
|
59
|
+
throw new Error(`Invalid promotion: ${current.promotionGate} → ${targetGate}. ` +
|
|
60
|
+
`Expected next gate: ${expected ?? "none (already at live)"}. Gates must advance sequentially.`);
|
|
61
|
+
}
|
|
62
|
+
const updated = {
|
|
63
|
+
...current,
|
|
64
|
+
promotionGate: targetGate,
|
|
65
|
+
version: current.version + 1,
|
|
66
|
+
lastUpdatedAt: new Date().toISOString()
|
|
67
|
+
};
|
|
68
|
+
store.priorSets[idx] = updated;
|
|
69
|
+
await savePriorStore(store, storeDir);
|
|
70
|
+
return updated;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Rolls back a PriorSet to the previous gate.
|
|
74
|
+
* Rollback is always available from any gate above development.
|
|
75
|
+
* Throws if already at development.
|
|
76
|
+
*/
|
|
77
|
+
export async function rollbackPriorSet(priorSetId, storeDir) {
|
|
78
|
+
const store = await loadPriorStore(storeDir);
|
|
79
|
+
const idx = store.priorSets.findIndex(p => p.priorSetId === priorSetId);
|
|
80
|
+
if (idx === -1)
|
|
81
|
+
throw new Error(`PriorSet not found: ${priorSetId}`);
|
|
82
|
+
const current = store.priorSets[idx];
|
|
83
|
+
const prev = prevGate(current.promotionGate);
|
|
84
|
+
if (prev === null) {
|
|
85
|
+
throw new Error(`Cannot rollback ${priorSetId}: already at development gate.`);
|
|
86
|
+
}
|
|
87
|
+
const updated = {
|
|
88
|
+
...current,
|
|
89
|
+
promotionGate: prev,
|
|
90
|
+
version: current.version + 1,
|
|
91
|
+
lastUpdatedAt: new Date().toISOString()
|
|
92
|
+
};
|
|
93
|
+
store.priorSets[idx] = updated;
|
|
94
|
+
await savePriorStore(store, storeDir);
|
|
95
|
+
return updated;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Returns all PriorSets from the store.
|
|
99
|
+
*/
|
|
100
|
+
export async function listPriorSets(storeDir) {
|
|
101
|
+
const store = await loadPriorStore(storeDir);
|
|
102
|
+
return store.priorSets;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Returns a single PriorSet by ID, or null if not found.
|
|
106
|
+
*/
|
|
107
|
+
export async function getPriorSet(priorSetId, storeDir) {
|
|
108
|
+
const store = await loadPriorStore(storeDir);
|
|
109
|
+
return store.priorSets.find(p => p.priorSetId === priorSetId) ?? null;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=prior-sets.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface GuardedPromotionInput {
|
|
2
|
+
candidateId: string;
|
|
3
|
+
holdoutPassed: number;
|
|
4
|
+
holdoutTotal: number;
|
|
5
|
+
regressionCount: number;
|
|
6
|
+
rollbackPlan: string;
|
|
7
|
+
humanApproved: boolean;
|
|
8
|
+
permanentChange: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface GuardedPromotionDecision {
|
|
11
|
+
candidateId: string;
|
|
12
|
+
decision: "promote" | "reject";
|
|
13
|
+
reasons: string[];
|
|
14
|
+
allowedClaimWording: string;
|
|
15
|
+
nonClaims: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare function evaluateGuardedPromotion(input: GuardedPromotionInput): GuardedPromotionDecision;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function evaluateGuardedPromotion(input) {
|
|
2
|
+
const reasons = [];
|
|
3
|
+
if (input.holdoutTotal <= 0 || input.holdoutPassed !== input.holdoutTotal) {
|
|
4
|
+
reasons.push("holdout suite did not pass completely");
|
|
5
|
+
}
|
|
6
|
+
if (input.regressionCount > 0) {
|
|
7
|
+
reasons.push("regressions detected");
|
|
8
|
+
}
|
|
9
|
+
if (input.rollbackPlan.trim().length === 0) {
|
|
10
|
+
reasons.push("rollback plan missing");
|
|
11
|
+
}
|
|
12
|
+
if (input.permanentChange && !input.humanApproved) {
|
|
13
|
+
reasons.push("human approval required for permanent change");
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
candidateId: input.candidateId,
|
|
17
|
+
decision: reasons.length === 0 ? "promote" : "reject",
|
|
18
|
+
reasons,
|
|
19
|
+
allowedClaimWording: "guarded self-improvement candidate promotion only after holdout, rollback, and human approval gates pass.",
|
|
20
|
+
nonClaims: ["unqualified self-improving", "silent self-modification"]
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=promotion-gate.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blast Radius Estimator — pre-execution risk scoring.
|
|
3
|
+
*
|
|
4
|
+
* Before any attempt runs, estimates the potential scope of impact (0–100)
|
|
5
|
+
* based on:
|
|
6
|
+
* - Objective language (refactor/migrate/delete keywords increase radius)
|
|
7
|
+
* - Known hotspot files in the likely path (files with regression history)
|
|
8
|
+
* - Repository size (larger repos have wider potential impact)
|
|
9
|
+
* - Files explicitly in scope (each hotspot-adjacent file adds risk)
|
|
10
|
+
*
|
|
11
|
+
* The router uses this score to gate high-risk tasks onto high-trust models.
|
|
12
|
+
* The CLI displays it in the pre-run Mission Brief.
|
|
13
|
+
*/
|
|
14
|
+
import type { RepoHotspot } from "../graph/hotspots.js";
|
|
15
|
+
export interface BlastRadiusInput {
|
|
16
|
+
/** The task objective text, used for keyword analysis */
|
|
17
|
+
objective: string;
|
|
18
|
+
/** Absolute path to the repository root */
|
|
19
|
+
repoRoot: string;
|
|
20
|
+
/** Files the task has explicitly identified as targets (optional) */
|
|
21
|
+
currentFiles?: string[];
|
|
22
|
+
/** Hotspot data from MartinGraph.assembleHotspotContext() (optional) */
|
|
23
|
+
hotspots?: RepoHotspot[];
|
|
24
|
+
}
|
|
25
|
+
export interface BlastRadiusResult {
|
|
26
|
+
/** Risk score 0–100. >70 triggers high-trust route enforcement. */
|
|
27
|
+
score: number;
|
|
28
|
+
/** Files identified as likely to be touched based on scope + hotspot data */
|
|
29
|
+
filesAtRisk: string[];
|
|
30
|
+
/** Subset of filesAtRisk that have recorded regression history */
|
|
31
|
+
hotspotsInPath: string[];
|
|
32
|
+
/** Categorical risk label derived from score */
|
|
33
|
+
regressionRisk: "low" | "medium" | "high";
|
|
34
|
+
/** Human-readable explanation of what drove the score */
|
|
35
|
+
rationale: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Estimates the blast radius of a planned task before execution.
|
|
39
|
+
* Non-throwing: if directory reads fail, falls back gracefully with
|
|
40
|
+
* a conservative medium score.
|
|
41
|
+
*/
|
|
42
|
+
export declare function estimateBlastRadius(input: BlastRadiusInput): Promise<BlastRadiusResult>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blast Radius Estimator — pre-execution risk scoring.
|
|
3
|
+
*
|
|
4
|
+
* Before any attempt runs, estimates the potential scope of impact (0–100)
|
|
5
|
+
* based on:
|
|
6
|
+
* - Objective language (refactor/migrate/delete keywords increase radius)
|
|
7
|
+
* - Known hotspot files in the likely path (files with regression history)
|
|
8
|
+
* - Repository size (larger repos have wider potential impact)
|
|
9
|
+
* - Files explicitly in scope (each hotspot-adjacent file adds risk)
|
|
10
|
+
*
|
|
11
|
+
* The router uses this score to gate high-risk tasks onto high-trust models.
|
|
12
|
+
* The CLI displays it in the pre-run Mission Brief.
|
|
13
|
+
*/
|
|
14
|
+
import { readdir } from "node:fs/promises";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
/** Keywords that strongly suggest broad file scope changes */
|
|
17
|
+
const HIGH_BLAST_KEYWORDS = [
|
|
18
|
+
"refactor",
|
|
19
|
+
"migrate",
|
|
20
|
+
"restructure",
|
|
21
|
+
"rename",
|
|
22
|
+
"move",
|
|
23
|
+
"delete",
|
|
24
|
+
"remove all",
|
|
25
|
+
"overhaul",
|
|
26
|
+
"rewrite",
|
|
27
|
+
"reorganize",
|
|
28
|
+
"consolidate"
|
|
29
|
+
];
|
|
30
|
+
/** Keywords that suggest narrow, test-scoped changes (reduce risk) */
|
|
31
|
+
const LOW_BLAST_KEYWORDS = [
|
|
32
|
+
"test",
|
|
33
|
+
"spec",
|
|
34
|
+
"fixture",
|
|
35
|
+
"mock",
|
|
36
|
+
"stub",
|
|
37
|
+
"snapshot",
|
|
38
|
+
"add test",
|
|
39
|
+
"write test"
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Estimates the blast radius of a planned task before execution.
|
|
43
|
+
* Non-throwing: if directory reads fail, falls back gracefully with
|
|
44
|
+
* a conservative medium score.
|
|
45
|
+
*/
|
|
46
|
+
export async function estimateBlastRadius(input) {
|
|
47
|
+
const { objective, repoRoot, currentFiles = [], hotspots = [] } = input;
|
|
48
|
+
const objectiveLower = objective.toLowerCase();
|
|
49
|
+
const rationale = [];
|
|
50
|
+
let score = 10; // Every task has nonzero blast radius
|
|
51
|
+
// ── Keyword analysis ──────────────────────────────────────────────────────
|
|
52
|
+
const highKeywordsFound = HIGH_BLAST_KEYWORDS.filter((k) => objectiveLower.includes(k));
|
|
53
|
+
if (highKeywordsFound.length > 0) {
|
|
54
|
+
const delta = Math.min(25, highKeywordsFound.length * 12);
|
|
55
|
+
score += delta;
|
|
56
|
+
rationale.push(`Objective contains high-scope keywords: ${highKeywordsFound.join(", ")} (+${String(delta)})`);
|
|
57
|
+
}
|
|
58
|
+
const lowKeywordsFound = LOW_BLAST_KEYWORDS.filter((k) => objectiveLower.includes(k));
|
|
59
|
+
if (lowKeywordsFound.length > 0 && highKeywordsFound.length === 0) {
|
|
60
|
+
score -= 10;
|
|
61
|
+
rationale.push(`Objective is test-scoped (${lowKeywordsFound[0]}), reducing radius (-10)`);
|
|
62
|
+
}
|
|
63
|
+
// ── Repository size ───────────────────────────────────────────────────────
|
|
64
|
+
const repoFileCount = await countFiles(repoRoot);
|
|
65
|
+
if (repoFileCount > 500) {
|
|
66
|
+
score += 20;
|
|
67
|
+
rationale.push(`Large repository (${String(repoFileCount)} files, +20)`);
|
|
68
|
+
}
|
|
69
|
+
else if (repoFileCount > 100) {
|
|
70
|
+
score += 8;
|
|
71
|
+
rationale.push(`Medium repository (${String(repoFileCount)} files, +8)`);
|
|
72
|
+
}
|
|
73
|
+
// ── Hotspot overlap ───────────────────────────────────────────────────────
|
|
74
|
+
const filesAtRisk = [...currentFiles];
|
|
75
|
+
const hotspotsInPath = [];
|
|
76
|
+
// Add hotspot files that are likely in scope (high regression count)
|
|
77
|
+
const criticalHotspots = hotspots.filter((h) => h.regressionCount > 2);
|
|
78
|
+
for (const hotspot of criticalHotspots) {
|
|
79
|
+
const delta = 15;
|
|
80
|
+
score += delta;
|
|
81
|
+
hotspotsInPath.push(hotspot.file);
|
|
82
|
+
if (!filesAtRisk.includes(hotspot.file))
|
|
83
|
+
filesAtRisk.push(hotspot.file);
|
|
84
|
+
rationale.push(`Hotspot in path: ${hotspot.file} (${String(hotspot.regressionCount)} regressions, +${String(delta)})`);
|
|
85
|
+
}
|
|
86
|
+
// Files with any regression history add smaller penalty
|
|
87
|
+
const mildHotspots = hotspots.filter((h) => h.regressionCount > 0 && h.regressionCount <= 2);
|
|
88
|
+
for (const hotspot of mildHotspots) {
|
|
89
|
+
score += 5;
|
|
90
|
+
if (!filesAtRisk.includes(hotspot.file))
|
|
91
|
+
filesAtRisk.push(hotspot.file);
|
|
92
|
+
}
|
|
93
|
+
if (mildHotspots.length > 0) {
|
|
94
|
+
rationale.push(`${String(mildHotspots.length)} files with minor regression history (+${String(mildHotspots.length * 5)})`);
|
|
95
|
+
}
|
|
96
|
+
// ── Explicit files in scope ────────────────────────────────────────────────
|
|
97
|
+
if (currentFiles.length > 0) {
|
|
98
|
+
const adjacentHotspots = currentFiles.filter((f) => hotspots.some((h) => h.file === f && h.regressionCount > 0));
|
|
99
|
+
if (adjacentHotspots.length > 0) {
|
|
100
|
+
score += adjacentHotspots.length * 10;
|
|
101
|
+
rationale.push(`${String(adjacentHotspots.length)} explicitly targeted files have regression history (+${String(adjacentHotspots.length * 10)})`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// ── Clamp and label ───────────────────────────────────────────────────────
|
|
105
|
+
const clampedScore = Math.max(0, Math.min(100, score));
|
|
106
|
+
const regressionRisk = clampedScore >= 70 ? "high" : clampedScore >= 40 ? "medium" : "low";
|
|
107
|
+
if (rationale.length === 0) {
|
|
108
|
+
rationale.push("No elevated risk signals detected");
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
score: clampedScore,
|
|
112
|
+
filesAtRisk,
|
|
113
|
+
hotspotsInPath,
|
|
114
|
+
regressionRisk,
|
|
115
|
+
rationale: rationale.join("; ")
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Counts source files in a directory (non-recursive for performance).
|
|
120
|
+
* Only counts .ts, .tsx, .js, .jsx, .py, .go, .rs files — not config/docs.
|
|
121
|
+
* Returns 0 on any error (missing dir, permission denied, etc.).
|
|
122
|
+
*/
|
|
123
|
+
async function countFiles(dir) {
|
|
124
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
125
|
+
".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java", ".cs", ".rb"
|
|
126
|
+
]);
|
|
127
|
+
async function countRecursive(current, depth) {
|
|
128
|
+
if (depth > 6)
|
|
129
|
+
return 0; // don't recurse into deep vendor trees
|
|
130
|
+
try {
|
|
131
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
132
|
+
let count = 0;
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (entry.isDirectory()) {
|
|
138
|
+
count += await countRecursive(join(current, entry.name), depth + 1);
|
|
139
|
+
}
|
|
140
|
+
else if (SOURCE_EXTENSIONS.has(extname(entry.name))) {
|
|
141
|
+
count += 1;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return count;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return countRecursive(dir, 0);
|
|
151
|
+
}
|
|
152
|
+
function extname(name) {
|
|
153
|
+
const dot = name.lastIndexOf(".");
|
|
154
|
+
return dot >= 0 ? name.slice(dot) : "";
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=blast-radius.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type LeashInput, type PolicyResult } from "@martin/policy";
|
|
2
|
+
import type { SafetyLeashDecision } from "../leash.js";
|
|
3
|
+
export interface PolicyLeashOptions {
|
|
4
|
+
/** Explicit compiled policy path. Raw Rego fails closed inside @martin/policy. */
|
|
5
|
+
policyPath?: string;
|
|
6
|
+
/** Explicit compiled WASM policy path. Takes precedence over policyPath. */
|
|
7
|
+
policyWasmPath?: string;
|
|
8
|
+
/** Repository root used for .martin/policy.wasm discovery. */
|
|
9
|
+
repoRoot?: string;
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface PolicyDeniedInput {
|
|
13
|
+
surface: string;
|
|
14
|
+
command?: string | null;
|
|
15
|
+
path?: string;
|
|
16
|
+
value?: string | null;
|
|
17
|
+
reasons: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface PolicyLeashVerdict {
|
|
20
|
+
allow: boolean;
|
|
21
|
+
reasons: string[];
|
|
22
|
+
deniedInputs: PolicyDeniedInput[];
|
|
23
|
+
inputsEvaluated: number;
|
|
24
|
+
source?: PolicyResult["source"];
|
|
25
|
+
policyPath?: string;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
export type PolicyBackedSafetyLeashDecision = SafetyLeashDecision & {
|
|
29
|
+
policyVerdict: PolicyLeashVerdict;
|
|
30
|
+
};
|
|
31
|
+
export declare function applyPolicyToLeashDecision(decision: SafetyLeashDecision, inputs: LeashInput[], options?: PolicyLeashOptions): Promise<PolicyBackedSafetyLeashDecision>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { evaluate as evaluateOpaPolicy } from "@martin/policy";
|
|
2
|
+
export async function applyPolicyToLeashDecision(decision, inputs, options = {}) {
|
|
3
|
+
const policyVerdict = await evaluatePolicyInputs(inputs, options);
|
|
4
|
+
if (policyVerdict.allow) {
|
|
5
|
+
return {
|
|
6
|
+
...decision,
|
|
7
|
+
policyVerdict
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
const policyViolations = policyVerdict.deniedInputs.flatMap(toPolicyViolations);
|
|
11
|
+
const policyBlockedCommands = policyVerdict.deniedInputs
|
|
12
|
+
.map((input) => input.command)
|
|
13
|
+
.filter((command) => typeof command === "string" && command.length > 0);
|
|
14
|
+
return {
|
|
15
|
+
...decision,
|
|
16
|
+
allowed: false,
|
|
17
|
+
blocked: true,
|
|
18
|
+
riskLevel: "blocked",
|
|
19
|
+
blockedCommands: [...new Set([...decision.blockedCommands, ...policyBlockedCommands])],
|
|
20
|
+
violations: [...decision.violations, ...policyViolations],
|
|
21
|
+
reason: joinReasons([
|
|
22
|
+
decision.reason,
|
|
23
|
+
`OPA policy denied ${policyVerdict.deniedInputs.length} safety input(s): ${policyVerdict.reasons.join("; ")}`
|
|
24
|
+
]),
|
|
25
|
+
policyVerdict
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async function evaluatePolicyInputs(inputs, options) {
|
|
29
|
+
if (inputs.length === 0) {
|
|
30
|
+
return {
|
|
31
|
+
allow: true,
|
|
32
|
+
reasons: [],
|
|
33
|
+
deniedInputs: [],
|
|
34
|
+
inputsEvaluated: 0
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const deniedInputs = [];
|
|
38
|
+
const reasons = new Set();
|
|
39
|
+
let source;
|
|
40
|
+
let policyPath;
|
|
41
|
+
for (const input of inputs) {
|
|
42
|
+
try {
|
|
43
|
+
const result = await withTimeout(evaluateOpaPolicy(input, options.policyWasmPath ?? options.policyPath, options.repoRoot ? { repoRoot: options.repoRoot } : undefined), options.timeoutMs ?? 2_000);
|
|
44
|
+
source ??= result.source;
|
|
45
|
+
policyPath ??= result.policyPath;
|
|
46
|
+
if (!result.allow) {
|
|
47
|
+
for (const reason of result.reasons) {
|
|
48
|
+
reasons.add(reason);
|
|
49
|
+
}
|
|
50
|
+
deniedInputs.push({
|
|
51
|
+
surface: input.surface,
|
|
52
|
+
...(input.command !== undefined ? { command: input.command } : {}),
|
|
53
|
+
...(input.path !== undefined ? { path: input.path } : {}),
|
|
54
|
+
...(input.value !== undefined ? { value: input.value } : {}),
|
|
55
|
+
reasons: result.reasons
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
reasons.add(message);
|
|
62
|
+
deniedInputs.push({
|
|
63
|
+
surface: input.surface,
|
|
64
|
+
...(input.command !== undefined ? { command: input.command } : {}),
|
|
65
|
+
...(input.path !== undefined ? { path: input.path } : {}),
|
|
66
|
+
...(input.value !== undefined ? { value: input.value } : {}),
|
|
67
|
+
reasons: [message]
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
allow: false,
|
|
71
|
+
reasons: [...reasons],
|
|
72
|
+
deniedInputs,
|
|
73
|
+
inputsEvaluated: inputs.length,
|
|
74
|
+
...(source ? { source } : {}),
|
|
75
|
+
...(policyPath ? { policyPath } : {}),
|
|
76
|
+
error: message
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
allow: deniedInputs.length === 0,
|
|
82
|
+
reasons: [...reasons],
|
|
83
|
+
deniedInputs,
|
|
84
|
+
inputsEvaluated: inputs.length,
|
|
85
|
+
...(source ? { source } : {}),
|
|
86
|
+
...(policyPath ? { policyPath } : {})
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function toPolicyViolations(input) {
|
|
90
|
+
return input.reasons.map((reason) => ({
|
|
91
|
+
kind: "policy_denied",
|
|
92
|
+
message: `OPA policy denied ${input.surface}: ${reason}`,
|
|
93
|
+
...(input.command ? { command: input.command } : {}),
|
|
94
|
+
...(input.path ? { file: input.path } : {}),
|
|
95
|
+
...(input.value ? { match: input.value } : {})
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
function joinReasons(values) {
|
|
99
|
+
return values.filter((value) => Boolean(value && value.length > 0)).join(" ");
|
|
100
|
+
}
|
|
101
|
+
async function withTimeout(promise, timeoutMs) {
|
|
102
|
+
let timeout;
|
|
103
|
+
try {
|
|
104
|
+
return await Promise.race([
|
|
105
|
+
promise,
|
|
106
|
+
new Promise((_, reject) => {
|
|
107
|
+
timeout = setTimeout(() => reject(new Error(`OPA policy evaluation timed out after ${timeoutMs}ms.`)), timeoutMs);
|
|
108
|
+
})
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
if (timeout) {
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=policy-leash.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memo.ts — SLICE-03
|
|
3
|
+
*
|
|
4
|
+
* Attempt memoization via objective + verifier + file hash key.
|
|
5
|
+
*
|
|
6
|
+
* On a cache HIT: the caller replays the stored patch with zero model calls.
|
|
7
|
+
* On a cache MISS: the caller runs the normal attempt path, then calls storeMemo.
|
|
8
|
+
*
|
|
9
|
+
* Cache key: SHA-256 of (objective, verifierCommand, sorted file path → hash pairs)
|
|
10
|
+
* Cache location: <memoRoot>/<key>/ (default: ~/.martin/memo/)
|
|
11
|
+
* Invalidation: any file hash change produces a different key → automatic miss
|
|
12
|
+
* TTL: keys never expire (deterministic inputs → deterministic output is safe to reuse)
|
|
13
|
+
*/
|
|
14
|
+
export interface MemoInput {
|
|
15
|
+
/** Exact objective text. */
|
|
16
|
+
objective: string;
|
|
17
|
+
/** The verifier command string (e.g. "pnpm test"). */
|
|
18
|
+
verifierCommand: string;
|
|
19
|
+
/**
|
|
20
|
+
* Map of relevant file paths → their SHA-256 (or similar) hashes.
|
|
21
|
+
* Any change in any hash invalidates the cache key.
|
|
22
|
+
*/
|
|
23
|
+
fileHashes: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
export interface MemoEntry {
|
|
26
|
+
/** The SHA-256 cache key that produced this entry. */
|
|
27
|
+
key: string;
|
|
28
|
+
/** Unified diff text that was produced by the original attempt. */
|
|
29
|
+
patch: string;
|
|
30
|
+
/** Raw verifier output from the original successful attempt. */
|
|
31
|
+
verifierOutput: string;
|
|
32
|
+
/** ISO timestamp when the entry was stored. */
|
|
33
|
+
storedAt: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Computes the deterministic SHA-256 cache key for a given MemoInput.
|
|
37
|
+
*
|
|
38
|
+
* File hashes are sorted by path before hashing so that insertion order
|
|
39
|
+
* does not affect the key (same files → same key).
|
|
40
|
+
*
|
|
41
|
+
* verificationPlan step order IS significant (pass-order matters for verifiers).
|
|
42
|
+
*/
|
|
43
|
+
export declare function computeMemoKey(input: MemoInput): string;
|
|
44
|
+
/**
|
|
45
|
+
* Returns the stored MemoEntry for the given inputs, or null on a cache miss.
|
|
46
|
+
*
|
|
47
|
+
* A miss occurs when:
|
|
48
|
+
* - No entry exists for this key (never been run)
|
|
49
|
+
* - Any file hash changed (different key → automatic miss)
|
|
50
|
+
*/
|
|
51
|
+
export declare function lookupMemo(input: MemoInput, memoRoot?: string): Promise<MemoEntry | null>;
|
|
52
|
+
/**
|
|
53
|
+
* Persists a successful attempt result to the memo cache.
|
|
54
|
+
*
|
|
55
|
+
* Writes two files under <memoRoot>/<key>/:
|
|
56
|
+
* - entry.json — the full MemoEntry (patch + verifier output)
|
|
57
|
+
* - cache-hit.json — lightweight provenance artifact for audit trail
|
|
58
|
+
*
|
|
59
|
+
* The `entry.key` is set to the computed key before writing.
|
|
60
|
+
*/
|
|
61
|
+
export declare function storeMemo(input: MemoInput, entry: Omit<MemoEntry, "key"> & {
|
|
62
|
+
key?: string;
|
|
63
|
+
}, memoRoot?: string): Promise<string>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memo.ts — SLICE-03
|
|
3
|
+
*
|
|
4
|
+
* Attempt memoization via objective + verifier + file hash key.
|
|
5
|
+
*
|
|
6
|
+
* On a cache HIT: the caller replays the stored patch with zero model calls.
|
|
7
|
+
* On a cache MISS: the caller runs the normal attempt path, then calls storeMemo.
|
|
8
|
+
*
|
|
9
|
+
* Cache key: SHA-256 of (objective, verifierCommand, sorted file path → hash pairs)
|
|
10
|
+
* Cache location: <memoRoot>/<key>/ (default: ~/.martin/memo/)
|
|
11
|
+
* Invalidation: any file hash change produces a different key → automatic miss
|
|
12
|
+
* TTL: keys never expire (deterministic inputs → deterministic output is safe to reuse)
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Key computation
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* Computes the deterministic SHA-256 cache key for a given MemoInput.
|
|
23
|
+
*
|
|
24
|
+
* File hashes are sorted by path before hashing so that insertion order
|
|
25
|
+
* does not affect the key (same files → same key).
|
|
26
|
+
*
|
|
27
|
+
* verificationPlan step order IS significant (pass-order matters for verifiers).
|
|
28
|
+
*/
|
|
29
|
+
export function computeMemoKey(input) {
|
|
30
|
+
const sortedFiles = Object.entries(input.fileHashes)
|
|
31
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
32
|
+
const canonical = JSON.stringify({
|
|
33
|
+
objective: input.objective,
|
|
34
|
+
verifierCommand: input.verifierCommand,
|
|
35
|
+
fileHashes: Object.fromEntries(sortedFiles)
|
|
36
|
+
});
|
|
37
|
+
return createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
38
|
+
}
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Lookup
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
/**
|
|
43
|
+
* Returns the stored MemoEntry for the given inputs, or null on a cache miss.
|
|
44
|
+
*
|
|
45
|
+
* A miss occurs when:
|
|
46
|
+
* - No entry exists for this key (never been run)
|
|
47
|
+
* - Any file hash changed (different key → automatic miss)
|
|
48
|
+
*/
|
|
49
|
+
export async function lookupMemo(input, memoRoot = defaultMemoRoot()) {
|
|
50
|
+
const key = computeMemoKey(input);
|
|
51
|
+
const entryPath = join(memoRoot, key, "entry.json");
|
|
52
|
+
try {
|
|
53
|
+
const raw = await readFile(entryPath, "utf8");
|
|
54
|
+
return JSON.parse(raw);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Store
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
/**
|
|
64
|
+
* Persists a successful attempt result to the memo cache.
|
|
65
|
+
*
|
|
66
|
+
* Writes two files under <memoRoot>/<key>/:
|
|
67
|
+
* - entry.json — the full MemoEntry (patch + verifier output)
|
|
68
|
+
* - cache-hit.json — lightweight provenance artifact for audit trail
|
|
69
|
+
*
|
|
70
|
+
* The `entry.key` is set to the computed key before writing.
|
|
71
|
+
*/
|
|
72
|
+
export async function storeMemo(input, entry, memoRoot = defaultMemoRoot()) {
|
|
73
|
+
const key = computeMemoKey(input);
|
|
74
|
+
const keyDir = join(memoRoot, key);
|
|
75
|
+
await mkdir(keyDir, { recursive: true });
|
|
76
|
+
const fullEntry = { ...entry, key };
|
|
77
|
+
// Write the main entry
|
|
78
|
+
await writeFile(join(keyDir, "entry.json"), `${JSON.stringify(fullEntry, null, 2)}\n`, "utf8");
|
|
79
|
+
// Write the provenance artifact (lightweight, safe to read without loading full patch)
|
|
80
|
+
const provenance = {
|
|
81
|
+
key,
|
|
82
|
+
objective: input.objective,
|
|
83
|
+
verifierCommand: input.verifierCommand,
|
|
84
|
+
fileCount: Object.keys(input.fileHashes).length,
|
|
85
|
+
storedAt: entry.storedAt
|
|
86
|
+
};
|
|
87
|
+
await writeFile(join(keyDir, "cache-hit.json"), `${JSON.stringify(provenance, null, 2)}\n`, "utf8");
|
|
88
|
+
return key;
|
|
89
|
+
}
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Internals
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
function defaultMemoRoot() {
|
|
94
|
+
return process.env["MARTIN_MEMO_DIR"]?.trim()
|
|
95
|
+
?? join(homedir(), ".martin", "memo");
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=memo.js.map
|