martin-loop 0.1.5 → 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.
Files changed (274) hide show
  1. package/CODE_OF_CONDUCT.md +32 -0
  2. package/LICENSE +21 -21
  3. package/README.md +307 -398
  4. package/demo/seeded-workspace/README.md +35 -35
  5. package/demo/seeded-workspace/TASKS.md +29 -29
  6. package/demo/seeded-workspace/martin.config.yaml +11 -11
  7. package/demo/seeded-workspace/package.json +8 -8
  8. package/demo/seeded-workspace/src/invoice-summary.js +11 -11
  9. package/demo/seeded-workspace/test/invoice-summary.test.js +20 -20
  10. package/dist/bin/martin-loop.js +0 -0
  11. package/dist/vendor/adapters/counter.d.ts +1 -0
  12. package/dist/vendor/adapters/counter.js +4 -0
  13. package/dist/vendor/adapters/git-baseline.d.ts +50 -0
  14. package/dist/vendor/adapters/git-baseline.js +233 -0
  15. package/dist/vendor/adapters/openrouter-adapter.d.ts +15 -0
  16. package/dist/vendor/adapters/openrouter-adapter.js +302 -0
  17. package/dist/vendor/adapters/usage.d.ts +48 -0
  18. package/dist/vendor/adapters/usage.js +66 -0
  19. package/dist/vendor/cli/bin/exit.d.ts +12 -0
  20. package/dist/vendor/cli/bin/exit.js +28 -0
  21. package/dist/vendor/cli/commands/analyze.d.ts +5 -0
  22. package/dist/vendor/cli/commands/analyze.js +58 -0
  23. package/dist/vendor/cli/commands/audit-log-verify.d.ts +34 -0
  24. package/dist/vendor/cli/commands/audit-log-verify.js +99 -0
  25. package/dist/vendor/cli/commands/audit.d.ts +8 -0
  26. package/dist/vendor/cli/commands/audit.js +199 -0
  27. package/dist/vendor/cli/commands/corpus.d.ts +5 -0
  28. package/dist/vendor/cli/commands/corpus.js +60 -0
  29. package/dist/vendor/cli/commands/doctor.d.ts +8 -0
  30. package/dist/vendor/cli/commands/doctor.js +219 -0
  31. package/dist/vendor/cli/commands/explain.d.ts +17 -0
  32. package/dist/vendor/cli/commands/explain.js +176 -0
  33. package/dist/vendor/cli/commands/export.d.ts +5 -0
  34. package/dist/vendor/cli/commands/export.js +60 -0
  35. package/dist/vendor/cli/commands/governance.d.ts +8 -0
  36. package/dist/vendor/cli/commands/governance.js +95 -0
  37. package/dist/vendor/cli/commands/improve.d.ts +18 -0
  38. package/dist/vendor/cli/commands/improve.js +396 -0
  39. package/dist/vendor/cli/commands/init.d.ts +8 -0
  40. package/dist/vendor/cli/commands/init.js +281 -0
  41. package/dist/vendor/cli/commands/migration.d.ts +8 -0
  42. package/dist/vendor/cli/commands/migration.js +67 -0
  43. package/dist/vendor/cli/commands/prior.d.ts +23 -0
  44. package/dist/vendor/cli/commands/prior.js +145 -0
  45. package/dist/vendor/cli/commands/resume.d.ts +21 -0
  46. package/dist/vendor/cli/commands/resume.js +73 -0
  47. package/dist/vendor/cli/commands/verify.d.ts +6 -0
  48. package/dist/vendor/cli/commands/verify.js +43 -0
  49. package/dist/vendor/cli/research/public-corpus.d.ts +43 -0
  50. package/dist/vendor/cli/research/public-corpus.js +151 -0
  51. package/dist/vendor/cli/ui/error-card.d.ts +38 -0
  52. package/dist/vendor/cli/ui/error-card.js +103 -0
  53. package/dist/vendor/cli/ui/mission-brief.d.ts +41 -0
  54. package/dist/vendor/cli/ui/mission-brief.js +173 -0
  55. package/dist/vendor/cli/ui/summary-card.d.ts +34 -0
  56. package/dist/vendor/cli/ui/summary-card.js +102 -0
  57. package/dist/vendor/contracts/audit.d.ts +46 -0
  58. package/dist/vendor/contracts/audit.js +360 -0
  59. package/dist/vendor/contracts/post-phase15.d.ts +240 -0
  60. package/dist/vendor/contracts/post-phase15.js +166 -0
  61. package/dist/vendor/core/agent/mandates.d.ts +46 -0
  62. package/dist/vendor/core/agent/mandates.js +178 -0
  63. package/dist/vendor/core/agent/receipts.d.ts +38 -0
  64. package/dist/vendor/core/agent/receipts.js +131 -0
  65. package/dist/vendor/core/agent/signing.d.ts +17 -0
  66. package/dist/vendor/core/agent/signing.js +91 -0
  67. package/dist/vendor/core/attestation/sign.d.ts +25 -0
  68. package/dist/vendor/core/attestation/sign.js +216 -0
  69. package/dist/vendor/core/autonomy/autonomous-promotion.d.ts +120 -0
  70. package/dist/vendor/core/autonomy/autonomous-promotion.js +346 -0
  71. package/dist/vendor/core/autonomy/envelope-v2.d.ts +29 -0
  72. package/dist/vendor/core/autonomy/envelope-v2.js +60 -0
  73. package/dist/vendor/core/autonomy/envelope.d.ts +17 -0
  74. package/dist/vendor/core/autonomy/envelope.js +27 -0
  75. package/dist/vendor/core/autonomy/escalation-ledger.d.ts +20 -0
  76. package/dist/vendor/core/autonomy/escalation-ledger.js +18 -0
  77. package/dist/vendor/core/autonomy/resume.d.ts +15 -0
  78. package/dist/vendor/core/autonomy/resume.js +23 -0
  79. package/dist/vendor/core/circuit/circuit-breaker.d.ts +60 -0
  80. package/dist/vendor/core/circuit/circuit-breaker.js +143 -0
  81. package/dist/vendor/core/context-distillation.d.ts +3 -0
  82. package/dist/vendor/core/context-distillation.js +44 -0
  83. package/dist/vendor/core/context-flow/compile-context.d.ts +8 -0
  84. package/dist/vendor/core/context-flow/compile-context.js +111 -0
  85. package/dist/vendor/core/context-flow/entities.d.ts +2 -0
  86. package/dist/vendor/core/context-flow/entities.js +44 -0
  87. package/dist/vendor/core/context-flow/evaluate-policy.d.ts +2 -0
  88. package/dist/vendor/core/context-flow/evaluate-policy.js +42 -0
  89. package/dist/vendor/core/context-flow/index.d.ts +11 -0
  90. package/dist/vendor/core/context-flow/index.js +24 -0
  91. package/dist/vendor/core/context-flow/labels.d.ts +3 -0
  92. package/dist/vendor/core/context-flow/labels.js +17 -0
  93. package/dist/vendor/core/context-flow/normalizer.d.ts +9 -0
  94. package/dist/vendor/core/context-flow/normalizer.js +69 -0
  95. package/dist/vendor/core/context-flow/profiles.d.ts +33 -0
  96. package/dist/vendor/core/context-flow/profiles.js +36 -0
  97. package/dist/vendor/core/context-flow/redaction.d.ts +1 -0
  98. package/dist/vendor/core/context-flow/redaction.js +6 -0
  99. package/dist/vendor/core/context-flow/sensitivity.d.ts +2 -0
  100. package/dist/vendor/core/context-flow/sensitivity.js +27 -0
  101. package/dist/vendor/core/context-flow/sync-preview.d.ts +2 -0
  102. package/dist/vendor/core/context-flow/sync-preview.js +22 -0
  103. package/dist/vendor/core/context-flow/token-estimator.d.ts +3 -0
  104. package/dist/vendor/core/context-flow/token-estimator.js +13 -0
  105. package/dist/vendor/core/context-flow/types.d.ts +91 -0
  106. package/dist/vendor/core/context-flow/types.js +2 -0
  107. package/dist/vendor/core/context-utility.d.ts +47 -0
  108. package/dist/vendor/core/context-utility.js +405 -0
  109. package/dist/vendor/core/cost/pipeline.d.ts +92 -0
  110. package/dist/vendor/core/cost/pipeline.js +141 -0
  111. package/dist/vendor/core/cost/tagged-cost.d.ts +27 -0
  112. package/dist/vendor/core/cost/tagged-cost.js +55 -0
  113. package/dist/vendor/core/cost-governor.d.ts +2 -0
  114. package/dist/vendor/core/cost-governor.js +50 -0
  115. package/dist/vendor/core/cve/cve-check.d.ts +80 -0
  116. package/dist/vendor/core/cve/cve-check.js +172 -0
  117. package/dist/vendor/core/digital-twin/index.d.ts +27 -0
  118. package/dist/vendor/core/digital-twin/index.js +90 -0
  119. package/dist/vendor/core/drift/drift-graph.d.ts +47 -0
  120. package/dist/vendor/core/drift/drift-graph.js +100 -0
  121. package/dist/vendor/core/drift/objective-lock.d.ts +69 -0
  122. package/dist/vendor/core/drift/objective-lock.js +88 -0
  123. package/dist/vendor/core/drift/scope.d.ts +46 -0
  124. package/dist/vendor/core/drift/scope.js +102 -0
  125. package/dist/vendor/core/drift/signature-lock.d.ts +48 -0
  126. package/dist/vendor/core/drift/signature-lock.js +202 -0
  127. package/dist/vendor/core/drift/stale-proof-gate.d.ts +21 -0
  128. package/dist/vendor/core/drift/stale-proof-gate.js +19 -0
  129. package/dist/vendor/core/eval/known-bad-world-runner.d.ts +24 -0
  130. package/dist/vendor/core/eval/known-bad-world-runner.js +256 -0
  131. package/dist/vendor/core/evidence/claim-audit.d.ts +18 -0
  132. package/dist/vendor/core/evidence/claim-audit.js +89 -0
  133. package/dist/vendor/core/exit-intelligence.d.ts +2 -0
  134. package/dist/vendor/core/exit-intelligence.js +58 -0
  135. package/dist/vendor/core/explain/formatter.d.ts +42 -0
  136. package/dist/vendor/core/explain/formatter.js +171 -0
  137. package/dist/vendor/core/explain/timeline.d.ts +29 -0
  138. package/dist/vendor/core/explain/timeline.js +213 -0
  139. package/dist/vendor/core/failure-taxonomy.d.ts +2 -0
  140. package/dist/vendor/core/failure-taxonomy.js +76 -0
  141. package/dist/vendor/core/gateway/index.d.ts +10 -0
  142. package/dist/vendor/core/gateway/index.js +12 -0
  143. package/dist/vendor/core/gateway/registry.d.ts +40 -0
  144. package/dist/vendor/core/gateway/registry.js +97 -0
  145. package/dist/vendor/core/gateway/transport.d.ts +31 -0
  146. package/dist/vendor/core/gateway/transport.js +82 -0
  147. package/dist/vendor/core/gateway/vault.d.ts +19 -0
  148. package/dist/vendor/core/gateway/vault.js +29 -0
  149. package/dist/vendor/core/graph/adapters.d.ts +43 -0
  150. package/dist/vendor/core/graph/adapters.js +91 -0
  151. package/dist/vendor/core/graph/hotspots.d.ts +22 -0
  152. package/dist/vendor/core/graph/hotspots.js +30 -0
  153. package/dist/vendor/core/graph/index.d.ts +1 -0
  154. package/dist/vendor/core/graph/index.js +2 -0
  155. package/dist/vendor/core/honey/honey-tokens.d.ts +32 -0
  156. package/dist/vendor/core/honey/honey-tokens.js +44 -0
  157. package/dist/vendor/core/index.d.ts +2 -2
  158. package/dist/vendor/core/index.js +38 -12
  159. package/dist/vendor/core/learning/bayesian-update.d.ts +31 -0
  160. package/dist/vendor/core/learning/bayesian-update.js +60 -0
  161. package/dist/vendor/core/learning/prior-sets.d.ts +42 -0
  162. package/dist/vendor/core/learning/prior-sets.js +111 -0
  163. package/dist/vendor/core/learning/promotion-gate.d.ts +17 -0
  164. package/dist/vendor/core/learning/promotion-gate.js +23 -0
  165. package/dist/vendor/core/leash/blast-radius.d.ts +42 -0
  166. package/dist/vendor/core/leash/blast-radius.js +156 -0
  167. package/dist/vendor/core/leash/policy-leash.d.ts +31 -0
  168. package/dist/vendor/core/leash/policy-leash.js +117 -0
  169. package/dist/vendor/core/memo/memo.d.ts +63 -0
  170. package/dist/vendor/core/memo/memo.js +97 -0
  171. package/dist/vendor/core/memory/learning-pipeline.d.ts +154 -0
  172. package/dist/vendor/core/memory/learning-pipeline.js +391 -0
  173. package/dist/vendor/core/memory/palace.d.ts +84 -0
  174. package/dist/vendor/core/memory/palace.js +379 -0
  175. package/dist/vendor/core/merge/ast-merge.d.ts +22 -0
  176. package/dist/vendor/core/merge/ast-merge.js +350 -0
  177. package/dist/vendor/core/merge/text-merge.d.ts +12 -0
  178. package/dist/vendor/core/merge/text-merge.js +182 -0
  179. package/dist/vendor/core/otel/tracer.d.ts +45 -0
  180. package/dist/vendor/core/otel/tracer.js +116 -0
  181. package/dist/vendor/core/parallel/parallel-attempts.d.ts +28 -0
  182. package/dist/vendor/core/parallel/parallel-attempts.js +41 -0
  183. package/dist/vendor/core/parallel/scorer.d.ts +24 -0
  184. package/dist/vendor/core/parallel/scorer.js +65 -0
  185. package/dist/vendor/core/pattern-detection.d.ts +64 -0
  186. package/dist/vendor/core/pattern-detection.js +108 -0
  187. package/dist/vendor/core/persistence/checkpoint.d.ts +44 -0
  188. package/dist/vendor/core/persistence/checkpoint.js +156 -0
  189. package/dist/vendor/core/persistence/cleanup.d.ts +22 -0
  190. package/dist/vendor/core/persistence/cleanup.js +131 -0
  191. package/dist/vendor/core/persistence/index.d.ts +2 -0
  192. package/dist/vendor/core/persistence/index.js +1 -0
  193. package/dist/vendor/core/persistence/runs-reader.d.ts +52 -0
  194. package/dist/vendor/core/persistence/runs-reader.js +84 -0
  195. package/dist/vendor/core/persistence/store.d.ts +6 -1
  196. package/dist/vendor/core/persistence/store.js +5 -0
  197. package/dist/vendor/core/policy/file-touch-quota.d.ts +60 -0
  198. package/dist/vendor/core/policy/file-touch-quota.js +105 -0
  199. package/dist/vendor/core/policy/policy-loader.d.ts +30 -0
  200. package/dist/vendor/core/policy/policy-loader.js +170 -0
  201. package/dist/vendor/core/policy/policy-schema.d.ts +55 -0
  202. package/dist/vendor/core/policy/policy-schema.js +78 -0
  203. package/dist/vendor/core/probe/probe.d.ts +49 -0
  204. package/dist/vendor/core/probe/probe.js +115 -0
  205. package/dist/vendor/core/proof/patch-proof.d.ts +58 -0
  206. package/dist/vendor/core/proof/patch-proof.js +84 -0
  207. package/dist/vendor/core/proof/semantic-probe.d.ts +25 -0
  208. package/dist/vendor/core/proof/semantic-probe.js +82 -0
  209. package/dist/vendor/core/recovery/failure-mode-runner.d.ts +29 -0
  210. package/dist/vendor/core/recovery/failure-mode-runner.js +39 -0
  211. package/dist/vendor/core/red-blue/red-phase.d.ts +64 -0
  212. package/dist/vendor/core/red-blue/red-phase.js +141 -0
  213. package/dist/vendor/core/red-blue/risk-tiers.d.ts +22 -0
  214. package/dist/vendor/core/red-blue/risk-tiers.js +33 -0
  215. package/dist/vendor/core/replay/replay.d.ts +85 -0
  216. package/dist/vendor/core/replay/replay.js +109 -0
  217. package/dist/vendor/core/router/engine.d.ts +54 -0
  218. package/dist/vendor/core/router/engine.js +131 -0
  219. package/dist/vendor/core/router/index.d.ts +1 -0
  220. package/dist/vendor/core/router/index.js +2 -0
  221. package/dist/vendor/core/router/trust-calibration.d.ts +57 -0
  222. package/dist/vendor/core/router/trust-calibration.js +127 -0
  223. package/dist/vendor/core/run-martin.d.ts +2 -0
  224. package/dist/vendor/core/run-martin.js +287 -0
  225. package/dist/vendor/core/security/cve-scanner.d.ts +62 -0
  226. package/dist/vendor/core/security/cve-scanner.js +178 -0
  227. package/dist/vendor/core/sentinel/efficiency-sentinel.d.ts +29 -0
  228. package/dist/vendor/core/sentinel/efficiency-sentinel.js +30 -0
  229. package/dist/vendor/core/sentinel/progress-guard.d.ts +35 -0
  230. package/dist/vendor/core/sentinel/progress-guard.js +46 -0
  231. package/dist/vendor/core/siem/siem-emitter.d.ts +49 -0
  232. package/dist/vendor/core/siem/siem-emitter.js +157 -0
  233. package/dist/vendor/core/strategy/attempt-brief.d.ts +22 -0
  234. package/dist/vendor/core/strategy/attempt-brief.js +89 -0
  235. package/dist/vendor/core/summarize/diff-summary.d.ts +35 -0
  236. package/dist/vendor/core/summarize/diff-summary.js +204 -0
  237. package/dist/vendor/core/surface-signals.d.ts +21 -0
  238. package/dist/vendor/core/surface-signals.js +139 -0
  239. package/dist/vendor/core/truth/truth-wall.d.ts +51 -0
  240. package/dist/vendor/core/truth/truth-wall.js +69 -0
  241. package/dist/vendor/core/truth-spine.d.ts +26 -0
  242. package/dist/vendor/core/truth-spine.js +62 -0
  243. package/dist/vendor/core/types.d.ts +115 -0
  244. package/dist/vendor/core/types.js +2 -0
  245. package/dist/vendor/core/verification/tiered-verify.d.ts +17 -0
  246. package/dist/vendor/core/verification/tiered-verify.js +29 -0
  247. package/dist/vendor/core/verifier-pyramid.d.ts +32 -0
  248. package/dist/vendor/core/verifier-pyramid.js +111 -0
  249. package/dist/vendor/core/workflow-artifacts.d.ts +99 -0
  250. package/dist/vendor/core/workflow-artifacts.js +668 -0
  251. package/dist/vendor/core/wrap/supervised-run.d.ts +96 -0
  252. package/dist/vendor/core/wrap/supervised-run.js +178 -0
  253. package/docs/assets/cli-animated.svg +139 -0
  254. package/docs/assets/cli-static.svg +34 -0
  255. package/docs/assets/github-hero-v2.svg +23 -0
  256. package/docs/assets/martin-raplph.png.jpg +0 -0
  257. package/docs/assets/martinloop-logo.png +0 -0
  258. package/docs/assets/nvidia-inception-program-light.png +0 -0
  259. package/docs/assets/nvidia-inception-program.png +0 -0
  260. package/docs/assets/phase3c-sidesidebyside-demo.html +228 -0
  261. package/docs/assets/side-by-side.svg +134 -0
  262. package/docs/oss/CLAUDE-CODE-WALKTHROUGH.md +142 -142
  263. package/docs/oss/EXAMPLES.md +134 -134
  264. package/docs/oss/OSS-BOUNDARY-REPORT.json +1 -1
  265. package/docs/oss/OSS-BOUNDARY-REPORT.md +1 -1
  266. package/docs/oss/QUICKSTART.md +170 -165
  267. package/docs/oss/RALPH-LOOP-SAFETY.md +113 -113
  268. package/docs/oss/README.md +96 -96
  269. package/docs/oss/RELEASE-SURFACE-REPORT.json +2 -1
  270. package/docs/oss/RELEASE-SURFACE-REPORT.md +2 -1
  271. package/package.json +130 -58
  272. package/docs/distribution/DIRECTORY-SUBMISSIONS.md +0 -89
  273. package/docs/distribution/INTEGRATION-OUTREACH.md +0 -61
  274. package/docs/distribution/UNDER-3-CHALLENGE.md +0 -65
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Composable cost-aware LLM pipeline.
3
+ *
4
+ * Combines four patterns into one composable function:
5
+ * 1. Model routing by task complexity (Haiku vs Sonnet)
6
+ * 2. Immutable cost tracking with budget enforcement
7
+ * 3. Narrow retry logic (transient errors only, exponential backoff)
8
+ * 4. Prompt caching via Anthropic cache_control messages
9
+ *
10
+ * Does NOT import @anthropic-ai/sdk — uses an injected LlmClient interface
11
+ * so it works with the claude-cli adapter or any HTTP client.
12
+ */
13
+ // ---------------------------------------------------------------------------
14
+ // Model constants
15
+ // ---------------------------------------------------------------------------
16
+ export const MODEL_HAIKU = "claude-haiku-4-5-20251001";
17
+ export const MODEL_SONNET = "claude-sonnet-4-6";
18
+ const COMPLEXITY_CHAR_THRESHOLD = 10_000;
19
+ const COMPLEXITY_ITEM_THRESHOLD = 30;
20
+ // Private pricing for budget preflight estimation only.
21
+ // Canonical pricing lives in packages/adapters/src/usage.ts.
22
+ const PIPELINE_PRICING = {
23
+ [MODEL_HAIKU]: { inputPer1K: 0.00025, outputPer1K: 0.00125 },
24
+ [MODEL_SONNET]: { inputPer1K: 0.003, outputPer1K: 0.015 }
25
+ };
26
+ const FALLBACK_PRICING = { inputPer1K: 0.003, outputPer1K: 0.015 };
27
+ // ---------------------------------------------------------------------------
28
+ // 1. Model routing
29
+ // ---------------------------------------------------------------------------
30
+ /**
31
+ * Select the cheapest model that can handle the task complexity.
32
+ * Returns forceModel unchanged when provided.
33
+ */
34
+ export function selectModel(textLength, itemCount, forceModel) {
35
+ if (forceModel !== undefined)
36
+ return forceModel;
37
+ if (textLength >= COMPLEXITY_CHAR_THRESHOLD || itemCount >= COMPLEXITY_ITEM_THRESHOLD) {
38
+ return MODEL_SONNET;
39
+ }
40
+ return MODEL_HAIKU;
41
+ }
42
+ export function createCostTracker(budgetLimitUsd) {
43
+ return makeFrozenTracker(budgetLimitUsd, []);
44
+ }
45
+ export function addCostRecord(tracker, record) {
46
+ const full = Object.freeze({
47
+ ...record,
48
+ timestamp: new Date().toISOString()
49
+ });
50
+ return makeFrozenTracker(tracker.budgetLimitUsd, [...tracker.records, full]);
51
+ }
52
+ function makeFrozenTracker(budgetLimitUsd, records) {
53
+ const totalCost = records.reduce((sum, r) => sum + r.costUsd, 0);
54
+ return Object.freeze({
55
+ budgetLimitUsd,
56
+ records: Object.freeze(records),
57
+ totalCost,
58
+ overBudget: totalCost > budgetLimitUsd
59
+ });
60
+ }
61
+ function classifyError(err) {
62
+ const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
63
+ if (/econnreset|etimedout|enotfound|econnrefused|socket hang up/.test(msg))
64
+ return "retryable";
65
+ if (/429|rate.limit|too.many.requests/.test(msg))
66
+ return "retryable";
67
+ if (/5\d{2}|internal.server.error|overloaded/.test(msg))
68
+ return "retryable";
69
+ return "fatal";
70
+ }
71
+ export async function callWithRetry(fn, options = {}) {
72
+ const maxRetries = options.maxRetries ?? 3;
73
+ const baseDelayMs = options.baseDelayMs ?? 1_000;
74
+ let lastErr;
75
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
76
+ try {
77
+ return await fn();
78
+ }
79
+ catch (err) {
80
+ if (classifyError(err) === "fatal")
81
+ throw err;
82
+ lastErr = err;
83
+ if (attempt < maxRetries - 1) {
84
+ const delay = Math.min(baseDelayMs * Math.pow(2, attempt), 32_000);
85
+ await new Promise((resolve) => setTimeout(resolve, delay));
86
+ }
87
+ }
88
+ }
89
+ throw lastErr;
90
+ }
91
+ export function buildCachedMessages(systemPrompt, userInput) {
92
+ const systemMsg = Object.freeze({
93
+ role: "system",
94
+ content: Object.freeze([
95
+ Object.freeze({
96
+ type: "text",
97
+ text: systemPrompt,
98
+ cache_control: Object.freeze({ type: "ephemeral" })
99
+ })
100
+ ])
101
+ });
102
+ const userMsg = Object.freeze({
103
+ role: "user",
104
+ content: userInput
105
+ });
106
+ return Object.freeze([systemMsg, userMsg]);
107
+ }
108
+ export class BudgetExceededError extends Error {
109
+ totalCost;
110
+ budgetLimit;
111
+ constructor(totalCost, budgetLimit) {
112
+ super(`Budget exceeded: spent $${totalCost.toFixed(4)} of $${budgetLimit.toFixed(4)}`);
113
+ this.totalCost = totalCost;
114
+ this.budgetLimit = budgetLimit;
115
+ this.name = "BudgetExceededError";
116
+ }
117
+ }
118
+ export async function runCostAwarePipeline(input) {
119
+ const { text, config, tracker, client, systemPrompt } = input;
120
+ const itemCount = text.split("\n").length;
121
+ const model = selectModel(text.length, itemCount, config?.forceModel);
122
+ // Preflight budget check: estimate cost before spending
123
+ const estimatedInputTokens = Math.ceil(text.length / 3.5);
124
+ const estimatedOutputTokens = 500;
125
+ const pricing = PIPELINE_PRICING[model] ?? FALLBACK_PRICING;
126
+ const estimatedCost = (estimatedInputTokens / 1000) * pricing.inputPer1K +
127
+ (estimatedOutputTokens / 1000) * pricing.outputPer1K;
128
+ if (tracker.totalCost + estimatedCost > tracker.budgetLimitUsd) {
129
+ throw new BudgetExceededError(tracker.totalCost, tracker.budgetLimitUsd);
130
+ }
131
+ const messages = buildCachedMessages(systemPrompt, text);
132
+ const result = await callWithRetry(() => client.call(model, messages, { maxTokens: config?.maxTokens }), { maxRetries: config?.maxRetries, baseDelayMs: config?.baseDelayMs });
133
+ const updatedTracker = addCostRecord(tracker, {
134
+ model,
135
+ inputTokens: result.inputTokens,
136
+ outputTokens: result.outputTokens,
137
+ costUsd: result.costUsd
138
+ });
139
+ return { result, tracker: updatedTracker };
140
+ }
141
+ //# sourceMappingURL=pipeline.js.map
@@ -0,0 +1,27 @@
1
+ export interface TaggedCostEntry {
2
+ runId: string;
3
+ tags: Record<string, string>;
4
+ totalUsd: number;
5
+ inputTokens: number;
6
+ outputTokens: number;
7
+ timestamp: string;
8
+ }
9
+ export interface TaggedCostSummary {
10
+ tag: string;
11
+ value: string;
12
+ totalUsd: number;
13
+ runCount: number;
14
+ entries: TaggedCostEntry[];
15
+ }
16
+ /**
17
+ * Groups cost entries by the value of a specific tag key.
18
+ * Entries missing the tag key are grouped under "(untagged)".
19
+ * Results are sorted descending by totalUsd.
20
+ */
21
+ export declare function aggregateCostsByTag(entries: TaggedCostEntry[], tagKey: string): TaggedCostSummary[];
22
+ /**
23
+ * Formats a list of TaggedCostSummary objects as a plain-text table.
24
+ * Columns: tag value | runs | total USD
25
+ * Sorted descending by totalUsd (aggregateCostsByTag already sorts).
26
+ */
27
+ export declare function formatCostReport(summaries: TaggedCostSummary[]): string;
@@ -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,2 @@
1
+ import type { CostGovernorInput, CostGovernorState } from "./types.js";
2
+ export declare function evaluateCostGovernor(input: CostGovernorInput): CostGovernorState;
@@ -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