modelrig 0.0.1 → 0.1.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 (220) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +115 -8
  3. package/dist/adapters/anthropic.d.ts +46 -0
  4. package/dist/adapters/anthropic.d.ts.map +1 -0
  5. package/dist/adapters/anthropic.js +202 -0
  6. package/dist/adapters/deepseek.d.ts +41 -0
  7. package/dist/adapters/deepseek.d.ts.map +1 -0
  8. package/dist/adapters/deepseek.js +163 -0
  9. package/dist/adapters/gemini.d.ts +44 -0
  10. package/dist/adapters/gemini.d.ts.map +1 -0
  11. package/dist/adapters/gemini.js +222 -0
  12. package/dist/adapters/grok.d.ts +44 -0
  13. package/dist/adapters/grok.d.ts.map +1 -0
  14. package/dist/adapters/grok.js +168 -0
  15. package/dist/adapters/openai-compat.d.ts +74 -0
  16. package/dist/adapters/openai-compat.d.ts.map +1 -0
  17. package/dist/adapters/openai-compat.js +187 -0
  18. package/dist/adapters/openai.d.ts +49 -0
  19. package/dist/adapters/openai.d.ts.map +1 -0
  20. package/dist/adapters/openai.js +222 -0
  21. package/dist/bakeoff/engine.d.ts +52 -0
  22. package/dist/bakeoff/engine.d.ts.map +1 -0
  23. package/dist/bakeoff/engine.js +163 -0
  24. package/dist/bakeoff/replay.d.ts +52 -0
  25. package/dist/bakeoff/replay.d.ts.map +1 -0
  26. package/dist/bakeoff/replay.js +94 -0
  27. package/dist/bakeoff/samples.d.ts +62 -0
  28. package/dist/bakeoff/samples.d.ts.map +1 -0
  29. package/dist/bakeoff/samples.js +128 -0
  30. package/dist/bakeoff/stats.d.ts +13 -0
  31. package/dist/bakeoff/stats.d.ts.map +1 -0
  32. package/dist/bakeoff/stats.js +48 -0
  33. package/dist/bakeoff/types.d.ts +37 -0
  34. package/dist/bakeoff/types.d.ts.map +1 -0
  35. package/dist/bakeoff/types.js +10 -0
  36. package/dist/cli.d.ts +27 -0
  37. package/dist/cli.d.ts.map +1 -0
  38. package/dist/cli.js +572 -0
  39. package/dist/config.d.ts +104 -0
  40. package/dist/config.d.ts.map +1 -0
  41. package/dist/config.js +148 -0
  42. package/dist/feedback/coverage.d.ts +32 -0
  43. package/dist/feedback/coverage.d.ts.map +1 -0
  44. package/dist/feedback/coverage.js +95 -0
  45. package/dist/feedback/feedback.d.ts +35 -0
  46. package/dist/feedback/feedback.d.ts.map +1 -0
  47. package/dist/feedback/feedback.js +39 -0
  48. package/dist/index.d.ts +155 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +448 -0
  51. package/dist/init/finder.d.ts +79 -0
  52. package/dist/init/finder.d.ts.map +1 -0
  53. package/dist/init/finder.js +297 -0
  54. package/dist/init/run.d.ts +99 -0
  55. package/dist/init/run.d.ts.map +1 -0
  56. package/dist/init/run.js +281 -0
  57. package/dist/init/signatures.d.ts +39 -0
  58. package/dist/init/signatures.d.ts.map +1 -0
  59. package/dist/init/signatures.js +116 -0
  60. package/dist/matrix/aggregate.d.ts +89 -0
  61. package/dist/matrix/aggregate.d.ts.map +1 -0
  62. package/dist/matrix/aggregate.js +186 -0
  63. package/dist/query/grammar.d.ts +35 -0
  64. package/dist/query/grammar.d.ts.map +1 -0
  65. package/dist/query/grammar.js +279 -0
  66. package/dist/query/run.d.ts +8 -0
  67. package/dist/query/run.d.ts.map +1 -0
  68. package/dist/query/run.js +21 -0
  69. package/dist/registry/build.d.ts +126 -0
  70. package/dist/registry/build.d.ts.map +1 -0
  71. package/dist/registry/build.js +192 -0
  72. package/dist/registry/call-notes.d.ts +11 -0
  73. package/dist/registry/call-notes.d.ts.map +1 -0
  74. package/dist/registry/call-notes.js +72 -0
  75. package/dist/registry/claims.d.ts +63 -0
  76. package/dist/registry/claims.d.ts.map +1 -0
  77. package/dist/registry/claims.js +107 -0
  78. package/dist/registry/facts.d.ts +32 -0
  79. package/dist/registry/facts.d.ts.map +1 -0
  80. package/dist/registry/facts.js +120 -0
  81. package/dist/registry/fingerprint.d.ts +20 -0
  82. package/dist/registry/fingerprint.d.ts.map +1 -0
  83. package/dist/registry/fingerprint.js +150 -0
  84. package/dist/registry/parity.d.ts +26 -0
  85. package/dist/registry/parity.d.ts.map +1 -0
  86. package/dist/registry/parity.js +41 -0
  87. package/dist/registry/pricing.d.ts +19 -0
  88. package/dist/registry/pricing.d.ts.map +1 -0
  89. package/dist/registry/pricing.js +69 -0
  90. package/dist/registry/rows.d.ts +16 -0
  91. package/dist/registry/rows.d.ts.map +1 -0
  92. package/dist/registry/rows.js +57 -0
  93. package/dist/registry/seed.d.ts +43 -0
  94. package/dist/registry/seed.d.ts.map +1 -0
  95. package/dist/registry/seed.js +97 -0
  96. package/dist/registry/select.d.ts +68 -0
  97. package/dist/registry/select.d.ts.map +1 -0
  98. package/dist/registry/select.js +141 -0
  99. package/dist/registry/staleness.d.ts +18 -0
  100. package/dist/registry/staleness.d.ts.map +1 -0
  101. package/dist/registry/staleness.js +39 -0
  102. package/dist/routes/draft.d.ts +83 -0
  103. package/dist/routes/draft.d.ts.map +1 -0
  104. package/dist/routes/draft.js +135 -0
  105. package/dist/routes/loader.d.ts +26 -0
  106. package/dist/routes/loader.d.ts.map +1 -0
  107. package/dist/routes/loader.js +436 -0
  108. package/dist/routes/manifest.d.ts +24 -0
  109. package/dist/routes/manifest.d.ts.map +1 -0
  110. package/dist/routes/manifest.js +118 -0
  111. package/dist/routes/serveability.d.ts +52 -0
  112. package/dist/routes/serveability.d.ts.map +1 -0
  113. package/dist/routes/serveability.js +73 -0
  114. package/dist/routes/template.d.ts +18 -0
  115. package/dist/routes/template.d.ts.map +1 -0
  116. package/dist/routes/template.js +94 -0
  117. package/dist/run/cost.d.ts +26 -0
  118. package/dist/run/cost.d.ts.map +1 -0
  119. package/dist/run/cost.js +41 -0
  120. package/dist/run/envelope.d.ts +21 -0
  121. package/dist/run/envelope.d.ts.map +1 -0
  122. package/dist/run/envelope.js +45 -0
  123. package/dist/run/grounding.d.ts +56 -0
  124. package/dist/run/grounding.d.ts.map +1 -0
  125. package/dist/run/grounding.js +89 -0
  126. package/dist/run/repair.d.ts +36 -0
  127. package/dist/run/repair.d.ts.map +1 -0
  128. package/dist/run/repair.js +61 -0
  129. package/dist/run/resolve.d.ts +26 -0
  130. package/dist/run/resolve.d.ts.map +1 -0
  131. package/dist/run/resolve.js +81 -0
  132. package/dist/run/retry.d.ts +19 -0
  133. package/dist/run/retry.d.ts.map +1 -0
  134. package/dist/run/retry.js +50 -0
  135. package/dist/run/run.d.ts +41 -0
  136. package/dist/run/run.d.ts.map +1 -0
  137. package/dist/run/run.js +0 -0
  138. package/dist/run/validate.d.ts +15 -0
  139. package/dist/run/validate.d.ts.map +1 -0
  140. package/dist/run/validate.js +56 -0
  141. package/dist/savings/emit.d.ts +25 -0
  142. package/dist/savings/emit.d.ts.map +1 -0
  143. package/dist/savings/emit.js +75 -0
  144. package/dist/savings/odometer.d.ts +32 -0
  145. package/dist/savings/odometer.d.ts.map +1 -0
  146. package/dist/savings/odometer.js +70 -0
  147. package/dist/savings/swap-execute.d.ts +44 -0
  148. package/dist/savings/swap-execute.d.ts.map +1 -0
  149. package/dist/savings/swap-execute.js +87 -0
  150. package/dist/savings/swaps-client.d.ts +41 -0
  151. package/dist/savings/swaps-client.d.ts.map +1 -0
  152. package/dist/savings/swaps-client.js +81 -0
  153. package/dist/telemetry/capture.d.ts +28 -0
  154. package/dist/telemetry/capture.d.ts.map +1 -0
  155. package/dist/telemetry/capture.js +74 -0
  156. package/dist/telemetry/exporter.d.ts +62 -0
  157. package/dist/telemetry/exporter.d.ts.map +1 -0
  158. package/dist/telemetry/exporter.js +233 -0
  159. package/dist/telemetry/ingest-protocol.d.ts +161 -0
  160. package/dist/telemetry/ingest-protocol.d.ts.map +1 -0
  161. package/dist/telemetry/ingest-protocol.js +163 -0
  162. package/dist/telemetry/sinks.d.ts +54 -0
  163. package/dist/telemetry/sinks.d.ts.map +1 -0
  164. package/dist/telemetry/sinks.js +89 -0
  165. package/dist/telemetry/store.d.ts +38 -0
  166. package/dist/telemetry/store.d.ts.map +1 -0
  167. package/dist/telemetry/store.js +332 -0
  168. package/dist/telemetry/supabase-exporter.d.ts +64 -0
  169. package/dist/telemetry/supabase-exporter.d.ts.map +1 -0
  170. package/dist/telemetry/supabase-exporter.js +211 -0
  171. package/dist/tenancy/api-key-format.d.ts +67 -0
  172. package/dist/tenancy/api-key-format.d.ts.map +1 -0
  173. package/dist/tenancy/api-key-format.js +91 -0
  174. package/dist/tenancy/constants.d.ts +23 -0
  175. package/dist/tenancy/constants.d.ts.map +1 -0
  176. package/dist/tenancy/constants.js +25 -0
  177. package/dist/tenancy/scrub.d.ts +47 -0
  178. package/dist/tenancy/scrub.d.ts.map +1 -0
  179. package/dist/tenancy/scrub.js +85 -0
  180. package/dist/types/failures-teaching.d.ts +56 -0
  181. package/dist/types/failures-teaching.d.ts.map +1 -0
  182. package/dist/types/failures-teaching.js +177 -0
  183. package/dist/types/failures.d.ts +27 -0
  184. package/dist/types/failures.d.ts.map +1 -0
  185. package/dist/types/failures.js +41 -0
  186. package/dist/types/provider.d.ts +79 -0
  187. package/dist/types/provider.d.ts.map +1 -0
  188. package/dist/types/provider.js +20 -0
  189. package/dist/types/route.d.ts +127 -0
  190. package/dist/types/route.d.ts.map +1 -0
  191. package/dist/types/route.js +6 -0
  192. package/dist/types/telemetry.d.ts +89 -0
  193. package/dist/types/telemetry.d.ts.map +1 -0
  194. package/dist/types/telemetry.js +6 -0
  195. package/dist/watch/budget.d.ts +19 -0
  196. package/dist/watch/budget.d.ts.map +1 -0
  197. package/dist/watch/budget.js +35 -0
  198. package/dist/watch/evaluate.d.ts +66 -0
  199. package/dist/watch/evaluate.d.ts.map +1 -0
  200. package/dist/watch/evaluate.js +94 -0
  201. package/dist/watch/proposals.d.ts +73 -0
  202. package/dist/watch/proposals.d.ts.map +1 -0
  203. package/dist/watch/proposals.js +169 -0
  204. package/dist/watch/run-record.d.ts +64 -0
  205. package/dist/watch/run-record.d.ts.map +1 -0
  206. package/dist/watch/run-record.js +86 -0
  207. package/dist/watch/run-reported.d.ts +20 -0
  208. package/dist/watch/run-reported.d.ts.map +1 -0
  209. package/dist/watch/run-reported.js +53 -0
  210. package/dist/watch/run.d.ts +40 -0
  211. package/dist/watch/run.d.ts.map +1 -0
  212. package/dist/watch/run.js +44 -0
  213. package/dist/watch/state-store.d.ts +48 -0
  214. package/dist/watch/state-store.d.ts.map +1 -0
  215. package/dist/watch/state-store.js +86 -0
  216. package/dist/watch/state.d.ts +21 -0
  217. package/dist/watch/state.d.ts.map +1 -0
  218. package/dist/watch/state.js +93 -0
  219. package/package.json +50 -14
  220. package/index.js +0 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proposals.d.ts","sourceRoot":"","sources":["../../src/watch/proposals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEjE,MAAM,MAAM,cAAc,GACtB,WAAW,GACX,WAAW,GACX,UAAU,GACV,UAAU,GACV,UAAU,GACV,SAAS,CAAC;AAWd,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CAO/E;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAClD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,iEAAiE;IACjE,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAkCD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC9B;mEAC+D;IAC/D,YAAY,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAChE;;;;;;;;;OASG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAC/E,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IAChD,mEAAmE;IACnE,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E;6EACyE;IACzE,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E;;;;;;;6DAOyD;IACzD,uFAAuF;IACvF,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,MAAM,CACJ,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,cAAc,EACpB,EAAE,EAAE,UAAU,GAAG,UAAU,EAC3B,SAAS,EAAE,MAAM,EACjB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,EAC/B,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAAC;IACpB,wDAAwD;IACxD,aAAa,CAAC,GAAG,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,eAAe,CAsJpF"}
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ /**
3
+ * Proposal lifecycle + control-plane client — Phase 4 WS1 (plan §4).
4
+ *
5
+ * suggested → evidenced (bake-off attached)
6
+ * → approved (human, recorded) → executed (swap machinery)
7
+ * suggested|evidenced → rejected | expired
8
+ *
9
+ * Proposals NEVER auto-execute: "executed" is only reachable through
10
+ * "approved", and approval is a human CLI action (`swap execute --approve`).
11
+ * Transport is PostgREST with the service-role key — proposals are
12
+ * control-plane-only state (cross-run truth, not per-inference telemetry).
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.assertTransition = assertTransition;
16
+ exports.createProposalsClient = createProposalsClient;
17
+ const ALLOWED_TRANSITIONS = {
18
+ suggested: ["evidenced", "rejected", "expired"],
19
+ evidenced: ["approved", "rejected", "expired"],
20
+ approved: ["executed"],
21
+ rejected: [],
22
+ executed: [],
23
+ expired: [],
24
+ };
25
+ function assertTransition(from, to) {
26
+ if (!ALLOWED_TRANSITIONS[from].includes(to)) {
27
+ throw new Error(`illegal proposal transition ${from} → ${to} (allowed from ${from}: ` +
28
+ `[${ALLOWED_TRANSITIONS[from].join(", ") || "none — terminal"}])`);
29
+ }
30
+ }
31
+ function toRecord(r) {
32
+ return {
33
+ id: r.id,
34
+ route: r.route,
35
+ kind: r.kind,
36
+ trigger: r.trigger,
37
+ triggerHash: r.trigger_hash,
38
+ evidence: r.evidence,
39
+ status: r.status,
40
+ createdAt: r.created_at,
41
+ updatedAt: r.updated_at,
42
+ expiresAt: r.expires_at,
43
+ approvedBy: r.approved_by ?? null,
44
+ decidedAt: r.decided_at ?? null,
45
+ };
46
+ }
47
+ function createProposalsClient(config) {
48
+ const fetchFn = config.fetchFn ?? fetch;
49
+ const base = config.supabaseUrl.replace(/\/$/, "");
50
+ async function request(path, init) {
51
+ const response = await fetchFn(`${base}/rest/v1/${path}`, {
52
+ ...init,
53
+ headers: {
54
+ "content-type": "application/json",
55
+ apikey: config.serviceRoleKey,
56
+ authorization: `Bearer ${config.serviceRoleKey}`,
57
+ ...(init.headers ?? {}),
58
+ },
59
+ });
60
+ if (!response.ok) {
61
+ throw new Error(`proposals ${init.method ?? "GET"} ${path} → ${response.status} ` +
62
+ `${await response.text().catch(() => "")}`);
63
+ }
64
+ return response;
65
+ }
66
+ return {
67
+ async submitDrafts(drafts) {
68
+ if (drafts.length === 0)
69
+ return 0;
70
+ const response = await request(`proposals?on_conflict=${encodeURIComponent("route,trigger_hash")}`, {
71
+ method: "POST",
72
+ headers: { prefer: "resolution=ignore-duplicates,return=representation" },
73
+ body: JSON.stringify(drafts.map((d) => ({
74
+ route: d.route,
75
+ kind: d.kind,
76
+ trigger: d.trigger,
77
+ trigger_hash: d.triggerHash,
78
+ status: "suggested",
79
+ expires_at: d.expiresAt,
80
+ }))),
81
+ });
82
+ const inserted = (await response.json().catch(() => []));
83
+ return Array.isArray(inserted) ? inserted.length : 0;
84
+ },
85
+ async list(orgId, status) {
86
+ const filter = status !== undefined ? `&status=eq.${status}` : "";
87
+ const scope = orgId !== null ? `&org_id=eq.${encodeURIComponent(orgId)}` : "";
88
+ const response = await request(`proposals?select=*${filter}${scope}&order=created_at.desc`, {
89
+ method: "GET",
90
+ });
91
+ return (await response.json()).map(toRecord);
92
+ },
93
+ async get(id) {
94
+ const response = await request(`proposals?select=*&id=eq.${encodeURIComponent(id)}`, {
95
+ method: "GET",
96
+ });
97
+ const rows = (await response.json());
98
+ return rows.length > 0 ? toRecord(rows[0]) : null;
99
+ },
100
+ async attachEvidence(id, evidence) {
101
+ await request(`proposals?id=eq.${encodeURIComponent(id)}`, {
102
+ method: "PATCH",
103
+ body: JSON.stringify({
104
+ status: "evidenced",
105
+ evidence,
106
+ updated_at: new Date().toISOString(),
107
+ }),
108
+ });
109
+ },
110
+ async setStatus(id, from, to) {
111
+ assertTransition(from, to);
112
+ // Status-scoped PATCH: a concurrent transition means zero rows match —
113
+ // never a blind overwrite.
114
+ await request(`proposals?id=eq.${encodeURIComponent(id)}&status=eq.${from}`, {
115
+ method: "PATCH",
116
+ body: JSON.stringify({ status: to, updated_at: new Date().toISOString() }),
117
+ });
118
+ },
119
+ async orgOf(id) {
120
+ const response = await request(`proposals?select=org_id&id=eq.${encodeURIComponent(id)}`, {
121
+ method: "GET",
122
+ });
123
+ const rows = (await response.json());
124
+ return rows.length > 0 && typeof rows[0].org_id === "string" ? rows[0].org_id : null;
125
+ },
126
+ async decide(id, from, to, decidedBy, decidedByUserId, orgId) {
127
+ assertTransition(from, to);
128
+ const now = new Date().toISOString();
129
+ // Status-scoped, and asks for the row back so we can tell "decided" from
130
+ // "someone else already decided" rather than assuming success.
131
+ // org_id in the WHERE clause is defense in depth, not the primary check.
132
+ // The caller authorizes against this row's org before we get here; this
133
+ // makes a bypass of THAT check unable to cross a tenant boundary anyway,
134
+ // because the row simply will not match. A service-role write with no
135
+ // tenant predicate is how an authorization bug becomes a data breach.
136
+ const orgPredicate = orgId === undefined ? "" : `&org_id=eq.${encodeURIComponent(orgId)}`;
137
+ const response = await request(`proposals?id=eq.${encodeURIComponent(id)}&status=eq.${from}${orgPredicate}`, {
138
+ method: "PATCH",
139
+ headers: { prefer: "return=representation" },
140
+ body: JSON.stringify({
141
+ status: to,
142
+ approved_by: decidedBy,
143
+ // P6 WS1 identity. `approved_by` stays the operator-TYPED display
144
+ // name; `decided_by` is the verified identity. They are separate
145
+ // columns on purpose — merging them would make the pre-identity
146
+ // rows indistinguishable from the identity-bound ones, and half
147
+ // the corpus would silently look more trustworthy than it is.
148
+ // Undefined (self-host, no auth server) leaves the column null,
149
+ // which is the honest record rather than a guess.
150
+ ...(decidedByUserId !== undefined && decidedByUserId !== null
151
+ ? { decided_by: decidedByUserId }
152
+ : {}),
153
+ decided_at: now,
154
+ updated_at: now,
155
+ }),
156
+ });
157
+ const rows = (await response.json().catch(() => []));
158
+ return Array.isArray(rows) && rows.length > 0;
159
+ },
160
+ async expireOverdue(now) {
161
+ const cutoff = now.toISOString();
162
+ await request(`proposals?status=in.${encodeURIComponent("(suggested,evidenced)")}` +
163
+ `&expires_at=lt.${encodeURIComponent(cutoff)}`, {
164
+ method: "PATCH",
165
+ body: JSON.stringify({ status: "expired", updated_at: cutoff }),
166
+ });
167
+ },
168
+ };
169
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Scheduled-job self-reporting — Phase 6.
3
+ *
4
+ * A4 gave the watch cron durable state. It did not give anyone a way to tell
5
+ * whether the cron RAN. `control_state` sat empty for a week and the three
6
+ * candidate explanations — never scheduled, running and failing, or running
7
+ * and finding nothing — were indistinguishable from outside. The only
8
+ * evidence lived in Render's log viewer, behind an API key that grants access
9
+ * to every workspace and every environment variable in the account.
10
+ *
11
+ * A job whose only proof of life is a vendor's log viewer is a job you cannot
12
+ * operate, and reaching for that key to read a log is a poor trade: it is the
13
+ * broadest possible credential borrowed for the narrowest possible question.
14
+ *
15
+ * So the job answers the question itself. One row, upserted each cycle, in the
16
+ * store we already own. "Did it run, when, and what did it do" becomes a
17
+ * SELECT — no vendor credential involved.
18
+ *
19
+ * Three properties matter more than the shape of the record:
20
+ *
21
+ * 1. **It records failures.** A report that only appears on success is
22
+ * indistinguishable from no report, which is precisely the ambiguity that
23
+ * made A4 unverifiable.
24
+ * 2. **It never throws.** Observability that can fail the thing it observes is
25
+ * worse than none.
26
+ * 3. **It is a separate key from the state.** A run report must never be able
27
+ * to corrupt the pricing snapshot it reports on.
28
+ */
29
+ import { scrubSecrets } from "../tenancy/scrub";
30
+ /** Key under which the last run report lives in `control_state`. */
31
+ export declare const WATCH_RUN_KEY = "watch:last-run";
32
+ export interface WatchRunRecord {
33
+ readonly outcome: "ok" | "failed";
34
+ readonly startedAt: string;
35
+ readonly finishedAt: string;
36
+ /** True when this cycle had no previous state and only established a baseline. */
37
+ readonly seeded?: boolean;
38
+ readonly events?: number;
39
+ readonly drafts?: number;
40
+ readonly inserted?: number;
41
+ /** Which store backed this run — the A4 regression is a file-backed cron. */
42
+ readonly stateStore?: string;
43
+ readonly error?: string;
44
+ }
45
+ export interface RunRecordConfig {
46
+ readonly supabaseUrl: string;
47
+ readonly serviceRoleKey: string;
48
+ readonly fetchFn?: typeof fetch;
49
+ }
50
+ /**
51
+ * Re-exported from the shared scrubber (C2's leak gate). This module owned the
52
+ * original copy; WS3 promoted it to `tenancy/scrub.ts` and widened its prefix
53
+ * set to the BYOK provider families, so error text stored here is scrubbed by
54
+ * the same gate the vault and telemetry boundaries use.
55
+ */
56
+ export { scrubSecrets };
57
+ /**
58
+ * Write the run report. Best-effort by design — see property 2 above.
59
+ *
60
+ * Silent when no control plane is configured: a self-hosted rig with no
61
+ * Supabase has nowhere to write and nothing to prove to us.
62
+ */
63
+ export declare function recordWatchRun(record: WatchRunRecord, config: RunRecordConfig): Promise<void>;
64
+ //# sourceMappingURL=run-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-record.d.ts","sourceRoot":"","sources":["../../src/watch/run-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAEhD,oEAAoE;AACpE,eAAO,MAAM,aAAa,mBAAmB,CAAC;AAE9C,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,IAAI,GAAG,QAAQ,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,6EAA6E;IAC7E,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACjC;AAED;;;;;GAKG;AACH,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,IAAI,CAAC,CA4Bf"}
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ /**
3
+ * Scheduled-job self-reporting — Phase 6.
4
+ *
5
+ * A4 gave the watch cron durable state. It did not give anyone a way to tell
6
+ * whether the cron RAN. `control_state` sat empty for a week and the three
7
+ * candidate explanations — never scheduled, running and failing, or running
8
+ * and finding nothing — were indistinguishable from outside. The only
9
+ * evidence lived in Render's log viewer, behind an API key that grants access
10
+ * to every workspace and every environment variable in the account.
11
+ *
12
+ * A job whose only proof of life is a vendor's log viewer is a job you cannot
13
+ * operate, and reaching for that key to read a log is a poor trade: it is the
14
+ * broadest possible credential borrowed for the narrowest possible question.
15
+ *
16
+ * So the job answers the question itself. One row, upserted each cycle, in the
17
+ * store we already own. "Did it run, when, and what did it do" becomes a
18
+ * SELECT — no vendor credential involved.
19
+ *
20
+ * Three properties matter more than the shape of the record:
21
+ *
22
+ * 1. **It records failures.** A report that only appears on success is
23
+ * indistinguishable from no report, which is precisely the ambiguity that
24
+ * made A4 unverifiable.
25
+ * 2. **It never throws.** Observability that can fail the thing it observes is
26
+ * worse than none.
27
+ * 3. **It is a separate key from the state.** A run report must never be able
28
+ * to corrupt the pricing snapshot it reports on.
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.scrubSecrets = exports.WATCH_RUN_KEY = void 0;
32
+ exports.recordWatchRun = recordWatchRun;
33
+ const scrub_1 = require("../tenancy/scrub");
34
+ Object.defineProperty(exports, "scrubSecrets", { enumerable: true, get: function () { return scrub_1.scrubSecrets; } });
35
+ /** Key under which the last run report lives in `control_state`. */
36
+ exports.WATCH_RUN_KEY = "watch:last-run";
37
+ /**
38
+ * Write the run report. Best-effort by design — see property 2 above.
39
+ *
40
+ * Silent when no control plane is configured: a self-hosted rig with no
41
+ * Supabase has nowhere to write and nothing to prove to us.
42
+ */
43
+ async function recordWatchRun(record, config) {
44
+ const { supabaseUrl, serviceRoleKey } = config;
45
+ if (supabaseUrl === "" || serviceRoleKey === "")
46
+ return;
47
+ const fetchFn = config.fetchFn ?? fetch;
48
+ const safe = record.error === undefined ? record : { ...record, error: (0, scrub_1.scrubSecrets)(record.error) };
49
+ try {
50
+ const response = await fetchFn(`${supabaseUrl.replace(/\/$/, "")}/rest/v1/control_state?on_conflict=key`, {
51
+ method: "POST",
52
+ headers: {
53
+ "content-type": "application/json",
54
+ apikey: serviceRoleKey,
55
+ authorization: `Bearer ${serviceRoleKey}`,
56
+ prefer: "resolution=merge-duplicates,return=minimal",
57
+ },
58
+ body: JSON.stringify([
59
+ { key: exports.WATCH_RUN_KEY, value: safe, updated_at: new Date().toISOString() },
60
+ ]),
61
+ });
62
+ if (!response.ok)
63
+ warnReportFailed(`${response.status} ${await response.text().catch(() => "")}`);
64
+ }
65
+ catch (err) {
66
+ warnReportFailed(err instanceof Error ? err.message : String(err));
67
+ }
68
+ }
69
+ /**
70
+ * Non-fatal, but never silent — A-C6.3, 2026-08-05.
71
+ *
72
+ * Property 2 above says this must not fail the cycle it observes, and that is
73
+ * still true. But the first version swallowed the outcome entirely, which
74
+ * recreated the very ambiguity this module exists to end one level up: a
75
+ * report that never lands looks exactly like a report that lands fine, and
76
+ * `watch:last-run` being absent would again have three explanations and no way
77
+ * to tell them apart.
78
+ *
79
+ * So: the cycle still succeeds, and the operator still finds out. The warning
80
+ * names the key so a reader who greps the cron log for it lands here.
81
+ */
82
+ function warnReportFailed(detail) {
83
+ console.warn(`[modelrig watch] could not write the ${exports.WATCH_RUN_KEY} self-report (${(0, scrub_1.scrubSecrets)(detail)}). ` +
84
+ "The cycle itself is unaffected — but until this succeeds, `control_state` cannot tell you " +
85
+ "whether this job ran, so fall back to watch:pricing-universe.updated_at.");
86
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * One watch cycle, with a run report — Phase 6.
3
+ *
4
+ * Extracted from the CLI rather than written inline, for two reasons. The
5
+ * obvious one: `cli.ts` crossed the package's 700-LOC invariant and the test
6
+ * suite said so. The better one: reporting-on-failure is the half that
7
+ * matters, and a `catch` branch buried in a CLI command is a branch nobody
8
+ * exercises. Here it has a test.
9
+ *
10
+ * A4 gave this job durable state but no proof of life. `control_state` sat
11
+ * empty for a week and "never scheduled", "running and failing" and "running
12
+ * and finding nothing" were indistinguishable from outside — readable only
13
+ * through Render's log viewer, behind an API key that grants the whole
14
+ * account. Now the job answers for itself, and answering costs no vendor
15
+ * credential.
16
+ */
17
+ import { type RunRecordConfig } from "./run-record";
18
+ import type { WatchCycleOptions, WatchCycleResult } from "./run";
19
+ export declare function runWatchCycleReported(opts: WatchCycleOptions, report: RunRecordConfig, now?: () => string): Promise<WatchCycleResult>;
20
+ //# sourceMappingURL=run-reported.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-reported.d.ts","sourceRoot":"","sources":["../../src/watch/run-reported.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAEjE,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,iBAAiB,EACvB,MAAM,EAAE,eAAe,EACvB,GAAG,GAAE,MAAM,MAAuC,GACjD,OAAO,CAAC,gBAAgB,CAAC,CAsC3B"}
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /**
3
+ * One watch cycle, with a run report — Phase 6.
4
+ *
5
+ * Extracted from the CLI rather than written inline, for two reasons. The
6
+ * obvious one: `cli.ts` crossed the package's 700-LOC invariant and the test
7
+ * suite said so. The better one: reporting-on-failure is the half that
8
+ * matters, and a `catch` branch buried in a CLI command is a branch nobody
9
+ * exercises. Here it has a test.
10
+ *
11
+ * A4 gave this job durable state but no proof of life. `control_state` sat
12
+ * empty for a week and "never scheduled", "running and failing" and "running
13
+ * and finding nothing" were indistinguishable from outside — readable only
14
+ * through Render's log viewer, behind an API key that grants the whole
15
+ * account. Now the job answers for itself, and answering costs no vendor
16
+ * credential.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.runWatchCycleReported = runWatchCycleReported;
20
+ const run_record_1 = require("./run-record");
21
+ const run_1 = require("./run");
22
+ async function runWatchCycleReported(opts, report, now = () => new Date().toISOString()) {
23
+ const startedAt = now();
24
+ const stateStore = opts.store?.description ?? "local file";
25
+ let result;
26
+ try {
27
+ result = await (0, run_1.runWatchCycle)(opts);
28
+ }
29
+ catch (err) {
30
+ // Report BEFORE rethrowing. A crashed cycle that leaves no trace is the
31
+ // exact state this module exists to end, and rethrowing still fails the
32
+ // cron so the platform's own alerting is unaffected.
33
+ await (0, run_record_1.recordWatchRun)({
34
+ outcome: "failed",
35
+ startedAt,
36
+ finishedAt: now(),
37
+ stateStore,
38
+ error: err instanceof Error ? err.message : String(err),
39
+ }, report);
40
+ throw err;
41
+ }
42
+ await (0, run_record_1.recordWatchRun)({
43
+ outcome: "ok",
44
+ startedAt,
45
+ finishedAt: now(),
46
+ seeded: result.seeded,
47
+ events: result.events,
48
+ drafts: result.drafts,
49
+ inserted: result.inserted,
50
+ stateStore,
51
+ }, report);
52
+ return result;
53
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * One watch cycle — Phase 4 WS1 (`modelrig watch run`, cron-able):
3
+ * 1. diff the pricing universe against last-seen state (or take injected
4
+ * fixture events — the test/demo path),
5
+ * 2. evaluate events into proposal drafts (pure),
6
+ * 3. sweep overdue proposals to expired,
7
+ * 4. submit drafts (idempotent on (route, trigger_hash)),
8
+ * 5. save state.
9
+ * The cycle itself spends $0 — paid evidence runs happen only through
10
+ * `bakeoff --from-proposal`, under the monthly watch budget.
11
+ */
12
+ import type { RouteEconomics, WatchEvent } from "./evaluate";
13
+ import type { ProposalsClient } from "./proposals";
14
+ import type { PriceUniverse } from "./state";
15
+ import type { WatchStateStore } from "./state-store";
16
+ export interface WatchCycleOptions {
17
+ readonly routes: readonly RouteEconomics[];
18
+ /** Local-file state path — the self-host fallback. Ignored when `store`
19
+ * is supplied. One of `store` or `statePath` is required. */
20
+ readonly statePath?: string;
21
+ /** Durable state store (addendum A4). On an ephemeral cron container this
22
+ * MUST be the control-plane store: a file-backed one re-seeds every run,
23
+ * so no price move can ever fire an event. */
24
+ readonly store?: WatchStateStore;
25
+ readonly universe: PriceUniverse;
26
+ readonly client: ProposalsClient;
27
+ readonly thresholdPct: number;
28
+ /** Fixture events (acceptance criterion 2's injected path) — when set,
29
+ * the state diff is skipped and state is NOT advanced. */
30
+ readonly injectedEvents?: readonly WatchEvent[];
31
+ readonly now?: Date;
32
+ }
33
+ export interface WatchCycleResult {
34
+ readonly seeded: boolean;
35
+ readonly events: number;
36
+ readonly drafts: number;
37
+ readonly inserted: number;
38
+ }
39
+ export declare function runWatchCycle(opts: WatchCycleOptions): Promise<WatchCycleResult>;
40
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/watch/run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C;iEAC6D;IAC7D,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;kDAE8C;IAC9C,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;8DAC0D;IAC1D,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAChD,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAiCtF"}
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ /**
3
+ * One watch cycle — Phase 4 WS1 (`modelrig watch run`, cron-able):
4
+ * 1. diff the pricing universe against last-seen state (or take injected
5
+ * fixture events — the test/demo path),
6
+ * 2. evaluate events into proposal drafts (pure),
7
+ * 3. sweep overdue proposals to expired,
8
+ * 4. submit drafts (idempotent on (route, trigger_hash)),
9
+ * 5. save state.
10
+ * The cycle itself spends $0 — paid evidence runs happen only through
11
+ * `bakeoff --from-proposal`, under the monthly watch budget.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.runWatchCycle = runWatchCycle;
15
+ const evaluate_1 = require("./evaluate");
16
+ const state_1 = require("./state");
17
+ const state_store_1 = require("./state-store");
18
+ async function runWatchCycle(opts) {
19
+ const now = opts.now ?? new Date();
20
+ const store = opts.store ??
21
+ (opts.statePath !== undefined
22
+ ? (0, state_store_1.createFileWatchStateStore)(opts.statePath)
23
+ : (() => {
24
+ throw new Error("runWatchCycle needs either a state store or a statePath");
25
+ })());
26
+ let events;
27
+ let seeded = false;
28
+ if (opts.injectedEvents !== undefined) {
29
+ events = opts.injectedEvents;
30
+ }
31
+ else {
32
+ const previous = await store.load();
33
+ seeded = previous === null;
34
+ events = (0, state_1.computeWatchEvents)(opts.universe, previous);
35
+ }
36
+ const drafts = (0, evaluate_1.evaluateEvents)(opts.routes, events, { thresholdPct: opts.thresholdPct });
37
+ // Expiry sweep first so a re-fired trigger never races its own stale row.
38
+ await opts.client.expireOverdue(now);
39
+ const inserted = drafts.length > 0 ? await opts.client.submitDrafts(drafts) : 0;
40
+ if (opts.injectedEvents === undefined) {
41
+ await store.save({ ...opts.universe });
42
+ }
43
+ return { seeded, events: events.length, drafts: drafts.length, inserted };
44
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Watch-state persistence — Phase 5 addendum A4.
3
+ *
4
+ * The watch loop's pricing snapshot has to survive between runs, and the
5
+ * runs happen in an EPHEMERAL cron container. A local file cannot do that:
6
+ * every run reads nothing, re-seeds, and reports zero events — the cron was
7
+ * live and structurally incapable of ever firing a proposal.
8
+ *
9
+ * Two implementations behind one interface:
10
+ * - control plane (default when Supabase is configured): a keyed row in
11
+ * `control_state`. Survives the container.
12
+ * - local file (the self-host / no-Supabase fallback): the Phase 4
13
+ * behavior, kept because a single-machine user should not need a
14
+ * control plane to run a watch cycle.
15
+ *
16
+ * A store that cannot load is NOT treated as "no previous state". Seeding on
17
+ * a transient read failure would re-baseline the universe and silently
18
+ * swallow every price move since the last successful run, which is exactly
19
+ * the bug this file exists to fix — so a failed read throws.
20
+ */
21
+ import type { PriceUniverse } from "./state";
22
+ /** Key under which the pricing universe lives in `control_state`. */
23
+ export declare const WATCH_STATE_KEY = "watch:pricing-universe";
24
+ export interface WatchStateStore {
25
+ /** Previous snapshot, or null when genuinely none has been stored yet. */
26
+ load(): Promise<PriceUniverse | null>;
27
+ save(universe: PriceUniverse): Promise<void>;
28
+ /** For the operator-facing log line: where this state actually lives. */
29
+ readonly description: string;
30
+ }
31
+ export declare function createFileWatchStateStore(path: string): WatchStateStore;
32
+ export interface ControlPlaneStateConfig {
33
+ readonly supabaseUrl: string;
34
+ readonly serviceRoleKey: string;
35
+ readonly key?: string;
36
+ readonly fetchFn?: typeof fetch;
37
+ }
38
+ export declare function createControlPlaneWatchStateStore(config: ControlPlaneStateConfig): WatchStateStore;
39
+ /** Pick the durable store when a control plane exists, else the local file. */
40
+ export declare function createWatchStateStore(opts: {
41
+ readonly filePath: string;
42
+ readonly export?: {
43
+ readonly supabaseUrl: string;
44
+ readonly serviceRoleKey: string;
45
+ } | undefined;
46
+ readonly fetchFn?: typeof fetch;
47
+ }): WatchStateStore;
48
+ //# sourceMappingURL=state-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-store.d.ts","sourceRoot":"","sources":["../../src/watch/state-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,qEAAqE;AACrE,eAAO,MAAM,eAAe,2BAA2B,CAAC;AAExD,MAAM,WAAW,eAAe;IAC9B,0EAA0E;IAC1E,IAAI,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IACtC,IAAI,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,yEAAyE;IACzE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAQvE;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACjC;AAED,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,uBAAuB,GAC9B,eAAe,CA+CjB;AAED,+EAA+E;AAC/E,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;IAChG,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACjC,GAAG,eAAe,CAOlB"}
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ /**
3
+ * Watch-state persistence — Phase 5 addendum A4.
4
+ *
5
+ * The watch loop's pricing snapshot has to survive between runs, and the
6
+ * runs happen in an EPHEMERAL cron container. A local file cannot do that:
7
+ * every run reads nothing, re-seeds, and reports zero events — the cron was
8
+ * live and structurally incapable of ever firing a proposal.
9
+ *
10
+ * Two implementations behind one interface:
11
+ * - control plane (default when Supabase is configured): a keyed row in
12
+ * `control_state`. Survives the container.
13
+ * - local file (the self-host / no-Supabase fallback): the Phase 4
14
+ * behavior, kept because a single-machine user should not need a
15
+ * control plane to run a watch cycle.
16
+ *
17
+ * A store that cannot load is NOT treated as "no previous state". Seeding on
18
+ * a transient read failure would re-baseline the universe and silently
19
+ * swallow every price move since the last successful run, which is exactly
20
+ * the bug this file exists to fix — so a failed read throws.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.WATCH_STATE_KEY = void 0;
24
+ exports.createFileWatchStateStore = createFileWatchStateStore;
25
+ exports.createControlPlaneWatchStateStore = createControlPlaneWatchStateStore;
26
+ exports.createWatchStateStore = createWatchStateStore;
27
+ const state_1 = require("./state");
28
+ /** Key under which the pricing universe lives in `control_state`. */
29
+ exports.WATCH_STATE_KEY = "watch:pricing-universe";
30
+ function createFileWatchStateStore(path) {
31
+ return {
32
+ description: `local file ${path} (no control plane configured — NOT durable across containers)`,
33
+ load: async () => (0, state_1.loadWatchState)(path),
34
+ save: async (universe) => {
35
+ (0, state_1.saveWatchState)(path, universe);
36
+ },
37
+ };
38
+ }
39
+ function createControlPlaneWatchStateStore(config) {
40
+ const fetchFn = config.fetchFn ?? fetch;
41
+ const base = config.supabaseUrl.replace(/\/$/, "");
42
+ const key = config.key ?? exports.WATCH_STATE_KEY;
43
+ const headers = {
44
+ "content-type": "application/json",
45
+ apikey: config.serviceRoleKey,
46
+ authorization: `Bearer ${config.serviceRoleKey}`,
47
+ };
48
+ return {
49
+ description: `control plane control_state["${key}"]`,
50
+ async load() {
51
+ const response = await fetchFn(`${base}/rest/v1/control_state?select=value&key=eq.${encodeURIComponent(key)}`, { headers });
52
+ if (!response.ok) {
53
+ // Loud, not silent: a read failure that returned null would look
54
+ // exactly like a first run and re-seed the baseline.
55
+ throw new Error(`watch state read failed (${response.status}) — refusing to re-seed, because ` +
56
+ "treating a failed read as 'no previous state' would discard every price move " +
57
+ `since the last successful cycle. ${await response.text().catch(() => "")}`);
58
+ }
59
+ const rows = (await response.json());
60
+ return rows.length > 0 ? rows[0].value : null;
61
+ },
62
+ async save(universe) {
63
+ const response = await fetchFn(`${base}/rest/v1/control_state?on_conflict=key`, {
64
+ method: "POST",
65
+ headers: { ...headers, prefer: "resolution=merge-duplicates,return=minimal" },
66
+ body: JSON.stringify([
67
+ { key, value: universe, updated_at: new Date().toISOString() },
68
+ ]),
69
+ });
70
+ if (!response.ok) {
71
+ throw new Error(`watch state write failed (${response.status}) — the next cycle would re-seed and ` +
72
+ `miss this cycle's changes. ${await response.text().catch(() => "")}`);
73
+ }
74
+ },
75
+ };
76
+ }
77
+ /** Pick the durable store when a control plane exists, else the local file. */
78
+ function createWatchStateStore(opts) {
79
+ if (opts.export === undefined)
80
+ return createFileWatchStateStore(opts.filePath);
81
+ return createControlPlaneWatchStateStore({
82
+ supabaseUrl: opts.export.supabaseUrl,
83
+ serviceRoleKey: opts.export.serviceRoleKey,
84
+ ...(opts.fetchFn !== undefined ? { fetchFn: opts.fetchFn } : {}),
85
+ });
86
+ }