segreant 0.0.0-stage → 0.2.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 (349) hide show
  1. package/LICENSE +116 -0
  2. package/LICENSING.md +62 -0
  3. package/README.md +315 -2
  4. package/baselines/lift-baselines.json +22 -0
  5. package/bin/publication-lock.mjs +748 -0
  6. package/bin/runtime-snapshot.mjs +266 -0
  7. package/bin/segreant-statusline.mjs +20 -0
  8. package/bin/segreant.mjs +96 -0
  9. package/dist/alerts/detect.js +245 -0
  10. package/dist/alerts/notify.js +66 -0
  11. package/dist/alloc/apply.js +0 -0
  12. package/dist/alloc/epistemic.js +154 -0
  13. package/dist/alloc/exact.js +714 -0
  14. package/dist/alloc/rules.js +0 -0
  15. package/dist/billing/countermodels.js +123 -0
  16. package/dist/billing/epistemic.js +636 -0
  17. package/dist/billing/importer.js +75 -0
  18. package/dist/billing/mapping.js +239 -0
  19. package/dist/billing/openaiCosts.js +352 -0
  20. package/dist/billing/openaiCostsCoverage.js +108 -0
  21. package/dist/billing/readiness.js +68 -0
  22. package/dist/billing/reconcile.js +414 -0
  23. package/dist/billing/scope.js +68 -0
  24. package/dist/billing/types.js +180 -0
  25. package/dist/budget/allocate.js +100 -0
  26. package/dist/budget/capDecision.js +457 -0
  27. package/dist/budget/controlLock.js +71 -0
  28. package/dist/budget/enforceability.js +39 -0
  29. package/dist/budget/guard.js +313 -0
  30. package/dist/budget/onlineControl.js +404 -0
  31. package/dist/budget/recommend.js +153 -0
  32. package/dist/capital.js +220 -0
  33. package/dist/causal/assignment.js +179 -0
  34. package/dist/causal/design.js +161 -0
  35. package/dist/causal/epistemic.js +428 -0
  36. package/dist/causal/estimand.js +64 -0
  37. package/dist/causal/estimate.js +237 -0
  38. package/dist/causal/family.js +61 -0
  39. package/dist/causal/identity.js +18 -0
  40. package/dist/causal/inference-ledger.js +400 -0
  41. package/dist/causal/ledger.js +201 -0
  42. package/dist/causal/measurement.js +150 -0
  43. package/dist/causal/ope.js +297 -0
  44. package/dist/causal/precision.js +206 -0
  45. package/dist/causal/producer.js +774 -0
  46. package/dist/causal/protocol.js +962 -0
  47. package/dist/causal/qualification.js +260 -0
  48. package/dist/causal/records.js +416 -0
  49. package/dist/causal/registry.js +58 -0
  50. package/dist/causal/sequential.js +679 -0
  51. package/dist/causal/transport.js +138 -0
  52. package/dist/causal/types.js +10 -0
  53. package/dist/cli/allocCmd.js +340 -0
  54. package/dist/cli/askModelPrices.js +59 -0
  55. package/dist/cli/backupCmd.js +100 -0
  56. package/dist/cli/basisLines.js +45 -0
  57. package/dist/cli/billingCmd.js +1007 -0
  58. package/dist/cli/capitalCmd.js +65 -0
  59. package/dist/cli/causalCmd.js +340 -0
  60. package/dist/cli/connectCmd.js +466 -0
  61. package/dist/cli/controlCmd.js +341 -0
  62. package/dist/cli/diagnosticsCmd.js +41 -0
  63. package/dist/cli/economicCmd.js +384 -0
  64. package/dist/cli/egressCmd.js +248 -0
  65. package/dist/cli/evidenceCmd.js +157 -0
  66. package/dist/cli/featuresCmd.js +53 -0
  67. package/dist/cli/flags.js +128 -0
  68. package/dist/cli/homeCmd.js +117 -0
  69. package/dist/cli/importCmd.js +657 -0
  70. package/dist/cli/launchCmd.js +90 -0
  71. package/dist/cli/marketCmd.js +178 -0
  72. package/dist/cli/opsCmd.js +335 -0
  73. package/dist/cli/outcomeCmd.js +120 -0
  74. package/dist/cli/packCmd.js +172 -0
  75. package/dist/cli/planCmd.js +181 -0
  76. package/dist/cli/pluginCmd.js +145 -0
  77. package/dist/cli/pricingDeclareCmd.js +178 -0
  78. package/dist/cli/quotaCmd.js +190 -0
  79. package/dist/cli/retention.js +28 -0
  80. package/dist/cli/runCmd.js +591 -0
  81. package/dist/cli/showCmd.js +500 -0
  82. package/dist/cli/teamCmd.js +760 -0
  83. package/dist/cli/ui.js +128 -0
  84. package/dist/cli/valueCmd.js +1001 -0
  85. package/dist/cli.js +628 -0
  86. package/dist/config.js +677 -0
  87. package/dist/connect/antigravity.js +310 -0
  88. package/dist/connect/claudeCode.js +218 -0
  89. package/dist/connect/codex.js +320 -0
  90. package/dist/connect/connectors.js +285 -0
  91. package/dist/connect/importShared.js +283 -0
  92. package/dist/connect/opencode.js +214 -0
  93. package/dist/connect/protobuf.js +126 -0
  94. package/dist/connect/registry.js +46 -0
  95. package/dist/connect/repoRoot.js +112 -0
  96. package/dist/cost/basis.js +168 -0
  97. package/dist/cost/coverage.js +53 -0
  98. package/dist/cost/declarations.js +37 -0
  99. package/dist/cost/exactPricing.js +132 -0
  100. package/dist/cost/firstSight.js +98 -0
  101. package/dist/cost/overrides.js +230 -0
  102. package/dist/cost/pricing.js +997 -0
  103. package/dist/cost/reprice.js +130 -0
  104. package/dist/dashboard/claim-support.js +271 -0
  105. package/dist/dashboard/contracts.js +211 -0
  106. package/dist/dashboard/routes.js +2004 -0
  107. package/dist/dashboard/server.js +76 -0
  108. package/dist/dashboard/sessions.js +345 -0
  109. package/dist/dashboard/settings.js +109 -0
  110. package/dist/dashboard/shared-types.js +6 -0
  111. package/dist/dashboard/static.js +111 -0
  112. package/dist/dashboard/value-cache.js +105 -0
  113. package/dist/dashboard/web/app/components/claimInspector.js +138 -0
  114. package/dist/dashboard/web/app/components/drawer.js +284 -0
  115. package/dist/dashboard/web/app/components/ruleFields.js +144 -0
  116. package/dist/dashboard/web/app/components/spine.js +163 -0
  117. package/dist/dashboard/web/app/components/workflow.js +82 -0
  118. package/dist/dashboard/web/app/core/actions.js +1021 -0
  119. package/dist/dashboard/web/app/core/api.js +164 -0
  120. package/dist/dashboard/web/app/core/chain.js +40 -0
  121. package/dist/dashboard/web/app/core/claimLayers.js +392 -0
  122. package/dist/dashboard/web/app/core/claimTypes.js +222 -0
  123. package/dist/dashboard/web/app/core/dom.js +138 -0
  124. package/dist/dashboard/web/app/core/fmt.js +159 -0
  125. package/dist/dashboard/web/app/core/focus.js +10 -0
  126. package/dist/dashboard/web/app/core/generated-contract.js +211 -0
  127. package/dist/dashboard/web/app/core/generated-payload-contract.js +4579 -0
  128. package/dist/dashboard/web/app/core/generated-types.js +8 -0
  129. package/dist/dashboard/web/app/core/griffin.js +10 -0
  130. package/dist/dashboard/web/app/core/jobs.js +12 -0
  131. package/dist/dashboard/web/app/core/period.js +35 -0
  132. package/dist/dashboard/web/app/core/periodWords.js +10 -0
  133. package/dist/dashboard/web/app/core/periods.js +32 -0
  134. package/dist/dashboard/web/app/core/pricingActions.js +264 -0
  135. package/dist/dashboard/web/app/core/privacyText.js +78 -0
  136. package/dist/dashboard/web/app/core/reconcileWords.js +63 -0
  137. package/dist/dashboard/web/app/core/registry.js +337 -0
  138. package/dist/dashboard/web/app/core/signal.js +140 -0
  139. package/dist/dashboard/web/app/core/slug.js +9 -0
  140. package/dist/dashboard/web/app/core/theme.js +48 -0
  141. package/dist/dashboard/web/app/core/view.js +69 -0
  142. package/dist/dashboard/web/app/main.js +362 -0
  143. package/dist/dashboard/web/app/views/allocation.js +278 -0
  144. package/dist/dashboard/web/app/views/control.js +249 -0
  145. package/dist/dashboard/web/app/views/data.js +136 -0
  146. package/dist/dashboard/web/app/views/evidence.js +553 -0
  147. package/dist/dashboard/web/app/views/privacy.js +13 -0
  148. package/dist/dashboard/web/app/views/spend.js +143 -0
  149. package/dist/dashboard/web/app/views/statement.js +189 -0
  150. package/dist/dashboard/web/app/views/system.js +80 -0
  151. package/dist/dashboard/web/app/views/value.js +534 -0
  152. package/dist/dashboard/web/app/views/workbench.js +312 -0
  153. package/dist/dashboard/web/classic.html +2151 -0
  154. package/dist/dashboard/web/index.html +51 -0
  155. package/dist/dashboard/web/styles/app.css +1336 -0
  156. package/dist/decision/assurance.js +425 -0
  157. package/dist/decision/control.js +336 -0
  158. package/dist/decision/countermodels.js +118 -0
  159. package/dist/decision/engine.js +382 -0
  160. package/dist/decision/epistemic.js +546 -0
  161. package/dist/demo/seed.js +741 -0
  162. package/dist/diagnostics.js +236 -0
  163. package/dist/economics/attribution.js +201 -0
  164. package/dist/economics/close.js +133 -0
  165. package/dist/economics/corrections.js +120 -0
  166. package/dist/economics/epistemic.js +164 -0
  167. package/dist/economics/events.js +294 -0
  168. package/dist/economics/fx.js +245 -0
  169. package/dist/economics/ledger.js +1516 -0
  170. package/dist/economics/money.js +177 -0
  171. package/dist/economics/rate.js +331 -0
  172. package/dist/economics/request.js +89 -0
  173. package/dist/economics/serialization.js +99 -0
  174. package/dist/egress/policy.js +192 -0
  175. package/dist/egress/proxyHealth.js +36 -0
  176. package/dist/egress/receipts.js +871 -0
  177. package/dist/egress/ruleValidation.js +127 -0
  178. package/dist/egress/transport.js +395 -0
  179. package/dist/epistemic/abstract.js +432 -0
  180. package/dist/epistemic/admissibility.js +213 -0
  181. package/dist/epistemic/assumption.js +134 -0
  182. package/dist/epistemic/claim-uses.js +202 -0
  183. package/dist/epistemic/claim.js +264 -0
  184. package/dist/epistemic/countermodel.js +278 -0
  185. package/dist/epistemic/dag.js +421 -0
  186. package/dist/epistemic/derivation.js +391 -0
  187. package/dist/epistemic/evidence.js +254 -0
  188. package/dist/epistemic/grain.js +82 -0
  189. package/dist/epistemic/issuance-map.js +260 -0
  190. package/dist/epistemic/ledger.js +1489 -0
  191. package/dist/epistemic/obligation.js +149 -0
  192. package/dist/epistemic/preservation.js +178 -0
  193. package/dist/epistemic/profile.js +61 -0
  194. package/dist/epistemic/revocation.js +65 -0
  195. package/dist/epistemic/scope.js +66 -0
  196. package/dist/epistemic/serialization.js +171 -0
  197. package/dist/epistemic/state.js +46 -0
  198. package/dist/epistemic/time.js +37 -0
  199. package/dist/epistemic/ux.js +104 -0
  200. package/dist/epistemic/witness.js +176 -0
  201. package/dist/export/allocationCsv.js +70 -0
  202. package/dist/export/billingCsv.js +36 -0
  203. package/dist/export/csv.js +52 -0
  204. package/dist/export/economic.js +105 -0
  205. package/dist/export/focusBilling.js +66 -0
  206. package/dist/export/statementCsv.js +104 -0
  207. package/dist/git/completeness.js +78 -0
  208. package/dist/git/contribution.js +598 -0
  209. package/dist/git/correlate.js +286 -0
  210. package/dist/git/quality.js +467 -0
  211. package/dist/git/repoScope.js +207 -0
  212. package/dist/git/survivalStore.js +186 -0
  213. package/dist/githubActionsEvidence.js +234 -0
  214. package/dist/guide.js +159 -0
  215. package/dist/judge/call.js +161 -0
  216. package/dist/judge/orchestrate.js +127 -0
  217. package/dist/judge/payload.js +47 -0
  218. package/dist/judge/tier.js +107 -0
  219. package/dist/judge/transcript.js +324 -0
  220. package/dist/market/consensus.js +181 -0
  221. package/dist/market/market.js +323 -0
  222. package/dist/market/refresh.js +119 -0
  223. package/dist/market/sources.js +236 -0
  224. package/dist/market/zip.js +98 -0
  225. package/dist/measurement/completeness.js +125 -0
  226. package/dist/measurement/model.js +113 -0
  227. package/dist/measurement/registry.js +166 -0
  228. package/dist/measurement/surrogate.js +325 -0
  229. package/dist/outcomes/contract.js +107 -0
  230. package/dist/outcomes/registry.js +259 -0
  231. package/dist/outcomes/work-unit.js +42 -0
  232. package/dist/pack/canonical.js +63 -0
  233. package/dist/pack/export.js +120 -0
  234. package/dist/pack/index.js +5 -0
  235. package/dist/pack/manifest.js +77 -0
  236. package/dist/pack/signature.js +69 -0
  237. package/dist/pack/types.js +455 -0
  238. package/dist/pack/verifier.js +275 -0
  239. package/dist/plans/detect.js +147 -0
  240. package/dist/plugins/contract.js +497 -0
  241. package/dist/plugins/host.js +333 -0
  242. package/dist/plugins/index.js +4 -0
  243. package/dist/plugins/intake.js +137 -0
  244. package/dist/plugins/invocation.js +158 -0
  245. package/dist/plugins/isolation.js +203 -0
  246. package/dist/proxy/server.js +891 -0
  247. package/dist/proxy/stream-proposals.js +249 -0
  248. package/dist/proxy/usage.js +213 -0
  249. package/dist/quota/limits.js +138 -0
  250. package/dist/quota/statusline.js +68 -0
  251. package/dist/quota/view.js +122 -0
  252. package/dist/research/complexity/calibration.js +110 -0
  253. package/dist/research/complexity/lab.js +134 -0
  254. package/dist/research/complexity/profile.js +78 -0
  255. package/dist/research/economics/budgetRisk.js +100 -0
  256. package/dist/research/economics/capital.js +101 -0
  257. package/dist/research/economics/decisionLedger.js +103 -0
  258. package/dist/research/economics/decomposition.js +106 -0
  259. package/dist/research/economics/execution.js +42 -0
  260. package/dist/research/economics/frontier.js +113 -0
  261. package/dist/research/economics/metricSafety.js +74 -0
  262. package/dist/research/economics/offPolicy.js +120 -0
  263. package/dist/research/economics/promotion.js +85 -0
  264. package/dist/scan/knownApps.js +113 -0
  265. package/dist/scan/scan.js +220 -0
  266. package/dist/store/allocation.js +543 -0
  267. package/dist/store/backup.js +368 -0
  268. package/dist/store/billing.js +919 -0
  269. package/dist/store/causal.js +2547 -0
  270. package/dist/store/causalLineage.js +912 -0
  271. package/dist/store/causalProducer.js +462 -0
  272. package/dist/store/db.js +2726 -0
  273. package/dist/store/economicReadModel.js +270 -0
  274. package/dist/store/ope.js +67 -0
  275. package/dist/store/realization.js +379 -0
  276. package/dist/store/rows.js +52 -0
  277. package/dist/store/schema.js +2218 -0
  278. package/dist/store/scopeEvidence.js +67 -0
  279. package/dist/team/ledger-evidence.js +82 -0
  280. package/dist/team/rollup.js +496 -0
  281. package/dist/team/standardize.js +115 -0
  282. package/dist/util/json.js +24 -0
  283. package/dist/util/pool.js +19 -0
  284. package/dist/util/quiet.js +18 -0
  285. package/dist/util/resource-limits.js +113 -0
  286. package/dist/util/statements.js +61 -0
  287. package/dist/util/time.js +7 -0
  288. package/dist/util/transaction.js +68 -0
  289. package/dist/value/anytime.js +106 -0
  290. package/dist/value/characterization.js +77 -0
  291. package/dist/value/cohort.js +212 -0
  292. package/dist/value/drift.js +211 -0
  293. package/dist/value/epistemic.js +448 -0
  294. package/dist/value/frontier.js +513 -0
  295. package/dist/value/gates.js +276 -0
  296. package/dist/value/instrumentationSensitivity.js +76 -0
  297. package/dist/value/lenses.js +398 -0
  298. package/dist/value/lift.js +206 -0
  299. package/dist/value/liftBaseline.js +355 -0
  300. package/dist/value/liftEfficiency.js +68 -0
  301. package/dist/value/marginal.js +154 -0
  302. package/dist/value/proposals.js +263 -0
  303. package/dist/value/realization.js +1159 -0
  304. package/dist/value/receipt.js +222 -0
  305. package/dist/value/receiptReconciliation.js +117 -0
  306. package/dist/value/reliability.js +101 -0
  307. package/dist/value/report.js +338 -0
  308. package/dist/value/selfReported.js +224 -0
  309. package/dist/value/sourceDepth.js +25 -0
  310. package/dist/value/taskType.js +46 -0
  311. package/dist/value/timeReclaimed.js +90 -0
  312. package/dist/value/usage.js +281 -0
  313. package/dist/version.js +20 -0
  314. package/docs/ALLOCATION.md +263 -0
  315. package/docs/ARCHITECTURE.md +452 -0
  316. package/docs/BILLING-EVIDENCE-IMPORT.md +274 -0
  317. package/docs/CAPABILITY-EVIDENCE-CONTRACT.md +220 -0
  318. package/docs/CAUSAL-EVIDENCE-PROTOCOL.md +295 -0
  319. package/docs/CAUSAL-PRODUCER-CONTRACT.md +128 -0
  320. package/docs/CLI-JSON-CONTRACT.md +38 -0
  321. package/docs/COMPATIBILITY.md +64 -0
  322. package/docs/DATA-BOUNDARIES.md +300 -0
  323. package/docs/EVIDENCE-PROVENANCE.md +40 -0
  324. package/docs/FAQ.md +275 -0
  325. package/docs/FOCUS-COMPATIBILITY.md +15 -0
  326. package/docs/GETTING-STARTED.md +236 -0
  327. package/docs/GUIDE.md +830 -0
  328. package/docs/INTEGRATIONS.md +233 -0
  329. package/docs/MARKET-SOURCES.md +142 -0
  330. package/docs/METHODOLOGY.md +166 -0
  331. package/docs/NEUTRALITY.md +95 -0
  332. package/docs/PRIVACY.md +51 -0
  333. package/docs/PROVIDER-RECONCILIATION.md +260 -0
  334. package/docs/README.md +52 -0
  335. package/docs/REAL-PROVIDER-RECONCILIATION-RUNBOOK.md +44 -0
  336. package/docs/RELEASE-GATE.md +682 -0
  337. package/docs/RELEASE-PROCESS.md +68 -0
  338. package/docs/RELIABILITY-PERFORMANCE.md +149 -0
  339. package/docs/RETURN-ON-INTELLIGENCE.md +808 -0
  340. package/docs/SUPPORT.md +37 -0
  341. package/docs/THE-STANDARD.md +235 -0
  342. package/docs/THREAT-MODEL.md +114 -0
  343. package/market/consensus-fits.json +1 -0
  344. package/market/snapshot.json +1 -0
  345. package/package.json +79 -4
  346. package/pricing/models.json +1400 -0
  347. package/standalone/segreantpack-verifier.d.mts +61 -0
  348. package/standalone/segreantpack-verifier.mjs +715 -0
  349. package/web/assets/brand/mark-256.png +0 -0
@@ -0,0 +1,266 @@
1
+ // Immutable copy of the compiled runtime, used by the CLI launcher.
2
+ //
3
+ // The launcher has to release the publication gate as soon as the copy exists —
4
+ // holding it for the length of a command would starve every queued build — but
5
+ // the command it launched can go on resolving modules and reading bundled
6
+ // resources long after that. `segreant start` is the case that decides the shape
7
+ // of this module: it resolves its command promise the moment the proxy and
8
+ // dashboard sockets are listening, then serves for hours, and the dashboard
9
+ // reads the bundled pricing card per REQUEST rather than at import.
10
+ //
11
+ // So the snapshot's correctness condition is a lifetime, not a completion. It
12
+ // must outlive every process that imported it.
13
+ //
14
+ // The copy is keyed by a hash of its contents and shared by every launch of the
15
+ // same build. A fresh copy per process was correct but slow: on Windows the
16
+ // first open of each newly written module file costs about a second across the
17
+ // module graph, on every command. A content-keyed copy is never modified after
18
+ // it is complete, so sharing it cannot hand a process a half-published build: a
19
+ // new build hashes differently and gets its own directory.
20
+ //
21
+ // Each process holds a lease file inside the snapshot while it runs. Shared
22
+ // snapshots are created, leased and reaped only under one machine-wide
23
+ // snapshot lock. The package's own publication lock is not enough: two package
24
+ // roots with identical builds (a checkout and a copy of it) hash to the same
25
+ // snapshot but hold different publication locks, and a reaper in one could
26
+ // remove the snapshot another had just created and not yet leased. A snapshot
27
+ // is reaped when no live process leases it, it is not the current build's, and
28
+ // nothing has leased it for SHARED_IDLE_MS. A process killed mid-copy leaves an owner-stamped private
29
+ // directory that a later launcher reaps once its owner is demonstrably dead —
30
+ // never by pathname, which could delete a running server's module tree.
31
+ import { createHash } from 'node:crypto';
32
+ import {
33
+ cpSync,
34
+ existsSync,
35
+ mkdirSync,
36
+ mkdtempSync,
37
+ readdirSync,
38
+ readFileSync,
39
+ renameSync,
40
+ rmSync,
41
+ statSync,
42
+ utimesSync,
43
+ writeFileSync,
44
+ } from 'node:fs';
45
+ import { tmpdir } from 'node:os';
46
+ import { join } from 'node:path';
47
+ import { acquirePublicationLock, LOCK_STALE_MS, processIsAlive } from './publication-lock.mjs';
48
+
49
+ export const SNAPSHOT_PREFIX = 'segreant-runtime-';
50
+ export const SHARED_SNAPSHOT_PREFIX = `${SNAPSHOT_PREFIX}build-`;
51
+ export const SNAPSHOT_OWNER_FILE = 'owner.json';
52
+ export const SNAPSHOT_COMPLETE_FILE = 'complete.json';
53
+ export const SNAPSHOT_LEASE_DIR = 'leases';
54
+ // Deliberately outside SNAPSHOT_PREFIX, so the orphan reaper never touches it.
55
+ export const SNAPSHOT_LOCK_DIR = 'segreant-snapshot-lock';
56
+ // An unleased snapshot of another build survives this long after its last
57
+ // lease, so two checkouts used alternately do not re-copy on every command.
58
+ export const SHARED_IDLE_MS = 10 * 60_000;
59
+
60
+ // The compiled runtime resolves these relative to the PACKAGE root rather than
61
+ // to its own module: the bundled pricing card, the Lift baselines, the public
62
+ // market snapshot, and the package version. A snapshot without them is not behaviourally equivalent to
63
+ // the checked-out or installed layout, and the difference only surfaces at the
64
+ // first request that needs one.
65
+ const ROOT_RESOURCES = ['pricing', 'baselines', 'market', 'package.json'];
66
+
67
+ const REMOVE_OPTIONS = { recursive: true, force: true, maxRetries: 20, retryDelay: 25 };
68
+
69
+ function removeTree(path) {
70
+ try {
71
+ rmSync(path, REMOVE_OPTIONS);
72
+ return true;
73
+ } catch {
74
+ // Windows antivirus/indexer handles can briefly hold a just-copied file.
75
+ // A surviving snapshot is temp-directory residue that the reaper collects
76
+ // on a later run; it must never turn a completed command into a failure.
77
+ return false;
78
+ }
79
+ }
80
+
81
+ function readSnapshotOwner(path) {
82
+ try {
83
+ const owner = JSON.parse(readFileSync(join(path, SNAPSHOT_OWNER_FILE), 'utf8'));
84
+ if (!owner || typeof owner !== 'object') return null;
85
+ if (!Number.isInteger(owner.pid) || owner.pid <= 0) return null;
86
+ return { pid: owner.pid };
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ /** Content hash of everything a snapshot copies, in a stable order. */
93
+ export function runtimeKey(packageRoot) {
94
+ const hash = createHash('sha256');
95
+ const walk = (dir, rel) => {
96
+ const entries = readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
97
+ for (const entry of entries) {
98
+ const path = join(dir, entry.name);
99
+ const name = `${rel}/${entry.name}`;
100
+ if (entry.isDirectory()) walk(path, name);
101
+ else {
102
+ hash.update(`${name}\0`);
103
+ hash.update(readFileSync(path));
104
+ hash.update('\0');
105
+ }
106
+ }
107
+ };
108
+ for (const resource of ['dist', ...ROOT_RESOURCES]) {
109
+ const path = join(packageRoot, resource);
110
+ if (!existsSync(path)) continue;
111
+ if (statSync(path).isDirectory()) walk(path, resource);
112
+ else {
113
+ hash.update(`${resource}\0`);
114
+ hash.update(readFileSync(path));
115
+ hash.update('\0');
116
+ }
117
+ }
118
+ return hash.digest('hex').slice(0, 32);
119
+ }
120
+
121
+ function liveLeases(path) {
122
+ let names;
123
+ try {
124
+ names = readdirSync(join(path, SNAPSHOT_LEASE_DIR));
125
+ } catch {
126
+ return 0;
127
+ }
128
+ let live = 0;
129
+ for (const name of names) {
130
+ const pid = Number(name.replace(/\.json$/, ''));
131
+ if (processIsAlive(pid)) live++;
132
+ else rmSync(join(path, SNAPSHOT_LEASE_DIR, name), { force: true });
133
+ }
134
+ return live;
135
+ }
136
+
137
+ /** Time since the snapshot was last leased (its completion marker is touched on every lease). */
138
+ function idleFor(path) {
139
+ for (const candidate of [join(path, SNAPSHOT_COMPLETE_FILE), path]) {
140
+ try {
141
+ return Date.now() - statSync(candidate).mtimeMs;
142
+ } catch {
143
+ // Try the directory itself.
144
+ }
145
+ }
146
+ return Infinity;
147
+ }
148
+
149
+ /**
150
+ * Remove snapshots left behind by processes that are gone.
151
+ *
152
+ * Liveness, not age, is the primary test: a `segreant start` can legitimately own
153
+ * its snapshot for days, so an age-only reaper would delete a live server's
154
+ * runtime. Age only decides for a directory with no readable owner record,
155
+ * which can exist solely in the sliver between `mkdtemp` and the owner write.
156
+ *
157
+ * Shared build snapshots are reaped only when `currentKey` is given, which
158
+ * createRuntimeSnapshot does while holding the machine-wide snapshot lock: then
159
+ * no launcher, from any package root, can be between creating or leasing a
160
+ * snapshot and recording its lease.
161
+ */
162
+ export function reapOrphanRuntimeSnapshots(parent = tmpdir(), currentKey = null) {
163
+ let entries;
164
+ try {
165
+ entries = readdirSync(parent, { withFileTypes: true });
166
+ } catch {
167
+ return;
168
+ }
169
+ for (const entry of entries) {
170
+ if (!entry.isDirectory() || !entry.name.startsWith(SNAPSHOT_PREFIX)) continue;
171
+ const path = join(parent, entry.name);
172
+ if (entry.name.startsWith(SHARED_SNAPSHOT_PREFIX)) {
173
+ if (currentKey === null || entry.name === `${SHARED_SNAPSHOT_PREFIX}${currentKey}`) continue;
174
+ if (liveLeases(path) === 0 && idleFor(path) > SHARED_IDLE_MS) removeTree(path);
175
+ continue;
176
+ }
177
+ const owner = readSnapshotOwner(path);
178
+ if (owner) {
179
+ if (!processIsAlive(owner.pid)) removeTree(path);
180
+ continue;
181
+ }
182
+ try {
183
+ if (Date.now() - statSync(path).mtimeMs > LOCK_STALE_MS) removeTree(path);
184
+ } catch {
185
+ // A concurrent reaper won, or the directory vanished. Nothing to do.
186
+ }
187
+ }
188
+ }
189
+
190
+ function copyRuntime(packageRoot, root) {
191
+ cpSync(join(packageRoot, 'dist'), join(root, 'dist'), { recursive: true, force: true, errorOnExist: false });
192
+ for (const resource of ROOT_RESOURCES) {
193
+ const source = join(packageRoot, resource);
194
+ if (existsSync(source)) {
195
+ cpSync(source, join(root, resource), { recursive: true, force: true, errorOnExist: false });
196
+ }
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Lease the shared snapshot of the current build, creating it if needed.
202
+ *
203
+ * Call this while the package's publication lock is held: that makes the copy
204
+ * independent of a concurrent build. It takes the machine-wide snapshot lock
205
+ * itself, which keeps every reaper, from any package root, from removing a
206
+ * snapshot between its creation or lease and the recorded lease. The returned `dispose`
207
+ * releases the lease; it is idempotent and safe to register on process exit.
208
+ */
209
+ export function createRuntimeSnapshot(packageRoot, parent = tmpdir()) {
210
+ const key = runtimeKey(packageRoot);
211
+ const lockRoot = join(parent, SNAPSHOT_LOCK_DIR);
212
+ mkdirSync(lockRoot, { recursive: true });
213
+ const release = acquirePublicationLock(lockRoot);
214
+ try {
215
+ return leaseSharedSnapshot(packageRoot, parent, key);
216
+ } finally {
217
+ release?.();
218
+ }
219
+ }
220
+
221
+ function leaseSharedSnapshot(packageRoot, parent, key) {
222
+ const root = join(parent, `${SHARED_SNAPSHOT_PREFIX}${key}`);
223
+ if (!existsSync(join(root, SNAPSHOT_COMPLETE_FILE))) {
224
+ // An incomplete shared directory can only be residue of a failed rename.
225
+ if (existsSync(root)) removeTree(root);
226
+ const staging = mkdtempSync(join(parent, SNAPSHOT_PREFIX));
227
+ try {
228
+ // Stamp ownership before the expensive copy, so an interrupted creation is
229
+ // still reapable by liveness rather than having to age out.
230
+ writeFileSync(join(staging, SNAPSHOT_OWNER_FILE), JSON.stringify({ pid: process.pid }), 'utf8');
231
+ copyRuntime(packageRoot, staging);
232
+ writeFileSync(join(staging, SNAPSHOT_COMPLETE_FILE), JSON.stringify({ key }), 'utf8');
233
+ rmSync(join(staging, SNAPSHOT_OWNER_FILE), { force: true });
234
+ renameSync(staging, root);
235
+ } catch (error) {
236
+ removeTree(staging);
237
+ throw error;
238
+ }
239
+ }
240
+ const lease = join(root, SNAPSHOT_LEASE_DIR, `${process.pid}.json`);
241
+ mkdirSync(join(root, SNAPSHOT_LEASE_DIR), { recursive: true });
242
+ writeFileSync(lease, JSON.stringify({ pid: process.pid }), 'utf8');
243
+ const now = new Date();
244
+ try {
245
+ utimesSync(join(root, SNAPSHOT_COMPLETE_FILE), now, now);
246
+ } catch {
247
+ // The idle clock is advisory; a live lease is what protects the tree.
248
+ }
249
+ reapOrphanRuntimeSnapshots(parent, key);
250
+
251
+ let disposed = false;
252
+ return {
253
+ root,
254
+ key,
255
+ entry: join(root, 'dist', 'cli.js'),
256
+ dispose() {
257
+ if (disposed) return;
258
+ disposed = true;
259
+ try {
260
+ rmSync(lease, { force: true });
261
+ } catch {
262
+ // A lease left behind names a dead pid; the next reaper discards it.
263
+ }
264
+ },
265
+ };
266
+ }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ // Claude Code status line: `"statusLine": { "type": "command", "command": "segreant-statusline" }`.
3
+ // Deliberately bypasses the full launcher (re-exec, runtime snapshot, CLI load):
4
+ // a status line runs on every refresh and must answer in about a tenth of a
5
+ // second. Prints one line on stdout and always exits 0, so a failure here can
6
+ // never break the person's status bar.
7
+ import { readFileSync } from 'node:fs';
8
+
9
+ let line = 'segreant';
10
+ try {
11
+ let raw = '';
12
+ try { raw = readFileSync(0, 'utf8').slice(0, 1_000_000); } catch { raw = ''; }
13
+ const { dbPath } = await import('../dist/config.js');
14
+ const { statuslineLine } = await import('../dist/quota/statusline.js');
15
+ line = statuslineLine(dbPath(), raw, Date.now());
16
+ } catch {
17
+ line = 'segreant';
18
+ }
19
+ process.stdout.write(line + '\n');
20
+ process.exit(0);
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ // Thin launcher for the compiled runtime. Shipping JavaScript is intentional:
3
+ // Node does not permit TypeScript type stripping for files in node_modules.
4
+ //
5
+ // Node emits the "SQLite is an experimental feature" warning from an internal
6
+ // path that a userland process.emitWarning override can't intercept, so we
7
+ // re-exec ourselves once with --disable-warning to keep output clean. This is
8
+ // the only knob that reliably suppresses it for `npx segreant`.
9
+ import { spawn } from 'node:child_process';
10
+ import { acquirePublicationLock } from './publication-lock.mjs';
11
+ import { createRuntimeSnapshot, reapOrphanRuntimeSnapshots } from './runtime-snapshot.mjs';
12
+ import { fileURLToPath, pathToFileURL } from 'node:url';
13
+ import { dirname, join } from 'node:path';
14
+
15
+ // Fail fast with a human message on too-old Node before Node's SQLite runtime
16
+ // dependency reports a less actionable error.
17
+ const major = Number(process.versions.node.split('.')[0]);
18
+ if (major < 24) {
19
+ console.error(
20
+ `Segreant needs Node >= 24 (you have ${process.versions.node}).\n` +
21
+ `The packaged runtime targets Node 24 or newer.\n` +
22
+ `Upgrade Node from https://nodejs.org/ and re-run.`,
23
+ );
24
+ process.exit(1);
25
+ }
26
+
27
+ const self = fileURLToPath(import.meta.url);
28
+
29
+ if (!process.env.__SEGREANT_CHILD) {
30
+ // Asynchronous, so the launcher can forward signals. With spawnSync it could
31
+ // not: a SIGTERM to `segreant` (a supervisor, `kill <pid>`) ended the
32
+ // launcher and left a running `start` or dashboard behind as an orphan.
33
+ // Ctrl+C in a terminal worked only because it signals the whole group.
34
+ const child = spawn(
35
+ process.execPath,
36
+ ['--disable-warning=ExperimentalWarning', self, ...process.argv.slice(2)],
37
+ { stdio: 'inherit', env: { ...process.env, __SEGREANT_CHILD: '1' } },
38
+ );
39
+ for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
40
+ process.on(signal, () => { if (child.exitCode === null && child.signalCode === null) child.kill(signal); });
41
+ }
42
+ child.on('error', (error) => {
43
+ console.error(`Segreant could not start its runtime: ${error.message}`);
44
+ process.exit(1);
45
+ });
46
+ child.on('exit', (code, signal) => {
47
+ if (typeof code === 'number') process.exit(code);
48
+ // A child ended by a forwarded SIGINT/SIGTERM is the requested stop.
49
+ if (signal === 'SIGINT' || signal === 'SIGTERM' || signal === 'SIGHUP') process.exit(signal === 'SIGINT' ? 130 : 143);
50
+ console.error(`Segreant runtime terminated by signal ${signal ?? 'unknown'}`);
51
+ process.exit(1);
52
+ });
53
+ } else {
54
+ const here = dirname(self);
55
+ // Collect snapshots whose owning process is gone before adding another one,
56
+ // so a killed `segreant start` cannot make temp grow without bound.
57
+ reapOrphanRuntimeSnapshots();
58
+
59
+ // The launcher participates in the same exclusive gate as publication.
60
+ // Acquiring (rather than merely checking) the lock closes the race where a
61
+ // build starts immediately after a reader's old existsSync check. Lock
62
+ // failure is intentionally fatal: bypassing it would make a reader's artifact
63
+ // guarantee depend on an unverified filesystem assumption.
64
+ const release = acquirePublicationLock(join(here, '..'));
65
+ let snapshot;
66
+ try {
67
+ // Lease (and, for a new build, copy) while the publisher is excluded. Once
68
+ // the lock is released all module resolution and resource reading happens
69
+ // inside this immutable, content-keyed tree,
70
+ // so no later build can replace a dependency half-way through the import
71
+ // graph — or half-way through a request served hours later.
72
+ snapshot = createRuntimeSnapshot(join(here, '..'));
73
+ } finally {
74
+ release?.();
75
+ }
76
+
77
+ // Registered before the import so a failed import still releases the lease.
78
+ // The lease has to outlive the imported runtime, not merely the promise that
79
+ // runtime resolves when its deferred command work settles: `segreant start`
80
+ // settles that promise as soon as its sockets are listening and then serves
81
+ // for hours. Process exit is the only point at which no copied module or
82
+ // resource can still be needed.
83
+ process.on('exit', snapshot.dispose);
84
+
85
+ // Direct imports of dist/* and tools such as npm pack do not participate in
86
+ // this gate; the atomic reader guarantee is intentionally limited to the
87
+ // supported bin launcher and the build protocol.
88
+ const runtime = await import(pathToFileURL(snapshot.entry).href);
89
+ // The production CLI schedules dispatch with setImmediate so the launcher can
90
+ // release the publication lock before command work begins. Awaiting the
91
+ // completion promise keeps the launcher's own frame alive for the whole of a
92
+ // short command; it is no longer what decides when the snapshot dies.
93
+ if (runtime.cliCompletion && typeof runtime.cliCompletion.then === 'function') {
94
+ await runtime.cliCompletion;
95
+ }
96
+ }
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Proactive governance — turning passive measurement into alerts.
3
+ *
4
+ * A spend-governance tool that only blocks (silently) is half a product: the
5
+ * user finds out something went wrong by noticing their agent stalled, or by
6
+ * reading a dashboard they happened to open. This detects the conditions worth
7
+ * surfacing — budget pressure, spend spikes, runaway loops, throttling, value
8
+ * craters, untrustworthy pricing — from data already on the device. No external
9
+ * services, no notifications leaving the machine.
10
+ *
11
+ * `detectAlerts` is pure over precomputed inputs (testable without a store);
12
+ * `computeAlerts` is the store-backed wrapper the CLI and dashboard call.
13
+ */
14
+ import { startOfLocalDay } from '../budget/guard.js';
15
+ const SEVERITY_ORDER = { critical: 0, warn: 1, info: 2 };
16
+ function percentile(sorted, p) {
17
+ if (sorted.length === 0)
18
+ return 0;
19
+ const pos = (sorted.length - 1) * p;
20
+ const lo = Math.floor(pos);
21
+ const hi = Math.ceil(pos);
22
+ if (lo === hi)
23
+ return sorted[lo];
24
+ return sorted[lo] + (pos - lo) * (sorted[hi] - sorted[lo]);
25
+ }
26
+ function fmt(n) {
27
+ return n >= 1 ? n.toFixed(2) : n.toFixed(4);
28
+ }
29
+ export function alertCoverage(inp) {
30
+ const capConfigured = inp.dailyCapUsd !== null || inp.dailySoftUsd !== null;
31
+ // A request can only be blocked by something that blocks. A soft threshold
32
+ // warns and does not, so it does not light this channel.
33
+ const blockingConfigured = inp.dailyCapUsd !== null || inp.runaway !== null;
34
+ const baseline = percentile([...inp.baselineActiveDaySpends].sort((a, b) => a - b), 0.9);
35
+ const channels = [
36
+ {
37
+ channel: 'budget-cap',
38
+ live: capConfigured,
39
+ darkBecause: capConfigured ? null : 'no daily cap or soft threshold is set, so no budget alert can fire',
40
+ },
41
+ {
42
+ channel: 'runaway-loop',
43
+ live: inp.runaway !== null,
44
+ darkBecause: inp.runaway !== null ? null : 'no runaway velocity threshold is set, so no burst can trip it',
45
+ },
46
+ {
47
+ channel: 'throttling',
48
+ live: blockingConfigured,
49
+ darkBecause: blockingConfigured ? null : 'nothing is configured that would block a request, so none can be observed blocked',
50
+ },
51
+ {
52
+ channel: 'spend-spike',
53
+ live: baseline > 0,
54
+ // "yet" is a claim about the operator's history, and it is false on a
55
+ // ledger whose prior days Segreant deleted on their own retention policy
56
+ // (D-182). This is the surface whose whole job is to give the reason a
57
+ // channel is dark, so giving the wrong one here is worse than anywhere.
58
+ darkBecause: baseline > 0
59
+ ? null
60
+ : inp.baselineTruncated === true
61
+ ? 'the prior active days in this window were deleted by retention, so no baseline survives to compare against'
62
+ : 'no prior active day exists yet, so there is no baseline to exceed',
63
+ },
64
+ {
65
+ channel: 'value-crater',
66
+ live: inp.realizedSpendShare !== null,
67
+ darkBecause: inp.realizedSpendShare !== null ? null : 'realized value is uninstrumented, so no realization share exists to fall',
68
+ },
69
+ {
70
+ channel: 'pricing-trust',
71
+ live: (inp.pricedWindowSpendUsd ?? 0) > 0,
72
+ darkBecause: (inp.pricedWindowSpendUsd ?? 0) > 0
73
+ ? null
74
+ : 'no priced spend in the window, so an estimated share of zero reflects absent spend rather than verified pricing',
75
+ },
76
+ ];
77
+ const liveChannels = channels.filter((channel) => channel.live).length;
78
+ const total = channels.length;
79
+ const summary = liveChannels === total
80
+ ? `${liveChannels} of ${total} alert channels are watching`
81
+ : `${liveChannels} of ${total} alert channels are watching; the rest cannot fire as configured`;
82
+ return Object.freeze({
83
+ channels: Object.freeze(channels),
84
+ liveChannels,
85
+ complete: liveChannels === total,
86
+ summary,
87
+ });
88
+ }
89
+ export function detectAlerts(inp) {
90
+ const out = [];
91
+ // Budget pressure — critical once the hard cap is reached (requests now blocked).
92
+ const basisNote = inp.capExcludesImported ? ' The cap counts live proxy spend only; imported subscription spend is excluded.' : '';
93
+ if (inp.dailyCapUsd !== null && inp.todaySpendUsd >= inp.dailyCapUsd) {
94
+ out.push({
95
+ id: 'budget-exhausted',
96
+ severity: 'critical',
97
+ title: 'Daily budget reached',
98
+ detail: 'New requests are being blocked until the daily cap resets.' + basisNote,
99
+ metric: `$${fmt(inp.todaySpendUsd)} / $${fmt(inp.dailyCapUsd)}`,
100
+ });
101
+ }
102
+ else if (inp.dailySoftUsd !== null && inp.todaySpendUsd >= inp.dailySoftUsd) {
103
+ out.push({
104
+ id: 'budget-soft',
105
+ severity: 'warn',
106
+ title: 'Approaching daily cap',
107
+ detail: 'Spend has crossed the soft-warn threshold for today.' + basisNote,
108
+ metric: `$${fmt(inp.todaySpendUsd)} / $${fmt(inp.dailySoftUsd)} soft`,
109
+ });
110
+ }
111
+ // Runaway loop — a burst of spend in a short window.
112
+ if (inp.runaway && inp.runaway.tripped) {
113
+ out.push({
114
+ id: 'runaway',
115
+ severity: 'critical',
116
+ title: 'Runaway velocity',
117
+ detail: `A burst of spend in the last ${inp.runaway.windowSec}s tripped the runaway guard — check for a stuck agent loop.`,
118
+ metric: `$${fmt(inp.runaway.windowCostUsd)} in ${inp.runaway.windowSec}s`,
119
+ });
120
+ }
121
+ // Spend spike — today is well above the typical active day. Compared on TOTAL
122
+ // spend, because the baseline series is total (like-for-like regardless of basis).
123
+ const todayTotal = inp.todayTotalSpendUsd ?? inp.todaySpendUsd;
124
+ const base = percentile([...inp.baselineActiveDaySpends].sort((a, b) => a - b), 0.9);
125
+ if (base > 0 && todayTotal >= 0.01 && todayTotal > base * 2) {
126
+ out.push({
127
+ id: 'spend-spike',
128
+ severity: 'warn',
129
+ title: 'Spend spike',
130
+ // The alert still FIRES on a narrowed baseline, deliberately (D-173's
131
+ // rule, D-182's application): withholding a live overspend signal because
132
+ // a privacy setting shortened its comparison would withdraw the whole
133
+ // claim to repair half of it. What deletion undermines is the baseline's
134
+ // claim to represent a typical month, not the observation that today is
135
+ // far above what survives -- so the alert fires and the comparison says
136
+ // what it was computed over. No direction is asserted: deleting the
137
+ // oldest days can move a p90 either way.
138
+ detail: inp.baselineTruncated === true
139
+ ? `Today is well above the days that survive in this window — but retention deleted part of the comparison period, so this is not a month's typical. Compared against ${inp.baselineActiveDaySpends.length} surviving active day(s).`
140
+ : 'Today is well above your typical active day — worth a look before it compounds.',
141
+ metric: `${(todayTotal / base).toFixed(1)}× your p90 day ($${fmt(base)})`,
142
+ });
143
+ }
144
+ // Throttling — the user's agent is being blocked and may not realize it.
145
+ if (inp.blocked24h > 0) {
146
+ out.push({
147
+ id: 'throttled',
148
+ severity: 'warn',
149
+ title: 'Requests blocked by budget',
150
+ detail: 'Your agent is being throttled. Raise the cap or investigate what is driving the spend.',
151
+ metric: `${inp.blocked24h} blocked in 24h`,
152
+ });
153
+ }
154
+ // Value crater — spend isn't converting into kept outcomes (only when instrumented).
155
+ if (inp.realizedSpendShare !== null && inp.realizedSpendShare < 0.3) {
156
+ out.push({
157
+ id: 'value-crater',
158
+ severity: 'warn',
159
+ title: 'Low realized value',
160
+ detail: 'Most recent spend is not turning into kept, verified outcomes. Check the frontier for where it is leaking.',
161
+ metric: `${Math.round(inp.realizedSpendShare * 100)}% realized`,
162
+ });
163
+ }
164
+ // Data quality — a chunk of spend used estimated (unverified) pricing.
165
+ if (inp.estimatedShare > 0.2) {
166
+ out.push({
167
+ id: 'estimated-pricing',
168
+ severity: 'info',
169
+ title: 'Costs are approximate',
170
+ detail: 'Some spend used estimated pricing (an unrecognized model). Re-verify pricing/models.json before billing-grade use.',
171
+ metric: `${Math.round(inp.estimatedShare * 100)}% of last-7d spend estimated`,
172
+ });
173
+ }
174
+ // Prices the user set are not estimates of a list price, and not checked
175
+ // against one either; said apart from "approximate" so neither is mislabelled.
176
+ if ((inp.declaredShare ?? 0) > 0.2) {
177
+ out.push({
178
+ id: 'declared-pricing',
179
+ severity: 'info',
180
+ title: 'Some costs use prices you set',
181
+ detail: 'Part of recent spend is priced at prices set with segreant pricing set; they are not checked against a price list or your bill.',
182
+ metric: `${Math.round((inp.declaredShare ?? 0) * 100)}% of last-7d spend at prices you set`,
183
+ });
184
+ }
185
+ return out.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
186
+ }
187
+ /** Gather the inputs once, so alerts and their coverage are read from the same observation. */
188
+ function gatherAlertInputs(store, config, opts) {
189
+ const now = opts.now ?? Date.now();
190
+ const day = 24 * 60 * 60 * 1000;
191
+ const dayStart = startOfLocalDay(now);
192
+ // Cap-related alerts must read the same basis the guard ENFORCES on, or the
193
+ // dashboard would warn about a block that will never happen (or miss one that will).
194
+ const liveOnly = !config.budget.capIncludesImported;
195
+ const todaySpendUsd = store.spendBetween(dayStart, now + 1000, liveOnly);
196
+ // Baseline = prior active days (exclude today), so a spike compares like-for-like.
197
+ const baselineStartMs = now - 30 * day;
198
+ const priorSeries = store.series(baselineStartMs, dayStart, day);
199
+ const baselineActiveDaySpends = priorSeries.map((s) => s.costUsd).filter((x) => x > 0);
200
+ // Same predicate as every other window in this sweep: truncated when the
201
+ // window STARTS strictly before a recorded boundary, because `prune` deletes
202
+ // rows strictly older than it. Null is no prune on record (D-170).
203
+ const baselinePrunedBeforeMs = store.retentionFloor().requestsPrunedBeforeMs;
204
+ const baselineTruncated = baselinePrunedBeforeMs !== null && baselineStartMs < baselinePrunedBeforeMs;
205
+ const blocked24h = store.healthStats(now - day, now + 1000).blocked;
206
+ const week = store.healthStats(now - 7 * day, now + 1000);
207
+ const estimatedShare = week.totalCostUsd > 0 ? week.estimatedCostUsd / week.totalCostUsd : 0;
208
+ const declaredShare = week.totalCostUsd > 0 ? week.declaredCostUsd / week.totalCostUsd : 0;
209
+ // Carried alongside the share so coverage can tell an unestimated window from
210
+ // an empty one; the share alone cannot (D-141).
211
+ const pricedWindowSpendUsd = week.totalCostUsd;
212
+ let runaway = null;
213
+ if (config.budget.runawayMaxUsd !== null) {
214
+ const w = store.spendInWindow(now, config.budget.runawayWindowSec * 1000, liveOnly);
215
+ runaway = {
216
+ tripped: w.costUsd >= config.budget.runawayMaxUsd,
217
+ windowCostUsd: w.costUsd,
218
+ windowSec: config.budget.runawayWindowSec,
219
+ };
220
+ }
221
+ return Object.freeze({
222
+ todaySpendUsd,
223
+ todayTotalSpendUsd: liveOnly ? store.spendBetween(dayStart, now + 1000) : todaySpendUsd,
224
+ capExcludesImported: liveOnly,
225
+ dailyCapUsd: config.budget.dailyUsd,
226
+ dailySoftUsd: config.budget.dailySoftUsd,
227
+ baselineActiveDaySpends,
228
+ baselineTruncated,
229
+ baselinePrunedBeforeMs,
230
+ blocked24h,
231
+ estimatedShare,
232
+ declaredShare,
233
+ pricedWindowSpendUsd,
234
+ runaway,
235
+ realizedSpendShare: opts.realizedSpendShare ?? null,
236
+ });
237
+ }
238
+ /** Gather alert inputs from the store + config and detect. `realizedSpendShare` is passed in (git-gated). */
239
+ export function computeAlerts(store, config, opts = {}) {
240
+ return detectAlerts(gatherAlertInputs(store, config, opts));
241
+ }
242
+ /** The same inputs `computeAlerts` gathers, read for coverage rather than for alerts. */
243
+ export function computeAlertCoverage(store, config, opts = {}) {
244
+ return alertCoverage(gatherAlertInputs(store, config, opts));
245
+ }