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,55 @@
|
|
|
1
|
+
// SLICE-17 — Per-run cost attribution by tag
|
|
2
|
+
// Groups cost ledger entries by arbitrary key-value tags for chargeback reporting.
|
|
3
|
+
/**
|
|
4
|
+
* Groups cost entries by the value of a specific tag key.
|
|
5
|
+
* Entries missing the tag key are grouped under "(untagged)".
|
|
6
|
+
* Results are sorted descending by totalUsd.
|
|
7
|
+
*/
|
|
8
|
+
export function aggregateCostsByTag(entries, tagKey) {
|
|
9
|
+
const groups = new Map();
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
const tagValue = entry.tags[tagKey] ?? "(untagged)";
|
|
12
|
+
const existing = groups.get(tagValue);
|
|
13
|
+
if (existing) {
|
|
14
|
+
existing.push(entry);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
groups.set(tagValue, [entry]);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const summaries = [];
|
|
21
|
+
for (const [value, groupEntries] of groups) {
|
|
22
|
+
const totalUsd = groupEntries.reduce((sum, e) => sum + e.totalUsd, 0);
|
|
23
|
+
summaries.push({
|
|
24
|
+
tag: tagKey,
|
|
25
|
+
value,
|
|
26
|
+
totalUsd,
|
|
27
|
+
runCount: groupEntries.length,
|
|
28
|
+
entries: groupEntries
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// Sort descending by totalUsd
|
|
32
|
+
summaries.sort((a, b) => b.totalUsd - a.totalUsd);
|
|
33
|
+
return summaries;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Formats a list of TaggedCostSummary objects as a plain-text table.
|
|
37
|
+
* Columns: tag value | runs | total USD
|
|
38
|
+
* Sorted descending by totalUsd (aggregateCostsByTag already sorts).
|
|
39
|
+
*/
|
|
40
|
+
export function formatCostReport(summaries) {
|
|
41
|
+
if (summaries.length === 0) {
|
|
42
|
+
return "No cost data available.";
|
|
43
|
+
}
|
|
44
|
+
const tagKey = summaries[0].tag;
|
|
45
|
+
const header = `${tagKey.padEnd(30)} | ${"runs".padStart(6)} | ${"total USD".padStart(10)}`;
|
|
46
|
+
const separator = "-".repeat(header.length);
|
|
47
|
+
const rows = summaries.map((s) => {
|
|
48
|
+
const value = s.value.padEnd(30);
|
|
49
|
+
const runs = String(s.runCount).padStart(6);
|
|
50
|
+
const cost = `$${s.totalUsd.toFixed(4)}`.padStart(10);
|
|
51
|
+
return `${value} | ${runs} | ${cost}`;
|
|
52
|
+
});
|
|
53
|
+
return [header, separator, ...rows].join("\n");
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=tagged-cost.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function evaluateCostGovernor(input) {
|
|
2
|
+
const totalTokens = input.cost.tokensIn + input.cost.tokensOut;
|
|
3
|
+
const projectedUsd = input.cost.actualUsd + (input.projectedUsage?.actualUsd ?? 0);
|
|
4
|
+
const projectedTokens = totalTokens +
|
|
5
|
+
(input.projectedUsage?.tokensIn ?? 0) +
|
|
6
|
+
(input.projectedUsage?.tokensOut ?? 0);
|
|
7
|
+
const remainingBudgetUsd = clamp(input.budget.maxUsd - input.cost.actualUsd);
|
|
8
|
+
const remainingIterations = Math.max(0, input.budget.maxIterations - input.attemptsUsed);
|
|
9
|
+
const remainingTokens = Math.max(0, input.budget.maxTokens - totalTokens);
|
|
10
|
+
const hardLimitReached = input.cost.actualUsd >= input.budget.maxUsd ||
|
|
11
|
+
projectedUsd > input.budget.maxUsd ||
|
|
12
|
+
totalTokens >= input.budget.maxTokens ||
|
|
13
|
+
projectedTokens > input.budget.maxTokens ||
|
|
14
|
+
input.attemptsUsed >= input.budget.maxIterations;
|
|
15
|
+
if (hardLimitReached) {
|
|
16
|
+
return {
|
|
17
|
+
pressure: "hard_limit",
|
|
18
|
+
shouldStop: true,
|
|
19
|
+
remainingBudgetUsd,
|
|
20
|
+
remainingIterations,
|
|
21
|
+
remainingTokens,
|
|
22
|
+
recommendedIntervention: "stop_loop"
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const softLimitReached = input.cost.actualUsd >= input.budget.softLimitUsd ||
|
|
26
|
+
projectedUsd >= input.budget.softLimitUsd ||
|
|
27
|
+
totalTokens >= input.budget.maxTokens * 0.75 ||
|
|
28
|
+
remainingIterations <= 1;
|
|
29
|
+
if (softLimitReached) {
|
|
30
|
+
return {
|
|
31
|
+
pressure: "soft_limit",
|
|
32
|
+
shouldStop: false,
|
|
33
|
+
remainingBudgetUsd,
|
|
34
|
+
remainingIterations,
|
|
35
|
+
remainingTokens,
|
|
36
|
+
recommendedIntervention: "compress_context"
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
pressure: "healthy",
|
|
41
|
+
shouldStop: false,
|
|
42
|
+
remainingBudgetUsd,
|
|
43
|
+
remainingIterations,
|
|
44
|
+
remainingTokens
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function clamp(value) {
|
|
48
|
+
return value < 0 ? 0 : Number(value.toFixed(4));
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=cost-governor.js.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cve-check.ts — SLICE-22
|
|
3
|
+
*
|
|
4
|
+
* CVE-watch pre-execution hook.
|
|
5
|
+
*
|
|
6
|
+
* Before a patch is applied, scans the diff for any package.json changes that
|
|
7
|
+
* introduce or upgrade a dependency. Each new/upgraded package is checked against
|
|
8
|
+
* the OSV (Open Source Vulnerabilities) API.
|
|
9
|
+
*
|
|
10
|
+
* Blocking behavior (configurable via martin.policy.yaml):
|
|
11
|
+
* cveCheck:
|
|
12
|
+
* enabled: true # default: true
|
|
13
|
+
* blockSeverity: HIGH # CRITICAL | HIGH | MEDIUM | LOW
|
|
14
|
+
* failClosed: false # true = block on API error (default: false = warn)
|
|
15
|
+
*
|
|
16
|
+
* The result is written to cve-check.json in the attempt artifacts directory.
|
|
17
|
+
*/
|
|
18
|
+
export type CveSeverity = "CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "UNKNOWN";
|
|
19
|
+
export interface CveCheckPolicy {
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
/** Block when any finding is at or above this severity. */
|
|
22
|
+
blockSeverity: CveSeverity;
|
|
23
|
+
/** When true, an OSV API error blocks the patch. Default false (fail-open). */
|
|
24
|
+
failClosed: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface PackageRef {
|
|
27
|
+
name: string;
|
|
28
|
+
version: string;
|
|
29
|
+
ecosystem: "npm" | "PyPI" | "crates.io" | "Go";
|
|
30
|
+
}
|
|
31
|
+
export interface CveFinding {
|
|
32
|
+
vulnerabilityId: string;
|
|
33
|
+
severity: CveSeverity;
|
|
34
|
+
packageName: string;
|
|
35
|
+
packageVersion: string;
|
|
36
|
+
summary?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface CveCheckResult {
|
|
39
|
+
blocked: boolean;
|
|
40
|
+
/** Populated when blocked:true due to a CVE finding. */
|
|
41
|
+
reasonCode?: "cve_blocked" | "cve_api_error";
|
|
42
|
+
/** Human-readable explanation. */
|
|
43
|
+
reason?: string;
|
|
44
|
+
findings: CveFinding[];
|
|
45
|
+
/** Populated when the OSV call failed. */
|
|
46
|
+
apiError?: string;
|
|
47
|
+
/** true when cveCheck.enabled:false in policy. */
|
|
48
|
+
skipped?: boolean;
|
|
49
|
+
/** ISO timestamp of when the check ran. */
|
|
50
|
+
checkedAt: string;
|
|
51
|
+
/** Number of packages checked. */
|
|
52
|
+
checkedCount: number;
|
|
53
|
+
}
|
|
54
|
+
export interface OsvQueryResult {
|
|
55
|
+
results: Array<{
|
|
56
|
+
vulns?: Array<{
|
|
57
|
+
id: string;
|
|
58
|
+
summary?: string;
|
|
59
|
+
database_specific?: {
|
|
60
|
+
severity?: string;
|
|
61
|
+
};
|
|
62
|
+
severity?: Array<{
|
|
63
|
+
type: string;
|
|
64
|
+
score: string;
|
|
65
|
+
}>;
|
|
66
|
+
}>;
|
|
67
|
+
}>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Extracts added/upgraded npm package name+version pairs from a unified diff.
|
|
71
|
+
*
|
|
72
|
+
* Looks for `+ "name": "version"` patterns on lines added within package.json hunks.
|
|
73
|
+
* Supports regular and scoped packages (@scope/name).
|
|
74
|
+
*/
|
|
75
|
+
export declare function parseDependencyChanges(diff: string): PackageRef[];
|
|
76
|
+
export declare function checkCves(options: {
|
|
77
|
+
packages: PackageRef[];
|
|
78
|
+
policy: CveCheckPolicy;
|
|
79
|
+
fetchFn?: typeof fetch;
|
|
80
|
+
}): Promise<CveCheckResult>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cve-check.ts — SLICE-22
|
|
3
|
+
*
|
|
4
|
+
* CVE-watch pre-execution hook.
|
|
5
|
+
*
|
|
6
|
+
* Before a patch is applied, scans the diff for any package.json changes that
|
|
7
|
+
* introduce or upgrade a dependency. Each new/upgraded package is checked against
|
|
8
|
+
* the OSV (Open Source Vulnerabilities) API.
|
|
9
|
+
*
|
|
10
|
+
* Blocking behavior (configurable via martin.policy.yaml):
|
|
11
|
+
* cveCheck:
|
|
12
|
+
* enabled: true # default: true
|
|
13
|
+
* blockSeverity: HIGH # CRITICAL | HIGH | MEDIUM | LOW
|
|
14
|
+
* failClosed: false # true = block on API error (default: false = warn)
|
|
15
|
+
*
|
|
16
|
+
* The result is written to cve-check.json in the attempt artifacts directory.
|
|
17
|
+
*/
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Diff parsing
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* Extracts added/upgraded npm package name+version pairs from a unified diff.
|
|
23
|
+
*
|
|
24
|
+
* Looks for `+ "name": "version"` patterns on lines added within package.json hunks.
|
|
25
|
+
* Supports regular and scoped packages (@scope/name).
|
|
26
|
+
*/
|
|
27
|
+
export function parseDependencyChanges(diff) {
|
|
28
|
+
const found = [];
|
|
29
|
+
let inPackageJson = false;
|
|
30
|
+
for (const line of diff.split("\n")) {
|
|
31
|
+
// Track when we're inside a package.json diff block
|
|
32
|
+
if (line.startsWith("diff --git")) {
|
|
33
|
+
inPackageJson = /\bpackage\.json\b/.test(line);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (!inPackageJson)
|
|
37
|
+
continue;
|
|
38
|
+
// Only look at added lines (+ prefix, not +++ header)
|
|
39
|
+
if (!line.startsWith("+") || line.startsWith("+++"))
|
|
40
|
+
continue;
|
|
41
|
+
const content = line.slice(1); // strip leading +
|
|
42
|
+
// Match: "name": "version" or "name": "^version" etc.
|
|
43
|
+
// Supports scoped packages like @scope/name
|
|
44
|
+
const match = content.match(/"(@?[\w/.-]+)":\s*"[~^]?([0-9][^"]*|latest|next)"/u);
|
|
45
|
+
if (match?.[1] && match[2]) {
|
|
46
|
+
const name = match[1];
|
|
47
|
+
const version = match[2].replace(/^[~^]/, "");
|
|
48
|
+
// Skip non-package keys that might look like packages
|
|
49
|
+
const SKIP_KEYS = new Set(["version", "main", "module", "types", "license", "description"]);
|
|
50
|
+
if (!SKIP_KEYS.has(name)) {
|
|
51
|
+
found.push({ name, version, ecosystem: "npm" });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return found;
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// OSV query
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
const OSV_BATCH_URL = "https://api.osv.dev/v1/querybatch";
|
|
61
|
+
const SEVERITY_ORDER = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
62
|
+
function severityFromString(raw) {
|
|
63
|
+
const upper = (raw ?? "").toUpperCase();
|
|
64
|
+
if (upper === "CRITICAL")
|
|
65
|
+
return "CRITICAL";
|
|
66
|
+
if (upper === "HIGH")
|
|
67
|
+
return "HIGH";
|
|
68
|
+
if (upper === "MEDIUM")
|
|
69
|
+
return "MEDIUM";
|
|
70
|
+
if (upper === "LOW")
|
|
71
|
+
return "LOW";
|
|
72
|
+
return "UNKNOWN";
|
|
73
|
+
}
|
|
74
|
+
function isSeverityAtOrAbove(finding, threshold) {
|
|
75
|
+
const fi = SEVERITY_ORDER.indexOf(finding);
|
|
76
|
+
const ti = SEVERITY_ORDER.indexOf(threshold);
|
|
77
|
+
if (fi === -1 || ti === -1)
|
|
78
|
+
return false;
|
|
79
|
+
return fi >= ti;
|
|
80
|
+
}
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Main entry point
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
export async function checkCves(options) {
|
|
85
|
+
const checkedAt = new Date().toISOString();
|
|
86
|
+
const { packages, policy, fetchFn = fetch } = options;
|
|
87
|
+
// Fast exit: disabled by policy
|
|
88
|
+
if (!policy.enabled) {
|
|
89
|
+
return { blocked: false, findings: [], skipped: true, checkedAt, checkedCount: 0 };
|
|
90
|
+
}
|
|
91
|
+
// Fast exit: nothing to check
|
|
92
|
+
if (packages.length === 0) {
|
|
93
|
+
return { blocked: false, findings: [], checkedAt, checkedCount: 0 };
|
|
94
|
+
}
|
|
95
|
+
// Build OSV batch request
|
|
96
|
+
const queries = packages.map((pkg) => ({
|
|
97
|
+
package: { name: pkg.name, ecosystem: "npm" },
|
|
98
|
+
version: pkg.version
|
|
99
|
+
}));
|
|
100
|
+
let osvData;
|
|
101
|
+
try {
|
|
102
|
+
const response = await fetchFn(OSV_BATCH_URL, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "Content-Type": "application/json" },
|
|
105
|
+
body: JSON.stringify({ queries }),
|
|
106
|
+
signal: AbortSignal.timeout(10_000)
|
|
107
|
+
});
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
throw new Error(`OSV API returned HTTP ${response.status}`);
|
|
110
|
+
}
|
|
111
|
+
osvData = (await response.json());
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
const apiError = err instanceof Error ? err.message : String(err);
|
|
115
|
+
if (policy.failClosed) {
|
|
116
|
+
return {
|
|
117
|
+
blocked: true,
|
|
118
|
+
reasonCode: "cve_api_error",
|
|
119
|
+
reason: `CVE check failed: OSV API error (failClosed:true). Error: ${apiError}`,
|
|
120
|
+
findings: [],
|
|
121
|
+
apiError,
|
|
122
|
+
checkedAt,
|
|
123
|
+
checkedCount: packages.length
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Fail-open: warn but do not block
|
|
127
|
+
return {
|
|
128
|
+
blocked: false,
|
|
129
|
+
findings: [],
|
|
130
|
+
apiError,
|
|
131
|
+
reason: `CVE check skipped (OSV API unavailable, failClosed:false): ${apiError}`,
|
|
132
|
+
checkedAt,
|
|
133
|
+
checkedCount: packages.length
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// Parse findings
|
|
137
|
+
const findings = [];
|
|
138
|
+
for (let i = 0; i < (osvData.results ?? []).length; i++) {
|
|
139
|
+
const pkg = packages[i];
|
|
140
|
+
const result = osvData.results[i];
|
|
141
|
+
for (const vuln of result?.vulns ?? []) {
|
|
142
|
+
const severity = severityFromString(vuln.database_specific?.severity);
|
|
143
|
+
findings.push({
|
|
144
|
+
vulnerabilityId: vuln.id,
|
|
145
|
+
severity,
|
|
146
|
+
packageName: pkg?.name ?? "unknown",
|
|
147
|
+
packageVersion: pkg?.version ?? "unknown",
|
|
148
|
+
summary: vuln.summary
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Apply threshold
|
|
153
|
+
const blockingFindings = findings.filter((f) => isSeverityAtOrAbove(f.severity, policy.blockSeverity));
|
|
154
|
+
if (blockingFindings.length > 0) {
|
|
155
|
+
const names = blockingFindings.map((f) => `${f.packageName}@${f.packageVersion} (${f.severity})`).join(", ");
|
|
156
|
+
return {
|
|
157
|
+
blocked: true,
|
|
158
|
+
reasonCode: "cve_blocked",
|
|
159
|
+
reason: `CVE check blocked patch: ${blockingFindings.length} ${policy.blockSeverity}+ advisory(ies) found: ${names}`,
|
|
160
|
+
findings,
|
|
161
|
+
checkedAt,
|
|
162
|
+
checkedCount: packages.length
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
blocked: false,
|
|
167
|
+
findings,
|
|
168
|
+
checkedAt,
|
|
169
|
+
checkedCount: packages.length
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=cve-check.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ApprovalPolicy } from "../../contracts/index.js";
|
|
2
|
+
export interface DigitalTwinSimulationInput {
|
|
3
|
+
scenarioId: string;
|
|
4
|
+
objective?: string;
|
|
5
|
+
changedFiles: string[];
|
|
6
|
+
verificationPlan: string[];
|
|
7
|
+
requestedSurfaces: string[];
|
|
8
|
+
allowedSurfaces: string[];
|
|
9
|
+
approvalPolicy?: ApprovalPolicy;
|
|
10
|
+
allowedPaths?: string[];
|
|
11
|
+
deniedPaths?: string[];
|
|
12
|
+
allowedNetworkDomains?: string[];
|
|
13
|
+
requiresHumanApproval?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface DigitalTwinSimulationReport {
|
|
16
|
+
scenarioId: string;
|
|
17
|
+
riskTier: "low" | "medium" | "high" | "critical";
|
|
18
|
+
blastRadiusScore: number;
|
|
19
|
+
rollbackConfidence: number;
|
|
20
|
+
protectedSurfaceTouched: boolean;
|
|
21
|
+
requiresIndependentVerification: boolean;
|
|
22
|
+
recommendedAction: "proceed" | "require_review" | "block";
|
|
23
|
+
reasons: string[];
|
|
24
|
+
allowedClaimWording: string;
|
|
25
|
+
nonClaims: string[];
|
|
26
|
+
}
|
|
27
|
+
export declare function runDigitalTwinSimulation(input: DigitalTwinSimulationInput): DigitalTwinSimulationReport;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { evaluateAutonomyEnvelopeV2 } from "../autonomy/envelope-v2.js";
|
|
2
|
+
import { analyzeLoopSurface } from "../surface-signals.js";
|
|
3
|
+
export function runDigitalTwinSimulation(input) {
|
|
4
|
+
const envelopeDecision = evaluateAutonomyEnvelopeV2({
|
|
5
|
+
taskId: input.scenarioId,
|
|
6
|
+
taskFamily: "digital-twin",
|
|
7
|
+
requestedSurfaces: input.requestedSurfaces,
|
|
8
|
+
allowedSurfaces: input.allowedSurfaces,
|
|
9
|
+
verificationPlan: input.verificationPlan,
|
|
10
|
+
commands: input.verificationPlan,
|
|
11
|
+
changedFiles: input.changedFiles,
|
|
12
|
+
allowedPaths: input.allowedPaths,
|
|
13
|
+
deniedPaths: input.deniedPaths,
|
|
14
|
+
allowedNetworkDomains: input.allowedNetworkDomains,
|
|
15
|
+
approvalPolicy: input.approvalPolicy,
|
|
16
|
+
requiresHumanApproval: input.requiresHumanApproval
|
|
17
|
+
});
|
|
18
|
+
const surfaceSignals = analyzeLoopSurface({
|
|
19
|
+
objective: input.objective,
|
|
20
|
+
verificationPlan: input.verificationPlan,
|
|
21
|
+
changedFiles: input.changedFiles
|
|
22
|
+
});
|
|
23
|
+
const protectedSurfaceTouched = input.changedFiles.some(isProtectedSurface);
|
|
24
|
+
const blastRadiusScore = clampScore(10 +
|
|
25
|
+
surfaceSignals.workspaceGraphRiskScore * 25 +
|
|
26
|
+
surfaceSignals.crossBoundaryRiskScore * 25 +
|
|
27
|
+
(protectedSurfaceTouched ? 20 : 0) +
|
|
28
|
+
(envelopeDecision.blockedSurfaces.includes("command") ? 20 : 0) +
|
|
29
|
+
(envelopeDecision.blockedSurfaces.includes("dependency") ? 15 : 0) +
|
|
30
|
+
(input.requiresHumanApproval ? 15 : 0));
|
|
31
|
+
const riskTier = classifyRiskTier(blastRadiusScore);
|
|
32
|
+
const rollbackConfidence = clampRatio(0.92 -
|
|
33
|
+
surfaceSignals.workspaceGraphRiskScore * 0.25 -
|
|
34
|
+
surfaceSignals.crossBoundaryRiskScore * 0.2 -
|
|
35
|
+
(protectedSurfaceTouched ? 0.15 : 0) -
|
|
36
|
+
(input.requiresHumanApproval ? 0.12 : 0) -
|
|
37
|
+
(envelopeDecision.blockedSurfaces.includes("command") ? 0.1 : 0));
|
|
38
|
+
const reasons = [...envelopeDecision.reasons];
|
|
39
|
+
if (protectedSurfaceTouched) {
|
|
40
|
+
reasons.push("protected release or deployment surface touched");
|
|
41
|
+
}
|
|
42
|
+
if (surfaceSignals.workspaceGraphRiskScore >= 0.5) {
|
|
43
|
+
reasons.push("workspace graph risk is elevated");
|
|
44
|
+
}
|
|
45
|
+
if (surfaceSignals.crossBoundaryRiskScore >= 0.5) {
|
|
46
|
+
reasons.push("cross-boundary change risk is elevated");
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
scenarioId: input.scenarioId,
|
|
50
|
+
riskTier,
|
|
51
|
+
blastRadiusScore,
|
|
52
|
+
rollbackConfidence,
|
|
53
|
+
protectedSurfaceTouched,
|
|
54
|
+
requiresIndependentVerification: riskTier === "high" || riskTier === "critical" || protectedSurfaceTouched,
|
|
55
|
+
recommendedAction: riskTier === "critical"
|
|
56
|
+
? "block"
|
|
57
|
+
: riskTier === "high" || envelopeDecision.decision === "escalate"
|
|
58
|
+
? "require_review"
|
|
59
|
+
: "proceed",
|
|
60
|
+
reasons,
|
|
61
|
+
allowedClaimWording: "High-risk runs are simulated in a pre-merge digital twin with blast-radius scoring, rollback confidence, and explicit review recommendations.",
|
|
62
|
+
nonClaims: ["perfect predictive safety", "simulation replaces human judgment"]
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function classifyRiskTier(score) {
|
|
66
|
+
if (score >= 80)
|
|
67
|
+
return "critical";
|
|
68
|
+
if (score >= 55)
|
|
69
|
+
return "high";
|
|
70
|
+
if (score >= 30)
|
|
71
|
+
return "medium";
|
|
72
|
+
return "low";
|
|
73
|
+
}
|
|
74
|
+
function isProtectedSurface(file) {
|
|
75
|
+
const normalized = file.replace(/\\/gu, "/").toLowerCase();
|
|
76
|
+
return (normalized.startsWith(".github/workflows/") ||
|
|
77
|
+
normalized.startsWith("deploy/") ||
|
|
78
|
+
normalized.startsWith("infra/") ||
|
|
79
|
+
normalized.includes("/migrations/") ||
|
|
80
|
+
normalized.endsWith("/package.json") ||
|
|
81
|
+
normalized === "package.json" ||
|
|
82
|
+
normalized === "pnpm-lock.yaml");
|
|
83
|
+
}
|
|
84
|
+
function clampScore(value) {
|
|
85
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
86
|
+
}
|
|
87
|
+
function clampRatio(value) {
|
|
88
|
+
return Math.max(0, Math.min(1, Math.round(value * 100) / 100));
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight adjacency-list graph used by scope pruning (SLICE-02).
|
|
3
|
+
* Nodes are file paths (or symbol names). Edges represent import/reference
|
|
4
|
+
* relationships: for each node, the array contains its direct dependents.
|
|
5
|
+
*/
|
|
6
|
+
export interface DriftGraph {
|
|
7
|
+
/** Complete set of known node identifiers (file paths or symbol names). */
|
|
8
|
+
nodes: Set<string>;
|
|
9
|
+
/** Adjacency list: node → list of directly connected nodes (1 hop). */
|
|
10
|
+
edges: Record<string, string[]>;
|
|
11
|
+
}
|
|
12
|
+
export interface DriftReport {
|
|
13
|
+
changedExports: string[];
|
|
14
|
+
affectedImporters: Array<{
|
|
15
|
+
file: string;
|
|
16
|
+
importedSymbol: string;
|
|
17
|
+
}>;
|
|
18
|
+
/** Importers with no test coverage for the changed symbol */
|
|
19
|
+
unvalidatedDownstream: string[];
|
|
20
|
+
/** true only if unvalidatedDownstream is empty */
|
|
21
|
+
safe: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface DriftInput {
|
|
24
|
+
/** Workspace root — ts-morph will glob all .ts files under it */
|
|
25
|
+
rootDir: string;
|
|
26
|
+
/** Path of the changed file, relative to rootDir (forward slashes) */
|
|
27
|
+
changedFile: string;
|
|
28
|
+
/** List of exported symbol names that changed */
|
|
29
|
+
changedExports: string[];
|
|
30
|
+
/** List of test file paths (absolute or relative to rootDir) that cover the changed symbols */
|
|
31
|
+
testFiles: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Builds a DriftReport by walking the workspace with ts-morph.
|
|
35
|
+
*
|
|
36
|
+
* Algorithm:
|
|
37
|
+
* 1. If changedExports is empty → safe: true immediately (no public API touched)
|
|
38
|
+
* 2. Walk all .ts files (excluding node_modules) looking for imports of any changed symbol
|
|
39
|
+
* 3. For each importer, check if a test file also imports the same changed symbol
|
|
40
|
+
* 4. Importers without test coverage → unvalidatedDownstream → safe: false
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildDriftReport(input: DriftInput): Promise<DriftReport>;
|
|
43
|
+
/**
|
|
44
|
+
* Builds and writes drift-report.json to the run output directory.
|
|
45
|
+
* Only writes if changedExports is non-empty (no drift = no report needed).
|
|
46
|
+
*/
|
|
47
|
+
export declare function emitDriftReport(input: DriftInput, runDir: string): Promise<DriftReport>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { Project } from "ts-morph";
|
|
4
|
+
// ─── Core builder ─────────────────────────────────────────────────────────────
|
|
5
|
+
/**
|
|
6
|
+
* Builds a DriftReport by walking the workspace with ts-morph.
|
|
7
|
+
*
|
|
8
|
+
* Algorithm:
|
|
9
|
+
* 1. If changedExports is empty → safe: true immediately (no public API touched)
|
|
10
|
+
* 2. Walk all .ts files (excluding node_modules) looking for imports of any changed symbol
|
|
11
|
+
* 3. For each importer, check if a test file also imports the same changed symbol
|
|
12
|
+
* 4. Importers without test coverage → unvalidatedDownstream → safe: false
|
|
13
|
+
*/
|
|
14
|
+
export async function buildDriftReport(input) {
|
|
15
|
+
if (input.changedExports.length === 0) {
|
|
16
|
+
return {
|
|
17
|
+
changedExports: [],
|
|
18
|
+
affectedImporters: [],
|
|
19
|
+
unvalidatedDownstream: [],
|
|
20
|
+
safe: true
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const project = new Project({
|
|
24
|
+
skipAddingFilesFromTsConfig: true
|
|
25
|
+
});
|
|
26
|
+
// Add all .ts files from rootDir (not node_modules, not .d.ts)
|
|
27
|
+
project.addSourceFilesAtPaths([
|
|
28
|
+
normalizePath(join(input.rootDir, "**/*.ts")),
|
|
29
|
+
`!${normalizePath(join(input.rootDir, "**/node_modules/**"))}`,
|
|
30
|
+
`!${normalizePath(join(input.rootDir, "**/*.d.ts"))}`
|
|
31
|
+
]);
|
|
32
|
+
const changedFilePath = normalizePath(join(input.rootDir, input.changedFile));
|
|
33
|
+
const affectedImporters = [];
|
|
34
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
35
|
+
const filePath = normalizePath(sourceFile.getFilePath());
|
|
36
|
+
// Skip the changed file itself
|
|
37
|
+
if (filePath === changedFilePath)
|
|
38
|
+
continue;
|
|
39
|
+
// Check import declarations for any of the changed symbols
|
|
40
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
41
|
+
const namedImports = importDecl.getNamedImports();
|
|
42
|
+
for (const namedImport of namedImports) {
|
|
43
|
+
const symbolName = namedImport.getName();
|
|
44
|
+
if (input.changedExports.includes(symbolName)) {
|
|
45
|
+
affectedImporters.push({
|
|
46
|
+
file: filePath,
|
|
47
|
+
importedSymbol: symbolName
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Determine which importers are test files (by path convention or explicit list)
|
|
54
|
+
const testFilePaths = new Set(input.testFiles.map(f => normalizePath(f.startsWith("/") || /^[A-Za-z]:/.test(f)
|
|
55
|
+
? f
|
|
56
|
+
: join(input.rootDir, f))));
|
|
57
|
+
// Also treat files matching *.test.ts or *.spec.ts as test files
|
|
58
|
+
const testSymbolsCovered = new Set();
|
|
59
|
+
for (const importer of affectedImporters) {
|
|
60
|
+
const isTestFile = testFilePaths.has(importer.file) ||
|
|
61
|
+
/\.(test|spec)\.[tj]s$/.test(importer.file);
|
|
62
|
+
if (isTestFile) {
|
|
63
|
+
testSymbolsCovered.add(importer.importedSymbol);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// unvalidatedDownstream = non-test importers whose symbol has no test coverage
|
|
67
|
+
const unvalidatedDownstream = [];
|
|
68
|
+
for (const importer of affectedImporters) {
|
|
69
|
+
const isTestFile = testFilePaths.has(importer.file) ||
|
|
70
|
+
/\.(test|spec)\.[tj]s$/.test(importer.file);
|
|
71
|
+
if (!isTestFile && !testSymbolsCovered.has(importer.importedSymbol)) {
|
|
72
|
+
if (!unvalidatedDownstream.includes(importer.file)) {
|
|
73
|
+
unvalidatedDownstream.push(importer.file);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
changedExports: input.changedExports,
|
|
79
|
+
affectedImporters,
|
|
80
|
+
unvalidatedDownstream,
|
|
81
|
+
safe: unvalidatedDownstream.length === 0
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// ─── Emitter ──────────────────────────────────────────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Builds and writes drift-report.json to the run output directory.
|
|
87
|
+
* Only writes if changedExports is non-empty (no drift = no report needed).
|
|
88
|
+
*/
|
|
89
|
+
export async function emitDriftReport(input, runDir) {
|
|
90
|
+
const report = await buildDriftReport(input);
|
|
91
|
+
if (input.changedExports.length > 0) {
|
|
92
|
+
await writeFile(join(runDir, "drift-report.json"), JSON.stringify(report, null, 2), "utf8");
|
|
93
|
+
}
|
|
94
|
+
return report;
|
|
95
|
+
}
|
|
96
|
+
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
97
|
+
function normalizePath(p) {
|
|
98
|
+
return p.replace(/\\/g, "/");
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=drift-graph.js.map
|