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.
- package/LICENSE +201 -0
- package/README.md +115 -8
- package/dist/adapters/anthropic.d.ts +46 -0
- package/dist/adapters/anthropic.d.ts.map +1 -0
- package/dist/adapters/anthropic.js +202 -0
- package/dist/adapters/deepseek.d.ts +41 -0
- package/dist/adapters/deepseek.d.ts.map +1 -0
- package/dist/adapters/deepseek.js +163 -0
- package/dist/adapters/gemini.d.ts +44 -0
- package/dist/adapters/gemini.d.ts.map +1 -0
- package/dist/adapters/gemini.js +222 -0
- package/dist/adapters/grok.d.ts +44 -0
- package/dist/adapters/grok.d.ts.map +1 -0
- package/dist/adapters/grok.js +168 -0
- package/dist/adapters/openai-compat.d.ts +74 -0
- package/dist/adapters/openai-compat.d.ts.map +1 -0
- package/dist/adapters/openai-compat.js +187 -0
- package/dist/adapters/openai.d.ts +49 -0
- package/dist/adapters/openai.d.ts.map +1 -0
- package/dist/adapters/openai.js +222 -0
- package/dist/bakeoff/engine.d.ts +52 -0
- package/dist/bakeoff/engine.d.ts.map +1 -0
- package/dist/bakeoff/engine.js +163 -0
- package/dist/bakeoff/replay.d.ts +52 -0
- package/dist/bakeoff/replay.d.ts.map +1 -0
- package/dist/bakeoff/replay.js +94 -0
- package/dist/bakeoff/samples.d.ts +62 -0
- package/dist/bakeoff/samples.d.ts.map +1 -0
- package/dist/bakeoff/samples.js +128 -0
- package/dist/bakeoff/stats.d.ts +13 -0
- package/dist/bakeoff/stats.d.ts.map +1 -0
- package/dist/bakeoff/stats.js +48 -0
- package/dist/bakeoff/types.d.ts +37 -0
- package/dist/bakeoff/types.d.ts.map +1 -0
- package/dist/bakeoff/types.js +10 -0
- package/dist/cli.d.ts +27 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +572 -0
- package/dist/config.d.ts +104 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +148 -0
- package/dist/feedback/coverage.d.ts +32 -0
- package/dist/feedback/coverage.d.ts.map +1 -0
- package/dist/feedback/coverage.js +95 -0
- package/dist/feedback/feedback.d.ts +35 -0
- package/dist/feedback/feedback.d.ts.map +1 -0
- package/dist/feedback/feedback.js +39 -0
- package/dist/index.d.ts +155 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +448 -0
- package/dist/init/finder.d.ts +79 -0
- package/dist/init/finder.d.ts.map +1 -0
- package/dist/init/finder.js +297 -0
- package/dist/init/run.d.ts +99 -0
- package/dist/init/run.d.ts.map +1 -0
- package/dist/init/run.js +281 -0
- package/dist/init/signatures.d.ts +39 -0
- package/dist/init/signatures.d.ts.map +1 -0
- package/dist/init/signatures.js +116 -0
- package/dist/matrix/aggregate.d.ts +89 -0
- package/dist/matrix/aggregate.d.ts.map +1 -0
- package/dist/matrix/aggregate.js +186 -0
- package/dist/query/grammar.d.ts +35 -0
- package/dist/query/grammar.d.ts.map +1 -0
- package/dist/query/grammar.js +279 -0
- package/dist/query/run.d.ts +8 -0
- package/dist/query/run.d.ts.map +1 -0
- package/dist/query/run.js +21 -0
- package/dist/registry/build.d.ts +126 -0
- package/dist/registry/build.d.ts.map +1 -0
- package/dist/registry/build.js +192 -0
- package/dist/registry/call-notes.d.ts +11 -0
- package/dist/registry/call-notes.d.ts.map +1 -0
- package/dist/registry/call-notes.js +72 -0
- package/dist/registry/claims.d.ts +63 -0
- package/dist/registry/claims.d.ts.map +1 -0
- package/dist/registry/claims.js +107 -0
- package/dist/registry/facts.d.ts +32 -0
- package/dist/registry/facts.d.ts.map +1 -0
- package/dist/registry/facts.js +120 -0
- package/dist/registry/fingerprint.d.ts +20 -0
- package/dist/registry/fingerprint.d.ts.map +1 -0
- package/dist/registry/fingerprint.js +150 -0
- package/dist/registry/parity.d.ts +26 -0
- package/dist/registry/parity.d.ts.map +1 -0
- package/dist/registry/parity.js +41 -0
- package/dist/registry/pricing.d.ts +19 -0
- package/dist/registry/pricing.d.ts.map +1 -0
- package/dist/registry/pricing.js +69 -0
- package/dist/registry/rows.d.ts +16 -0
- package/dist/registry/rows.d.ts.map +1 -0
- package/dist/registry/rows.js +57 -0
- package/dist/registry/seed.d.ts +43 -0
- package/dist/registry/seed.d.ts.map +1 -0
- package/dist/registry/seed.js +97 -0
- package/dist/registry/select.d.ts +68 -0
- package/dist/registry/select.d.ts.map +1 -0
- package/dist/registry/select.js +141 -0
- package/dist/registry/staleness.d.ts +18 -0
- package/dist/registry/staleness.d.ts.map +1 -0
- package/dist/registry/staleness.js +39 -0
- package/dist/routes/draft.d.ts +83 -0
- package/dist/routes/draft.d.ts.map +1 -0
- package/dist/routes/draft.js +135 -0
- package/dist/routes/loader.d.ts +26 -0
- package/dist/routes/loader.d.ts.map +1 -0
- package/dist/routes/loader.js +436 -0
- package/dist/routes/manifest.d.ts +24 -0
- package/dist/routes/manifest.d.ts.map +1 -0
- package/dist/routes/manifest.js +118 -0
- package/dist/routes/serveability.d.ts +52 -0
- package/dist/routes/serveability.d.ts.map +1 -0
- package/dist/routes/serveability.js +73 -0
- package/dist/routes/template.d.ts +18 -0
- package/dist/routes/template.d.ts.map +1 -0
- package/dist/routes/template.js +94 -0
- package/dist/run/cost.d.ts +26 -0
- package/dist/run/cost.d.ts.map +1 -0
- package/dist/run/cost.js +41 -0
- package/dist/run/envelope.d.ts +21 -0
- package/dist/run/envelope.d.ts.map +1 -0
- package/dist/run/envelope.js +45 -0
- package/dist/run/grounding.d.ts +56 -0
- package/dist/run/grounding.d.ts.map +1 -0
- package/dist/run/grounding.js +89 -0
- package/dist/run/repair.d.ts +36 -0
- package/dist/run/repair.d.ts.map +1 -0
- package/dist/run/repair.js +61 -0
- package/dist/run/resolve.d.ts +26 -0
- package/dist/run/resolve.d.ts.map +1 -0
- package/dist/run/resolve.js +81 -0
- package/dist/run/retry.d.ts +19 -0
- package/dist/run/retry.d.ts.map +1 -0
- package/dist/run/retry.js +50 -0
- package/dist/run/run.d.ts +41 -0
- package/dist/run/run.d.ts.map +1 -0
- package/dist/run/run.js +0 -0
- package/dist/run/validate.d.ts +15 -0
- package/dist/run/validate.d.ts.map +1 -0
- package/dist/run/validate.js +56 -0
- package/dist/savings/emit.d.ts +25 -0
- package/dist/savings/emit.d.ts.map +1 -0
- package/dist/savings/emit.js +75 -0
- package/dist/savings/odometer.d.ts +32 -0
- package/dist/savings/odometer.d.ts.map +1 -0
- package/dist/savings/odometer.js +70 -0
- package/dist/savings/swap-execute.d.ts +44 -0
- package/dist/savings/swap-execute.d.ts.map +1 -0
- package/dist/savings/swap-execute.js +87 -0
- package/dist/savings/swaps-client.d.ts +41 -0
- package/dist/savings/swaps-client.d.ts.map +1 -0
- package/dist/savings/swaps-client.js +81 -0
- package/dist/telemetry/capture.d.ts +28 -0
- package/dist/telemetry/capture.d.ts.map +1 -0
- package/dist/telemetry/capture.js +74 -0
- package/dist/telemetry/exporter.d.ts +62 -0
- package/dist/telemetry/exporter.d.ts.map +1 -0
- package/dist/telemetry/exporter.js +233 -0
- package/dist/telemetry/ingest-protocol.d.ts +161 -0
- package/dist/telemetry/ingest-protocol.d.ts.map +1 -0
- package/dist/telemetry/ingest-protocol.js +163 -0
- package/dist/telemetry/sinks.d.ts +54 -0
- package/dist/telemetry/sinks.d.ts.map +1 -0
- package/dist/telemetry/sinks.js +89 -0
- package/dist/telemetry/store.d.ts +38 -0
- package/dist/telemetry/store.d.ts.map +1 -0
- package/dist/telemetry/store.js +332 -0
- package/dist/telemetry/supabase-exporter.d.ts +64 -0
- package/dist/telemetry/supabase-exporter.d.ts.map +1 -0
- package/dist/telemetry/supabase-exporter.js +211 -0
- package/dist/tenancy/api-key-format.d.ts +67 -0
- package/dist/tenancy/api-key-format.d.ts.map +1 -0
- package/dist/tenancy/api-key-format.js +91 -0
- package/dist/tenancy/constants.d.ts +23 -0
- package/dist/tenancy/constants.d.ts.map +1 -0
- package/dist/tenancy/constants.js +25 -0
- package/dist/tenancy/scrub.d.ts +47 -0
- package/dist/tenancy/scrub.d.ts.map +1 -0
- package/dist/tenancy/scrub.js +85 -0
- package/dist/types/failures-teaching.d.ts +56 -0
- package/dist/types/failures-teaching.d.ts.map +1 -0
- package/dist/types/failures-teaching.js +177 -0
- package/dist/types/failures.d.ts +27 -0
- package/dist/types/failures.d.ts.map +1 -0
- package/dist/types/failures.js +41 -0
- package/dist/types/provider.d.ts +79 -0
- package/dist/types/provider.d.ts.map +1 -0
- package/dist/types/provider.js +20 -0
- package/dist/types/route.d.ts +127 -0
- package/dist/types/route.d.ts.map +1 -0
- package/dist/types/route.js +6 -0
- package/dist/types/telemetry.d.ts +89 -0
- package/dist/types/telemetry.d.ts.map +1 -0
- package/dist/types/telemetry.js +6 -0
- package/dist/watch/budget.d.ts +19 -0
- package/dist/watch/budget.d.ts.map +1 -0
- package/dist/watch/budget.js +35 -0
- package/dist/watch/evaluate.d.ts +66 -0
- package/dist/watch/evaluate.d.ts.map +1 -0
- package/dist/watch/evaluate.js +94 -0
- package/dist/watch/proposals.d.ts +73 -0
- package/dist/watch/proposals.d.ts.map +1 -0
- package/dist/watch/proposals.js +169 -0
- package/dist/watch/run-record.d.ts +64 -0
- package/dist/watch/run-record.d.ts.map +1 -0
- package/dist/watch/run-record.js +86 -0
- package/dist/watch/run-reported.d.ts +20 -0
- package/dist/watch/run-reported.d.ts.map +1 -0
- package/dist/watch/run-reported.js +53 -0
- package/dist/watch/run.d.ts +40 -0
- package/dist/watch/run.d.ts.map +1 -0
- package/dist/watch/run.js +44 -0
- package/dist/watch/state-store.d.ts +48 -0
- package/dist/watch/state-store.d.ts.map +1 -0
- package/dist/watch/state-store.js +86 -0
- package/dist/watch/state.d.ts +21 -0
- package/dist/watch/state.d.ts.map +1 -0
- package/dist/watch/state.js +93 -0
- package/package.json +50 -14
- 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
|
+
}
|