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,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"}
|