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,161 @@
1
+ /**
2
+ * The telemetry wire contract — Phase 6 WS2.
3
+ *
4
+ * ONE definition of what a batch of telemetry looks like, and ONE function
5
+ * that turns a batch plus a tenant into the exact database writes it becomes.
6
+ * Both transports use them:
7
+ *
8
+ * direct mode exporter → stampBatch(batch, ownTenancy) → PostgREST
9
+ * api mode exporter → POST /v1/ingest → server → stampBatch(batch, keyTenancy) → PostgREST
10
+ *
11
+ * telemetry-ingestion-boundary.md §4 requires that "the SDK must treat api
12
+ * mode and direct mode identically … one exporter interface, two transports —
13
+ * enforced by a shared conformance test". This module is what makes that
14
+ * enforceable rather than aspirational: the conformance test does not compare
15
+ * two hand-written mappers for similarity, it asserts that both paths run the
16
+ * SAME mapper. A drift bug has nowhere to live.
17
+ *
18
+ * ── WHY THE WIRE ROWS DO NOT CARRY A TENANT ──────────────────────────────
19
+ * T3: tenancy is derived from the principal, never trusted from the caller. A
20
+ * client that could put `org_id` on a row could put someone else's `org_id` on
21
+ * a row. So the wire carries the telemetry and nothing else, and the tenant is
22
+ * stamped by whoever holds the principal — the exporter's config in direct
23
+ * mode, the resolved key in api mode.
24
+ *
25
+ * The single exception is `feedback.org_id`, and it is the reason WS2 exists.
26
+ * A verdict recorded on OUR hosted server belongs to the org of the person who
27
+ * recorded it, not to the org the server runs as, and one SQLite store holds
28
+ * verdicts for many orgs. So a feedback row may name its own org — and the
29
+ * ingest endpoint VERIFIES that name against the key rather than trusting it
30
+ * (see `foreignOrgFeedback`). A service-role exporter needs no such check: it
31
+ * already holds a credential that can write any row in the database.
32
+ */
33
+ import type { FailureClass } from "../types/failures";
34
+ /** The path the api sink posts to. Spelled once. */
35
+ export declare const INGEST_PATH = "/v1/ingest";
36
+ /** Bumped only for a breaking change to the shapes below. A server that does
37
+ * not recognise the version refuses the batch rather than guessing. */
38
+ export declare const INGEST_PROTOCOL_VERSION = 1;
39
+ /** Default ingest host — telemetry-ingestion-boundary.md §2.1. */
40
+ export declare const DEFAULT_INGEST_URL = "https://api.modelrig.ai";
41
+ /** The tenant stamped onto every row. Resolved from the principal, never from
42
+ * the payload. */
43
+ export interface Tenancy {
44
+ readonly org_id: string;
45
+ readonly project_id: string | null;
46
+ readonly source_id: string;
47
+ }
48
+ export interface WireInference {
49
+ readonly id: string;
50
+ readonly ts: string;
51
+ readonly route: string;
52
+ readonly route_version: number;
53
+ readonly provider: string;
54
+ readonly model: string;
55
+ readonly requested_tier: string;
56
+ readonly served_tier: string;
57
+ readonly tokens_in: number;
58
+ readonly tokens_out: number;
59
+ readonly tokens_cached: number;
60
+ readonly cost_estimate_usd: number;
61
+ readonly latency_ms: number;
62
+ readonly ttfb_ms: number | null;
63
+ readonly failure_class: FailureClass | null;
64
+ readonly attempts: unknown;
65
+ readonly tags: unknown;
66
+ readonly fingerprint_cell: string | null;
67
+ readonly served_variant: string | null;
68
+ readonly repaired_by: string | null;
69
+ readonly pricing_missing: boolean;
70
+ }
71
+ export interface WireFeedback {
72
+ readonly id: string;
73
+ readonly ts: string;
74
+ readonly inference_id: string | null;
75
+ readonly episode_key: string | null;
76
+ readonly verdict: "up" | "down";
77
+ readonly score: number | null;
78
+ readonly note: string | null;
79
+ /** WHO judged — "sdk", "console", a review-agent name. Not a tenant. */
80
+ readonly source: string;
81
+ /** Authenticated user who recorded it (00017), or null. */
82
+ readonly created_by: string | null;
83
+ /** The verdict's OWN org, when it differs from the exporter's. See the
84
+ * module header — this is the one field the ingest endpoint must verify. */
85
+ readonly org_id?: string;
86
+ }
87
+ export interface WireEnvelope {
88
+ readonly envelope_key: string;
89
+ readonly budget_usd: number;
90
+ readonly spent_usd: number;
91
+ readonly status: "open" | "exhausted";
92
+ }
93
+ export interface WireRoute {
94
+ readonly route: string;
95
+ readonly version: number;
96
+ readonly candidates: ReadonlyArray<{
97
+ readonly provider: string;
98
+ readonly model: string;
99
+ }>;
100
+ readonly capture: boolean;
101
+ }
102
+ export interface WireManifest {
103
+ readonly name: string;
104
+ readonly dimensions: unknown;
105
+ readonly contribution: boolean;
106
+ readonly serveability: readonly unknown[];
107
+ }
108
+ /** One flush's worth of telemetry. Every collection is optional — a flush that
109
+ * has only inferences sends only inferences. */
110
+ export interface IngestBatch {
111
+ readonly v: number;
112
+ /** The rig instance this came from. Half of an inference's identity under
113
+ * multi-tenancy (00013), and customer-chosen, so it is validated but not
114
+ * trusted to be unique across tenants. */
115
+ readonly source: string;
116
+ readonly inferences?: readonly WireInference[];
117
+ readonly feedback?: readonly WireFeedback[];
118
+ readonly envelopes?: readonly WireEnvelope[];
119
+ readonly routes?: readonly WireRoute[];
120
+ readonly manifest?: WireManifest;
121
+ /** Present when this batch advanced the exporter's inference watermark. */
122
+ readonly watermarkAt?: string;
123
+ }
124
+ /** One PostgREST write. `onConflict` is mandatory where `merge` is true —
125
+ * S5: a single-argument upsert targets the PK and 23505s later. */
126
+ export interface TableWrite {
127
+ readonly table: string;
128
+ readonly rows: readonly Record<string, unknown>[];
129
+ /** Real unique constraint the upsert resolves against. */
130
+ readonly onConflict: string;
131
+ /** true = merge-duplicates (mutable projection), false = ignore-duplicates
132
+ * (immutable fact; a retry must never rewrite what already landed). */
133
+ readonly merge: boolean;
134
+ }
135
+ /**
136
+ * Turn a batch plus a tenant into the writes it becomes.
137
+ *
138
+ * THE conformance seam. Both transports call this; nothing else builds a
139
+ * telemetry row. If you are about to add a table to the export, add it here
140
+ * and both modes gain it at once — which is the point.
141
+ */
142
+ export declare function stampBatch(batch: IngestBatch, tenancy: Tenancy): TableWrite[];
143
+ /**
144
+ * Feedback rows naming an org other than the presented key's.
145
+ *
146
+ * Exists as its own exported function because the ingest endpoint must drop
147
+ * these and COUNT them, and a check that lives inline in a request handler is
148
+ * a check nobody can test in isolation. Returns the offending ids.
149
+ */
150
+ export declare function foreignOrgFeedback(rows: readonly WireFeedback[], keyOrgId: string): readonly string[];
151
+ /** What `/v1/ingest` answers. Counts only — a per-row error list on a
152
+ * fire-and-forget telemetry path is a payload nobody reads and a leak surface
153
+ * nobody audits. */
154
+ export interface IngestOutcome {
155
+ readonly accepted: Readonly<Record<string, number>>;
156
+ readonly dropped: Readonly<Record<string, number>>;
157
+ /** reason → count. The teaching half: "clock_skew_future: 12" is actionable
158
+ * where "dropped: 12" is not. */
159
+ readonly reasons: Readonly<Record<string, number>>;
160
+ }
161
+ //# sourceMappingURL=ingest-protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest-protocol.d.ts","sourceRoot":"","sources":["../../src/telemetry/ingest-protocol.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ /**
3
+ * The telemetry wire contract — Phase 6 WS2.
4
+ *
5
+ * ONE definition of what a batch of telemetry looks like, and ONE function
6
+ * that turns a batch plus a tenant into the exact database writes it becomes.
7
+ * Both transports use them:
8
+ *
9
+ * direct mode exporter → stampBatch(batch, ownTenancy) → PostgREST
10
+ * api mode exporter → POST /v1/ingest → server → stampBatch(batch, keyTenancy) → PostgREST
11
+ *
12
+ * telemetry-ingestion-boundary.md §4 requires that "the SDK must treat api
13
+ * mode and direct mode identically … one exporter interface, two transports —
14
+ * enforced by a shared conformance test". This module is what makes that
15
+ * enforceable rather than aspirational: the conformance test does not compare
16
+ * two hand-written mappers for similarity, it asserts that both paths run the
17
+ * SAME mapper. A drift bug has nowhere to live.
18
+ *
19
+ * ── WHY THE WIRE ROWS DO NOT CARRY A TENANT ──────────────────────────────
20
+ * T3: tenancy is derived from the principal, never trusted from the caller. A
21
+ * client that could put `org_id` on a row could put someone else's `org_id` on
22
+ * a row. So the wire carries the telemetry and nothing else, and the tenant is
23
+ * stamped by whoever holds the principal — the exporter's config in direct
24
+ * mode, the resolved key in api mode.
25
+ *
26
+ * The single exception is `feedback.org_id`, and it is the reason WS2 exists.
27
+ * A verdict recorded on OUR hosted server belongs to the org of the person who
28
+ * recorded it, not to the org the server runs as, and one SQLite store holds
29
+ * verdicts for many orgs. So a feedback row may name its own org — and the
30
+ * ingest endpoint VERIFIES that name against the key rather than trusting it
31
+ * (see `foreignOrgFeedback`). A service-role exporter needs no such check: it
32
+ * already holds a credential that can write any row in the database.
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.DEFAULT_INGEST_URL = exports.INGEST_PROTOCOL_VERSION = exports.INGEST_PATH = void 0;
36
+ exports.stampBatch = stampBatch;
37
+ exports.foreignOrgFeedback = foreignOrgFeedback;
38
+ /** The path the api sink posts to. Spelled once. */
39
+ exports.INGEST_PATH = "/v1/ingest";
40
+ /** Bumped only for a breaking change to the shapes below. A server that does
41
+ * not recognise the version refuses the batch rather than guessing. */
42
+ exports.INGEST_PROTOCOL_VERSION = 1;
43
+ /** Default ingest host — telemetry-ingestion-boundary.md §2.1. */
44
+ exports.DEFAULT_INGEST_URL = "https://api.modelrig.ai";
45
+ /**
46
+ * Turn a batch plus a tenant into the writes it becomes.
47
+ *
48
+ * THE conformance seam. Both transports call this; nothing else builds a
49
+ * telemetry row. If you are about to add a table to the export, add it here
50
+ * and both modes gain it at once — which is the point.
51
+ */
52
+ function stampBatch(batch, tenancy) {
53
+ const writes = [];
54
+ const now = new Date().toISOString();
55
+ if (batch.routes !== undefined && batch.routes.length > 0) {
56
+ writes.push({
57
+ table: "routes",
58
+ onConflict: "org_id,route,version",
59
+ merge: true,
60
+ rows: batch.routes.map((r) => ({
61
+ org_id: tenancy.org_id,
62
+ route: r.route,
63
+ version: r.version,
64
+ candidates: r.candidates,
65
+ capture: r.capture,
66
+ updated_at: now,
67
+ })),
68
+ });
69
+ }
70
+ if (batch.manifest !== undefined) {
71
+ writes.push({
72
+ table: "rig_manifests",
73
+ onConflict: "org_id,source_id",
74
+ merge: true,
75
+ rows: [
76
+ {
77
+ org_id: tenancy.org_id,
78
+ source_id: tenancy.source_id,
79
+ name: batch.manifest.name,
80
+ dimensions: batch.manifest.dimensions,
81
+ contribution: batch.manifest.contribution,
82
+ serveability: batch.manifest.serveability,
83
+ updated_at: now,
84
+ },
85
+ ],
86
+ });
87
+ }
88
+ if (batch.inferences !== undefined && batch.inferences.length > 0) {
89
+ writes.push({
90
+ table: "inferences",
91
+ // 00013's tenant-first primary key. Named explicitly so a retry lands on
92
+ // the row it already wrote rather than on the old single-column pk.
93
+ onConflict: "org_id,source_id,id",
94
+ // An inference is a fact that happened. A retry must not be able to
95
+ // rewrite it — idempotent by doing nothing, not by overwriting.
96
+ merge: false,
97
+ rows: batch.inferences.map((row) => ({
98
+ org_id: tenancy.org_id,
99
+ project_id: tenancy.project_id,
100
+ source_id: tenancy.source_id,
101
+ ...row,
102
+ })),
103
+ });
104
+ }
105
+ if (batch.watermarkAt !== undefined) {
106
+ writes.push({
107
+ table: "export_watermarks",
108
+ onConflict: "org_id,source_id",
109
+ merge: true,
110
+ rows: [
111
+ {
112
+ org_id: tenancy.org_id,
113
+ source_id: tenancy.source_id,
114
+ last_exported_ts: batch.watermarkAt,
115
+ },
116
+ ],
117
+ });
118
+ }
119
+ if (batch.feedback !== undefined && batch.feedback.length > 0) {
120
+ writes.push({
121
+ table: "feedback",
122
+ onConflict: "id",
123
+ merge: false,
124
+ rows: batch.feedback.map(({ org_id, ...row }) => ({
125
+ // The verdict's own org wins when it has one — that is the whole of
126
+ // QA-P6-010's fix. Note that `source_id` is deliberately absent:
127
+ // `feedback.source` already means "who judged", and two columns a
128
+ // letter apart meaning different things is N2's cautionary tale.
129
+ org_id: org_id ?? tenancy.org_id,
130
+ project_id: tenancy.project_id,
131
+ ...row,
132
+ })),
133
+ });
134
+ }
135
+ if (batch.envelopes !== undefined && batch.envelopes.length > 0) {
136
+ writes.push({
137
+ table: "envelopes",
138
+ onConflict: "org_id,envelope_key",
139
+ merge: true,
140
+ rows: batch.envelopes.map((e) => ({
141
+ org_id: tenancy.org_id,
142
+ envelope_key: e.envelope_key,
143
+ budget_usd: e.budget_usd,
144
+ spent_usd: e.spent_usd,
145
+ status: e.status,
146
+ exported_at: now,
147
+ })),
148
+ });
149
+ }
150
+ return writes;
151
+ }
152
+ /**
153
+ * Feedback rows naming an org other than the presented key's.
154
+ *
155
+ * Exists as its own exported function because the ingest endpoint must drop
156
+ * these and COUNT them, and a check that lives inline in a request handler is
157
+ * a check nobody can test in isolation. Returns the offending ids.
158
+ */
159
+ function foreignOrgFeedback(rows, keyOrgId) {
160
+ return rows
161
+ .filter((r) => r.org_id !== undefined && r.org_id !== keyOrgId)
162
+ .map((r) => r.id);
163
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The two telemetry transports — Phase 6 WS2.
3
+ *
4
+ * telemetry-ingestion-boundary.md §2.4 names three modes. Two of them are
5
+ * sinks and live here; the third (local-only) is the absence of a sink.
6
+ *
7
+ * direct self-host + our own single-tenant deployments · their database
8
+ * api every hosted customer · their `rig_sk_` key only
9
+ *
10
+ * Both implement `TelemetrySink`, both are driven by the SAME exporter loop,
11
+ * and both produce their database rows through `stampBatch` — so the promise
12
+ * that "self-host ↔ hosted migration is a config change, not a data model
13
+ * change" is enforced by construction rather than by review.
14
+ *
15
+ * ── WHY THE API SINK GZIPS AND THE DIRECT ONE DOES NOT ───────────────────
16
+ * The api sink crosses the public internet with one batch per flush, and a
17
+ * batch is highly repetitive JSON. The direct sink talks to PostgREST, which
18
+ * would have to decompress it again on the way into the same database the
19
+ * caller already holds a service-role key for. Compression there would be
20
+ * cost with no payer.
21
+ */
22
+ import type { IngestBatch, IngestOutcome, Tenancy } from "./ingest-protocol";
23
+ export interface TelemetrySink {
24
+ /** Named so a boot banner and `/status` can say which lane is live without
25
+ * inspecting config — the "which mode am I in?" question got asked enough
26
+ * during WS1 to be worth answering in the object. */
27
+ readonly mode: "direct" | "api";
28
+ /** Deliver one batch. Throws on failure; the exporter owns retry/backoff. */
29
+ send(batch: IngestBatch): Promise<void>;
30
+ }
31
+ export interface DirectSinkConfig {
32
+ readonly supabaseUrl: string;
33
+ /** Service-role key — server-side only; never reaches a browser. */
34
+ readonly serviceRoleKey: string;
35
+ /** The tenant this deployment stamps. Every row it writes lands here. */
36
+ readonly tenancy: Tenancy;
37
+ readonly fetchFn?: typeof fetch;
38
+ }
39
+ export declare function createDirectSink(config: DirectSinkConfig): TelemetrySink;
40
+ export interface ApiSinkConfig {
41
+ /** Base URL of the ingest service, e.g. https://api.modelrig.ai */
42
+ readonly ingestUrl: string;
43
+ /** `rig_sk_…` with the `ingest` scope. The ONLY credential a hosted
44
+ * customer holds — no database URL, no service role (§2's one rule). */
45
+ readonly apiKey: string;
46
+ readonly fetchFn?: typeof fetch;
47
+ /** Called with each batch's outcome. The exporter logs dropped rows through
48
+ * this: a batch the server partly refused is a 200, and a customer whose
49
+ * rows are being dropped must hear about it from their own process rather
50
+ * than by noticing a gap in a chart weeks later. */
51
+ readonly onOutcome?: (outcome: IngestOutcome) => void;
52
+ }
53
+ export declare function createApiSink(config: ApiSinkConfig): TelemetrySink;
54
+ //# sourceMappingURL=sinks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sinks.d.ts","sourceRoot":"","sources":["../../src/telemetry/sinks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAE7E,MAAM,WAAW,aAAa;IAC5B;;yDAEqD;IACrD,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,KAAK,CAAC;IAChC,6EAA6E;IAC7E,IAAI,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC;AAMD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACjC;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,aAAa,CAiCxE;AAMD,MAAM,WAAW,aAAa;IAC5B,mEAAmE;IACnE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;4EACwE;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;IAChC;;;wDAGoD;IACpD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;CACvD;AAKD,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,aAAa,CAgClE"}
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ /**
3
+ * The two telemetry transports — Phase 6 WS2.
4
+ *
5
+ * telemetry-ingestion-boundary.md §2.4 names three modes. Two of them are
6
+ * sinks and live here; the third (local-only) is the absence of a sink.
7
+ *
8
+ * direct self-host + our own single-tenant deployments · their database
9
+ * api every hosted customer · their `rig_sk_` key only
10
+ *
11
+ * Both implement `TelemetrySink`, both are driven by the SAME exporter loop,
12
+ * and both produce their database rows through `stampBatch` — so the promise
13
+ * that "self-host ↔ hosted migration is a config change, not a data model
14
+ * change" is enforced by construction rather than by review.
15
+ *
16
+ * ── WHY THE API SINK GZIPS AND THE DIRECT ONE DOES NOT ───────────────────
17
+ * The api sink crosses the public internet with one batch per flush, and a
18
+ * batch is highly repetitive JSON. The direct sink talks to PostgREST, which
19
+ * would have to decompress it again on the way into the same database the
20
+ * caller already holds a service-role key for. Compression there would be
21
+ * cost with no payer.
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.createDirectSink = createDirectSink;
25
+ exports.createApiSink = createApiSink;
26
+ const node_zlib_1 = require("node:zlib");
27
+ const ingest_protocol_1 = require("./ingest-protocol");
28
+ function createDirectSink(config) {
29
+ const base = config.supabaseUrl.replace(/\/$/, "");
30
+ const fetchFn = config.fetchFn ?? fetch;
31
+ return {
32
+ mode: "direct",
33
+ async send(batch) {
34
+ for (const write of (0, ingest_protocol_1.stampBatch)(batch, config.tenancy)) {
35
+ const response = await fetchFn(
36
+ // S5: the upsert names a real unique constraint. A single-argument
37
+ // upsert targets the primary key and 23505s later.
38
+ `${base}/rest/v1/${write.table}?on_conflict=${encodeURIComponent(write.onConflict)}`, {
39
+ method: "POST",
40
+ headers: {
41
+ "content-type": "application/json",
42
+ apikey: config.serviceRoleKey,
43
+ authorization: `Bearer ${config.serviceRoleKey}`,
44
+ prefer: write.merge
45
+ ? "resolution=merge-duplicates,return=minimal"
46
+ : "resolution=ignore-duplicates,return=minimal",
47
+ },
48
+ body: JSON.stringify(write.rows),
49
+ });
50
+ if (!response.ok) {
51
+ throw new Error(`sink POST ${write.table} → ${response.status} ${await response.text().catch(() => "")}`);
52
+ }
53
+ }
54
+ },
55
+ };
56
+ }
57
+ /** Below this, gzip costs more than it saves. */
58
+ const GZIP_MIN_BYTES = 1024;
59
+ function createApiSink(config) {
60
+ const url = config.ingestUrl.replace(/\/$/, "") + ingest_protocol_1.INGEST_PATH;
61
+ const fetchFn = config.fetchFn ?? fetch;
62
+ return {
63
+ mode: "api",
64
+ async send(batch) {
65
+ const json = JSON.stringify({ ...batch, v: ingest_protocol_1.INGEST_PROTOCOL_VERSION });
66
+ const compress = Buffer.byteLength(json) >= GZIP_MIN_BYTES;
67
+ const response = await fetchFn(url, {
68
+ method: "POST",
69
+ headers: {
70
+ "content-type": "application/json",
71
+ authorization: `Bearer ${config.apiKey}`,
72
+ ...(compress ? { "content-encoding": "gzip" } : {}),
73
+ },
74
+ body: compress ? (0, node_zlib_1.gzipSync)(json) : json,
75
+ });
76
+ if (!response.ok) {
77
+ // The key itself never appears in the message — C2's leak gate covers
78
+ // failure text, and an exporter's warnings are the likeliest place for
79
+ // a secret to reach someone else's log aggregator.
80
+ throw new Error(`ingest POST → ${response.status} ${await response.text().catch(() => "")}`);
81
+ }
82
+ if (config.onOutcome !== undefined) {
83
+ const outcome = (await response.json().catch(() => null));
84
+ if (outcome !== null)
85
+ config.onOutcome(outcome);
86
+ }
87
+ },
88
+ };
89
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * SQLite telemetry store — spec §5, plan §2. Package-owned storage; zero
3
+ * InferWealth Supabase involvement. Schema created idempotently on open;
4
+ * PRAGMA user_version tracks migrations (v2: fingerprint_cell, Phase 2;
5
+ * v3: captures table + served_variant/repaired_by, Phase 3).
6
+ *
7
+ * PRIVACY (Phase 3 WS4): the `captures` table is created here but NEVER read
8
+ * here — capture access lives exclusively behind telemetry/capture.ts's
9
+ * separate handle. ExportableTelemetryStore MUST NOT gain capture accessors;
10
+ * the export-isolation tests are the tripwire.
11
+ */
12
+ import type { EnvelopeRow, FeedbackRow, TelemetryRow, TelemetryStore } from "../types/telemetry";
13
+ /**
14
+ * Superset of the TelemetryStore contract with the enumeration hooks the
15
+ * Supabase exporter needs (rowid-watermarked reads + local export state).
16
+ * Kept out of types/telemetry.ts on purpose: the public contract stays
17
+ * minimal; the exporter is wired against the concrete store.
18
+ */
19
+ export interface ExportableTelemetryStore extends TelemetryStore {
20
+ /** Rows with rowid > afterRowid, oldest first, with their rowids. */
21
+ readInferencesAfter(afterRowid: number, limit: number): Array<{
22
+ rowid: number;
23
+ row: TelemetryRow;
24
+ }>;
25
+ /** Feedback rows past the watermark, oldest first (Phase 4 mirror). */
26
+ readFeedbackAfter(afterRowid: number, limit: number): Array<{
27
+ rowid: number;
28
+ row: FeedbackRow;
29
+ }>;
30
+ /** Newest-first local feedback read (console/dev inspection). */
31
+ listFeedback(limit: number): FeedbackRow[];
32
+ listEnvelopes(): EnvelopeRow[];
33
+ /** Local exporter watermark (per source instance), 0 when unset. */
34
+ getExportWatermark(sourceId: string): number;
35
+ setExportWatermark(sourceId: string, lastRowid: number): void;
36
+ }
37
+ export declare function openTelemetryStore(dbPath: string): ExportableTelemetryStore;
38
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/telemetry/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGjG;;;;;GAKG;AACH,MAAM,WAAW,wBAAyB,SAAQ,cAAc;IAC9D,qEAAqE;IACrE,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,YAAY,CAAA;KAAE,CAAC,CAAC;IACpG,uEAAuE;IACvE,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,WAAW,CAAA;KAAE,CAAC,CAAC;IACjG,iEAAiE;IACjE,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC;IAC3C,aAAa,IAAI,WAAW,EAAE,CAAC;IAC/B,oEAAoE;IACpE,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC7C,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/D;AAyKD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,wBAAwB,CAkN3E"}