modelrig 0.8.0 → 0.9.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 (248) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/README.md +27 -2
  3. package/dist/artifacts/artifact.d.ts +12 -13
  4. package/dist/artifacts/artifact.d.ts.map +1 -1
  5. package/dist/artifacts/artifact.js +17 -23
  6. package/dist/artifacts/index.d.ts +13 -5
  7. package/dist/artifacts/index.d.ts.map +1 -1
  8. package/dist/artifacts/index.js +11 -4
  9. package/dist/artifacts/run-context.d.ts +3 -24
  10. package/dist/artifacts/run-context.d.ts.map +1 -1
  11. package/dist/artifacts/run-context.js +3 -39
  12. package/dist/bakeoff/engine.d.ts +29 -0
  13. package/dist/bakeoff/engine.d.ts.map +1 -1
  14. package/dist/bakeoff/engine.js +47 -0
  15. package/dist/bakeoff/eval-cases-rig.d.ts +36 -0
  16. package/dist/bakeoff/eval-cases-rig.d.ts.map +1 -0
  17. package/dist/bakeoff/eval-cases-rig.js +27 -0
  18. package/dist/bakeoff/eval-cases.d.ts +19 -0
  19. package/dist/bakeoff/eval-cases.d.ts.map +1 -1
  20. package/dist/bakeoff/eval-cases.js +18 -1
  21. package/dist/bakeoff/golden-judge.d.ts +47 -0
  22. package/dist/bakeoff/golden-judge.d.ts.map +1 -0
  23. package/dist/bakeoff/golden-judge.js +49 -0
  24. package/dist/bakeoff/quality.d.ts +30 -0
  25. package/dist/bakeoff/quality.d.ts.map +1 -1
  26. package/dist/bakeoff/quality.js +21 -0
  27. package/dist/bakeoff/replay.d.ts +11 -0
  28. package/dist/bakeoff/replay.d.ts.map +1 -1
  29. package/dist/bakeoff/replay.js +8 -0
  30. package/dist/bakeoff/report.d.ts +7 -0
  31. package/dist/bakeoff/report.d.ts.map +1 -1
  32. package/dist/bakeoff/report.js +26 -0
  33. package/dist/cli-judge.d.ts +12 -0
  34. package/dist/cli-judge.d.ts.map +1 -0
  35. package/dist/cli-judge.js +48 -0
  36. package/dist/cli.d.ts.map +1 -1
  37. package/dist/cli.js +14 -42
  38. package/dist/config.d.ts +54 -29
  39. package/dist/config.d.ts.map +1 -1
  40. package/dist/config.js +86 -47
  41. package/dist/detect/detector.d.ts +4 -2
  42. package/dist/detect/detector.d.ts.map +1 -1
  43. package/dist/detect/safe-harbor.d.ts +57 -0
  44. package/dist/detect/safe-harbor.d.ts.map +1 -0
  45. package/dist/detect/safe-harbor.js +278 -0
  46. package/dist/feedback/coverage.d.ts +11 -8
  47. package/dist/feedback/coverage.d.ts.map +1 -1
  48. package/dist/feedback/coverage.js +29 -16
  49. package/dist/feedback/grade.d.ts +51 -0
  50. package/dist/feedback/grade.d.ts.map +1 -0
  51. package/dist/feedback/grade.js +63 -0
  52. package/dist/gateway/http-protocol.d.ts +1 -1
  53. package/dist/gateway/http-protocol.d.ts.map +1 -1
  54. package/dist/gateway/http-protocol.js +1 -1
  55. package/dist/index.d.ts +44 -27
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +110 -43
  58. package/dist/init/observe-cli.js +1 -1
  59. package/dist/init/observe.d.ts +2 -2
  60. package/dist/init/observe.d.ts.map +1 -1
  61. package/dist/init/observe.js +11 -7
  62. package/dist/mask/arm.d.ts +112 -0
  63. package/dist/mask/arm.d.ts.map +1 -0
  64. package/dist/mask/arm.js +179 -0
  65. package/dist/mask/index.d.ts +2 -0
  66. package/dist/mask/index.d.ts.map +1 -1
  67. package/dist/mask/index.js +7 -1
  68. package/dist/mask/mask-policy-cli.d.ts +12 -4
  69. package/dist/mask/mask-policy-cli.d.ts.map +1 -1
  70. package/dist/mask/mask-policy-cli.js +34 -9
  71. package/dist/query/grammar.d.ts +2 -2
  72. package/dist/query/grammar.d.ts.map +1 -1
  73. package/dist/query/grammar.js +3 -1
  74. package/dist/query/saved.d.ts +13 -0
  75. package/dist/query/saved.d.ts.map +1 -1
  76. package/dist/query/saved.js +67 -1
  77. package/dist/receipts/types.d.ts +1 -1
  78. package/dist/receipts/types.d.ts.map +1 -1
  79. package/dist/receipts/types.js +1 -0
  80. package/dist/registry/build.d.ts +43 -1
  81. package/dist/registry/build.d.ts.map +1 -1
  82. package/dist/registry/build.js +52 -0
  83. package/dist/registry/pricing-shape.d.ts +29 -0
  84. package/dist/registry/pricing-shape.d.ts.map +1 -1
  85. package/dist/registry/pricing-shape.js +10 -1
  86. package/dist/registry/rows.d.ts.map +1 -1
  87. package/dist/registry/rows.js +1 -0
  88. package/dist/registry/select.d.ts +6 -1
  89. package/dist/registry/select.d.ts.map +1 -1
  90. package/dist/registry/select.js +11 -0
  91. package/dist/routes/call-config.d.ts +199 -0
  92. package/dist/routes/call-config.d.ts.map +1 -0
  93. package/dist/routes/call-config.js +343 -0
  94. package/dist/routes/loader.js +5 -3
  95. package/dist/routes/objective-loader.d.ts +12 -0
  96. package/dist/routes/objective-loader.d.ts.map +1 -1
  97. package/dist/routes/objective-loader.js +82 -0
  98. package/dist/routes/resolved-bundle.d.ts +50 -0
  99. package/dist/routes/resolved-bundle.d.ts.map +1 -1
  100. package/dist/routes/resolved-bundle.js +52 -0
  101. package/dist/routes/screen-loader.d.ts +28 -0
  102. package/dist/routes/screen-loader.d.ts.map +1 -1
  103. package/dist/routes/screen-loader.js +51 -0
  104. package/dist/run/accept.d.ts +2 -0
  105. package/dist/run/accept.d.ts.map +1 -1
  106. package/dist/run/accept.js +1 -0
  107. package/dist/run/dispatch.d.ts +42 -13
  108. package/dist/run/dispatch.d.ts.map +1 -1
  109. package/dist/run/dispatch.js +107 -25
  110. package/dist/run/grounding.d.ts +28 -2
  111. package/dist/run/grounding.d.ts.map +1 -1
  112. package/dist/run/grounding.js +54 -10
  113. package/dist/run/record-attempt.d.ts +4 -0
  114. package/dist/run/record-attempt.d.ts.map +1 -1
  115. package/dist/run/record-attempt.js +4 -0
  116. package/dist/run/routing-docs.js +1 -1
  117. package/dist/run/run.d.ts.map +1 -1
  118. package/dist/run/run.js +21 -26
  119. package/dist/savings/odometer.d.ts +4 -0
  120. package/dist/savings/odometer.d.ts.map +1 -1
  121. package/dist/savings/odometer.js +20 -1
  122. package/dist/savings/swap-execute.d.ts +7 -0
  123. package/dist/savings/swap-execute.d.ts.map +1 -1
  124. package/dist/savings/swap-execute.js +30 -0
  125. package/dist/savings/swaps-client.d.ts +10 -0
  126. package/dist/savings/swaps-client.d.ts.map +1 -1
  127. package/dist/screen/fixtures/probe-corpora.d.ts +23 -0
  128. package/dist/screen/fixtures/probe-corpora.d.ts.map +1 -0
  129. package/dist/screen/fixtures/probe-corpora.js +69 -0
  130. package/dist/screen/index.d.ts +21 -0
  131. package/dist/screen/index.d.ts.map +1 -0
  132. package/dist/screen/index.js +64 -0
  133. package/dist/screen/model-screen.d.ts +111 -0
  134. package/dist/screen/model-screen.d.ts.map +1 -0
  135. package/dist/screen/model-screen.js +178 -0
  136. package/dist/screen/patterns.d.ts +51 -0
  137. package/dist/screen/patterns.d.ts.map +1 -0
  138. package/dist/screen/patterns.js +151 -0
  139. package/dist/screen/phi-gate.d.ts +52 -0
  140. package/dist/screen/phi-gate.d.ts.map +1 -0
  141. package/dist/screen/phi-gate.js +86 -0
  142. package/dist/screen/policy.d.ts +71 -0
  143. package/dist/screen/policy.d.ts.map +1 -0
  144. package/dist/screen/policy.js +334 -0
  145. package/dist/screen/probe.d.ts +118 -0
  146. package/dist/screen/probe.d.ts.map +1 -0
  147. package/dist/screen/probe.js +132 -0
  148. package/dist/screen/screen-cli.d.ts +16 -0
  149. package/dist/screen/screen-cli.d.ts.map +1 -0
  150. package/dist/screen/screen-cli.js +178 -0
  151. package/dist/screen/screen.d.ts +99 -0
  152. package/dist/screen/screen.d.ts.map +1 -0
  153. package/dist/screen/screen.js +0 -0
  154. package/dist/screen/sidecar.d.ts +69 -0
  155. package/dist/screen/sidecar.d.ts.map +1 -0
  156. package/dist/screen/sidecar.js +145 -0
  157. package/dist/screen/types.d.ts +43 -2
  158. package/dist/screen/types.d.ts.map +1 -1
  159. package/dist/settings/api-fetcher.d.ts +14 -3
  160. package/dist/settings/api-fetcher.d.ts.map +1 -1
  161. package/dist/settings/api-fetcher.js +61 -10
  162. package/dist/settings/grading-guide.d.ts +76 -0
  163. package/dist/settings/grading-guide.d.ts.map +1 -0
  164. package/dist/settings/grading-guide.js +23 -0
  165. package/dist/settings/index.d.ts +3 -2
  166. package/dist/settings/index.d.ts.map +1 -1
  167. package/dist/settings/index.js +2 -1
  168. package/dist/settings/task-settings.d.ts +44 -7
  169. package/dist/settings/task-settings.d.ts.map +1 -1
  170. package/dist/settings/task-settings.js +53 -12
  171. package/dist/status.d.ts +26 -20
  172. package/dist/status.d.ts.map +1 -1
  173. package/dist/status.js +47 -29
  174. package/dist/telemetry/artifact-store.d.ts +5 -6
  175. package/dist/telemetry/artifact-store.d.ts.map +1 -1
  176. package/dist/telemetry/artifact-store.js +10 -24
  177. package/dist/telemetry/attempt-meta.d.ts +21 -0
  178. package/dist/telemetry/attempt-meta.d.ts.map +1 -1
  179. package/dist/telemetry/attempt-meta.js +9 -0
  180. package/dist/telemetry/exporter.d.ts +2 -2
  181. package/dist/telemetry/exporter.d.ts.map +1 -1
  182. package/dist/telemetry/exporter.js +22 -41
  183. package/dist/telemetry/grade-mappers.d.ts +22 -0
  184. package/dist/telemetry/grade-mappers.d.ts.map +1 -0
  185. package/dist/telemetry/grade-mappers.js +58 -0
  186. package/dist/telemetry/grades-sql.d.ts +53 -0
  187. package/dist/telemetry/grades-sql.d.ts.map +1 -0
  188. package/dist/telemetry/grades-sql.js +133 -0
  189. package/dist/telemetry/ingest-protocol.d.ts +29 -46
  190. package/dist/telemetry/ingest-protocol.d.ts.map +1 -1
  191. package/dist/telemetry/ingest-protocol.js +16 -54
  192. package/dist/telemetry/store-migrations.d.ts.map +1 -1
  193. package/dist/telemetry/store-migrations.js +51 -0
  194. package/dist/telemetry/store.d.ts +9 -6
  195. package/dist/telemetry/store.d.ts.map +1 -1
  196. package/dist/telemetry/store.js +8 -50
  197. package/dist/tenancy/api-key-format.d.ts +30 -1
  198. package/dist/tenancy/api-key-format.d.ts.map +1 -1
  199. package/dist/tenancy/api-key-format.js +33 -1
  200. package/dist/types/provider.d.ts +3 -0
  201. package/dist/types/provider.d.ts.map +1 -1
  202. package/dist/types/provider.js +25 -0
  203. package/dist/types/route.d.ts +78 -2
  204. package/dist/types/route.d.ts.map +1 -1
  205. package/dist/types/telemetry.d.ts +43 -46
  206. package/dist/types/telemetry.d.ts.map +1 -1
  207. package/dist/validate-cli.d.ts.map +1 -1
  208. package/dist/validate-cli.js +32 -0
  209. package/dist/verify/verify.d.ts +5 -0
  210. package/dist/verify/verify.d.ts.map +1 -1
  211. package/dist/verify/verify.js +2 -0
  212. package/dist/watch/evaluate.d.ts +84 -1
  213. package/dist/watch/evaluate.d.ts.map +1 -1
  214. package/dist/watch/evaluate.js +28 -0
  215. package/dist/watch/monitor.d.ts +8 -2
  216. package/dist/watch/monitor.d.ts.map +1 -1
  217. package/dist/watch/monitor.js +13 -5
  218. package/dist/watch/regret-monitor-cli.d.ts +16 -0
  219. package/dist/watch/regret-monitor-cli.d.ts.map +1 -0
  220. package/dist/watch/regret-monitor-cli.js +137 -0
  221. package/dist/watch/regret-monitor.d.ts +168 -0
  222. package/dist/watch/regret-monitor.d.ts.map +1 -0
  223. package/dist/watch/regret-monitor.js +304 -0
  224. package/dist/watch/upgrade.d.ts +62 -0
  225. package/dist/watch/upgrade.d.ts.map +1 -0
  226. package/dist/watch/upgrade.js +57 -0
  227. package/docs/agent-skill/SKILL.md +109 -5
  228. package/docs/how-it-fits.md +3 -2
  229. package/docs/instrumentation.md +149 -21
  230. package/docs/llms.txt +64 -20
  231. package/docs/mcp-oracle.md +249 -0
  232. package/docs/quickstart.md +103 -9
  233. package/package.json +16 -3
  234. package/registry/leaderboard.json +74 -0
  235. package/registry/registry.json +150 -112
  236. package/screen.d.ts +16 -0
  237. package/templates/call-disposition-qa.json +77 -0
  238. package/templates/catalog-cleansing.json +77 -0
  239. package/templates/compliance-review.json +76 -0
  240. package/templates/cs-next-action.json +76 -0
  241. package/templates/document-extraction.json +78 -0
  242. package/templates/financial-classification.json +75 -0
  243. package/templates/lead-qualification.json +76 -0
  244. package/templates/medical-classification.json +71 -0
  245. package/templates/ticket-triage.json +76 -0
  246. package/dist/telemetry/supabase-exporter.d.ts +0 -64
  247. package/dist/telemetry/supabase-exporter.d.ts.map +0 -1
  248. package/dist/telemetry/supabase-exporter.js +0 -211
@@ -0,0 +1,199 @@
1
+ /**
2
+ * SDK-side live call-config resolution (live-routing-evals spec §4, R-1; LRE-1a).
3
+ *
4
+ * A route's live call-config (which model/provider + sampling) can be edited in
5
+ * the console WITHOUT a redeploy. The SDK reads it just-before-call through
6
+ * `GET /v1/routes/:route/call-config`, caches the policy (short TTL, ETag-
7
+ * revalidated), and MERGES the pin over the resolved (mirrored/deploy-time)
8
+ * bundle before `run()`. This is R-1's cached-policy model: no per-call network
9
+ * cost when fresh, and outage-safe.
10
+ *
11
+ * ── FAIL-OPEN, ALWAYS — the hot path never throws ─────────────────────────
12
+ * `getCallConfig` NEVER throws. On any error it returns the LAST-KNOWN-GOOD
13
+ * cached policy; if there is none, it returns null — and a null resolution means
14
+ * "serve the mirrored/pinned bundle unchanged". A route with no override
15
+ * resolves BYTE-IDENTICAL to today (the golden invariant): `applyCallConfig`
16
+ * returns the SAME bundle reference when there is nothing to merge.
17
+ *
18
+ * ── PHASE 1 = PIN ONLY ────────────────────────────────────────────────────
19
+ * `mode='experiment'` (local weighted arm draw) and `mode='live'` (strict per-
20
+ * call read) are TYPED here so the wire shape is complete, but they are INERT:
21
+ * `applyCallConfig` acts on `mode='pin'` only and passes everything else through
22
+ * unchanged. Experiments ship in LRE-2.
23
+ */
24
+ import type { RouteBundle } from "../types/route";
25
+ export type CallConfigMode = "pin" | "experiment" | "live";
26
+ export type CallConfigSource = "user" | "optimizer" | "yaml-default";
27
+ /** The on-the-wire sampling block (snake_case, as the endpoint returns it). */
28
+ export interface CallConfigSampling {
29
+ readonly temperature?: number;
30
+ readonly top_p?: number;
31
+ readonly max_output_tokens?: number;
32
+ }
33
+ /** A `mode='pin'` config: a single model + optional sampling/variant. */
34
+ export interface CallConfigPin {
35
+ readonly provider: string;
36
+ readonly model: string;
37
+ readonly sampling?: CallConfigSampling;
38
+ readonly variant?: string;
39
+ }
40
+ /** A resolved current config, as the resolver caches it. `version===0` and
41
+ * `mode===null` is the INERT default (no override) — resolution is byte-identical
42
+ * to the mirrored bundle. */
43
+ export interface ResolvedCallConfig {
44
+ readonly version: number;
45
+ readonly mode: CallConfigMode | null;
46
+ readonly config: unknown;
47
+ readonly etag: string;
48
+ }
49
+ /** Pulls one route's current config from the control plane, ETag-aware. Returns
50
+ * the config, `"not-modified"` (304 — keep the cache), or null (miss / error —
51
+ * the resolver keeps last-known-good). NEVER throws. */
52
+ export interface CallConfigFetcher {
53
+ fetch(route: string, etag: string | undefined): Promise<ResolvedCallConfig | "not-modified" | null>;
54
+ }
55
+ export interface CallConfigResolver {
56
+ /** The effective config for a route. Reads the cache when fresh (within TTL);
57
+ * otherwise revalidates (ETag) and folds the result in. Fail-open: on error,
58
+ * returns last-known-good, else null. NEVER throws. `null` ⇒ no override ⇒
59
+ * serve the mirrored bundle unchanged. */
60
+ getCallConfig(route: string): Promise<ResolvedCallConfig | null>;
61
+ /** A copy of every resolved route's last-known config — for status/boot. */
62
+ snapshot(): ReadonlyMap<string, ResolvedCallConfig>;
63
+ }
64
+ export interface CallConfigResolverConfig {
65
+ readonly fetcher: CallConfigFetcher;
66
+ /** Cache freshness window (spec §4: ~30–60s). Within it, no fetch. */
67
+ readonly ttlMs: number;
68
+ /** Injectable clock for tests; defaults to Date.now. */
69
+ readonly now?: () => number;
70
+ }
71
+ /** Narrow an unknown endpoint body into a `ResolvedCallConfig`, or null when the
72
+ * shape is not what the endpoint promises (a malformed 200 is a miss, not
73
+ * trusted — we never fabricate an override from junk). */
74
+ export declare function parseCallConfig(body: unknown): ResolvedCallConfig | null;
75
+ /** Narrow a config to a `CallConfigPin`, or null (T7 — jsonb is not proof of
76
+ * shape). The single place a pin's shape is trusted on the SDK side. */
77
+ export declare function narrowPin(config: unknown): CallConfigPin | null;
78
+ /** The pin carried by a resolved config, or null when there is no pin override
79
+ * (no override, or a non-pin mode — inert in Phase 1). */
80
+ export declare function pinFromResolved(resolved: ResolvedCallConfig | null): CallConfigPin | null;
81
+ /**
82
+ * Merge a resolved call-config over a bundle, returning a runnable bundle.
83
+ *
84
+ * INERT BY DEFAULT: when `resolved` is null, or its mode is not `pin`, or the pin
85
+ * does not narrow, the SAME `bundle` reference is returned — byte-identical to
86
+ * today (the golden invariant). Otherwise the pin REPLACES the candidate set with
87
+ * the single pinned model (eligibility was gated at write, so it satisfies the
88
+ * route `require`) and merges its sampling into the policy.
89
+ *
90
+ * `mode='experiment'` is INERT here (returns the bundle unchanged): an experiment
91
+ * resolves through {@link resolveCallConfigForRun}, which draws an arm and returns
92
+ * the arm name for `inferences.meta.arm`. `applyCallConfig` stays pin-only so the
93
+ * pin/golden path is byte-identical.
94
+ */
95
+ export declare function applyCallConfig(bundle: RouteBundle, resolved: ResolvedCallConfig | null): RouteBundle;
96
+ /** One arm of an experiment: a resolved pin config + a positive weight + an
97
+ * optional stable name (defaults to the pin's variant or `provider/model`). The
98
+ * `ref` is a `CallConfigPin` so an arm merges over the bundle exactly like a pin. */
99
+ export interface ExperimentArm {
100
+ readonly ref: CallConfigPin;
101
+ readonly weight: number;
102
+ readonly name?: string;
103
+ }
104
+ /** A `mode='experiment'` config (spec §2.1): weighted arms, an optional
105
+ * stickiness key (names the input field whose VALUE keys deterministic
106
+ * assignment), and a fallback pin (served when the experiment cannot draw). */
107
+ export interface CallConfigExperiment {
108
+ readonly arms: readonly ExperimentArm[];
109
+ readonly stickyKey?: string;
110
+ readonly fallback: CallConfigPin;
111
+ }
112
+ /** The stable arm name stamped on `inferences.meta.arm` — the caller-declared
113
+ * name, else the pin's variant, else `provider/model`. Kept meta-safe (short, no
114
+ * whitespace) by construction: providers/models/variants are identifiers. */
115
+ export declare function armName(arm: ExperimentArm): string;
116
+ /** Narrow a jsonb `config` to a `CallConfigExperiment`, or null (T7 — jsonb is
117
+ * not proof of shape). Requires ≥1 well-formed arm (each `ref` a valid pin, each
118
+ * `weight` a positive finite number) and a fallback pin. Fail-open: a malformed
119
+ * experiment narrows to null and the caller serves the mirrored bundle. */
120
+ export declare function narrowExperiment(config: unknown): CallConfigExperiment | null;
121
+ /** FNV-1a 32-bit hash of a string → a stable uint32. Pure, dependency-free — the
122
+ * sticky-assignment key so the SAME unit key always draws the SAME arm (an A/B is
123
+ * measured honestly). Not cryptographic; a fast, well-distributed bucket hash. */
124
+ export declare function hashUnitKey(key: string): number;
125
+ export interface DrawArmOptions {
126
+ /** A stickiness unit key (already resolved from the input). When present the
127
+ * draw is DETERMINISTIC by `hash(unitKey)` — the same unit always lands on the
128
+ * same arm. Absent ⇒ a weighted random draw. */
129
+ readonly unitKey?: string;
130
+ /** Injectable RNG in [0,1) for the weighted-random path (tests seed it);
131
+ * defaults to Math.random. Ignored when `unitKey` is supplied. */
132
+ readonly rng?: () => number;
133
+ }
134
+ /**
135
+ * Draw one arm from an experiment (spec §4). Weighted by `weight`: an arm's draw
136
+ * probability is `weight / Σweights`. DETERMINISTIC by `hash(unitKey)` when a
137
+ * unit key is supplied (sticky assignment); weighted-random otherwise. Returns
138
+ * null only when there are no positive-weight arms (⇒ the caller serves the
139
+ * fallback). Pure over its inputs (the RNG/hash are the only entropy).
140
+ */
141
+ export declare function drawArm(experiment: CallConfigExperiment, opts?: DrawArmOptions): ExperimentArm | null;
142
+ /** The resolved bundle for a run PLUS the chosen arm name (or null for pin / no
143
+ * override / a non-experiment mode). The arm name is stamped on
144
+ * `inferences.meta.arm` (thread it into `run` via `RunOptions.arm`). */
145
+ export interface ResolvedForRun {
146
+ readonly bundle: RouteBundle;
147
+ readonly arm: string | null;
148
+ }
149
+ /**
150
+ * Resolve a route's call-config into a runnable bundle for ONE call, drawing an
151
+ * experiment arm locally when `mode='experiment'` (spec §4, R-1). FAIL-OPEN and
152
+ * byte-identical for pin: `mode='pin'` merges exactly like {@link applyCallConfig}
153
+ * (arm null), a null/`live`/malformed resolution returns the bundle unchanged
154
+ * (arm null), and a malformed experiment falls back to the experiment's own
155
+ * fallback pin, else the mirrored bundle. NEVER throws.
156
+ *
157
+ * For an experiment, the stickiness unit key is `opts.unitKey` when given, else
158
+ * the value of `input[experiment.stickyKey]` (resolved from the run's input) —
159
+ * so the SAME unit is served the SAME arm across calls.
160
+ */
161
+ export declare function resolveCallConfigForRun(bundle: RouteBundle, resolved: ResolvedCallConfig | null, opts?: {
162
+ readonly unitKey?: string;
163
+ readonly input?: Record<string, unknown>;
164
+ readonly rng?: () => number;
165
+ }): ResolvedForRun;
166
+ /**
167
+ * Build the cached call-config resolver. Holds a per-route cache with a fetch
168
+ * timestamp + ETag; a read within `ttlMs` returns the cache with no network; a
169
+ * stale read revalidates (If-None-Match) and folds the result in. Every fetch is
170
+ * fail-open: a rejection or a null leaves the last-known value in place, and a
171
+ * route never yet resolved returns null (⇒ serve the mirrored bundle unchanged).
172
+ */
173
+ export declare function createCallConfigResolver(config: CallConfigResolverConfig): CallConfigResolver;
174
+ export interface ApiCallConfigFetcherConfig {
175
+ /** Base URL of the hosted control plane — the SAME `ingestUrl` the api sink
176
+ * uses (the call-config path hangs off the host). */
177
+ readonly ingestUrl: string;
178
+ /** `rig_sk_…` key. GET needs no special scope (a read rides the readable route
179
+ * + config rows). Header only, never a URL (C2). */
180
+ readonly apiKey: string;
181
+ readonly fetchFn?: typeof fetch;
182
+ }
183
+ /**
184
+ * The api-mode call-config fetcher: one authenticated, ETag-aware GET per route.
185
+ * Mirrors the api sink's leak discipline (status only, never the body/URL in an
186
+ * error — C2). NEVER throws: a network/HTTP failure resolves null, a 304 resolves
187
+ * "not-modified", a malformed 200 resolves null — so the resolver stays fail-open.
188
+ */
189
+ export declare function createApiCallConfigFetcher(config: ApiCallConfigFetcherConfig): CallConfigFetcher;
190
+ /**
191
+ * Convenience: resolve `route`'s config through the resolver, then merge it over
192
+ * `bundle`. NEVER throws (delegates to `getCallConfig`'s fail-open) and returns
193
+ * the SAME bundle reference when there is no override (byte-identical).
194
+ */
195
+ export declare function getCallConfig(route: string, deps: {
196
+ readonly resolver: CallConfigResolver;
197
+ readonly bundle: RouteBundle;
198
+ }): Promise<RouteBundle>;
199
+ //# sourceMappingURL=call-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"call-config.d.ts","sourceRoot":"","sources":["../../src/routes/call-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAiB,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGjE,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,YAAY,GAAG,MAAM,CAAC;AAC3D,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,WAAW,GAAG,cAAc,CAAC;AAErE,+EAA+E;AAC/E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;6BAE6B;AAC7B,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;wDAEwD;AACxD,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,kBAAkB,GAAG,cAAc,GAAG,IAAI,CAAC,CAAC;CACrG;AAED,MAAM,WAAW,kBAAkB;IACjC;;;8CAG0C;IAC1C,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IACjE,4EAA4E;IAC5E,QAAQ,IAAI,WAAW,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CACrD;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,sEAAsE;IACtE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,wDAAwD;IACxD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAMD;;0DAE0D;AAC1D,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,kBAAkB,GAAG,IAAI,CAQxE;AAED;wEACwE;AACxE,wBAAgB,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CAmB/D;AAED;0DAC0D;AAC1D,wBAAgB,eAAe,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,GAAG,aAAa,GAAG,IAAI,CAGzF;AA+BD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,kBAAkB,GAAG,IAAI,GAAG,WAAW,CAIrG;AAID;;qFAEqF;AACrF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;+EAE+E;AAC/E,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,CAAC;IACxC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;CAClC;AAED;;6EAE6E;AAC7E,wBAAgB,OAAO,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAElD;AAED;;;2EAG2E;AAC3E,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,OAAO,GAAG,oBAAoB,GAAG,IAAI,CAoB7E;AAED;;kFAEkF;AAClF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO/C;AAED,MAAM,WAAW,cAAc;IAC7B;;oDAEgD;IAChD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;sEACkE;IAClE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,UAAU,EAAE,oBAAoB,EAAE,IAAI,GAAE,cAAmB,GAAG,aAAa,GAAG,IAAI,CAczG;AAED;;wEAEwE;AACxE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,WAAW,EACnB,QAAQ,EAAE,kBAAkB,GAAG,IAAI,EACnC,IAAI,GAAE;IAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAAO,GAC9G,cAAc,CAsBhB;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,wBAAwB,GAAG,kBAAkB,CAmC7F;AAED,MAAM,WAAW,0BAA0B;IACzC;yDACqD;IACrD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;wDACoD;IACpD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACjC;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,0BAA0B,GAAG,iBAAiB,CAwBhG;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE;IAAE,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GAC5E,OAAO,CAAC,WAAW,CAAC,CAGtB"}
@@ -0,0 +1,343 @@
1
+ "use strict";
2
+ /**
3
+ * SDK-side live call-config resolution (live-routing-evals spec §4, R-1; LRE-1a).
4
+ *
5
+ * A route's live call-config (which model/provider + sampling) can be edited in
6
+ * the console WITHOUT a redeploy. The SDK reads it just-before-call through
7
+ * `GET /v1/routes/:route/call-config`, caches the policy (short TTL, ETag-
8
+ * revalidated), and MERGES the pin over the resolved (mirrored/deploy-time)
9
+ * bundle before `run()`. This is R-1's cached-policy model: no per-call network
10
+ * cost when fresh, and outage-safe.
11
+ *
12
+ * ── FAIL-OPEN, ALWAYS — the hot path never throws ─────────────────────────
13
+ * `getCallConfig` NEVER throws. On any error it returns the LAST-KNOWN-GOOD
14
+ * cached policy; if there is none, it returns null — and a null resolution means
15
+ * "serve the mirrored/pinned bundle unchanged". A route with no override
16
+ * resolves BYTE-IDENTICAL to today (the golden invariant): `applyCallConfig`
17
+ * returns the SAME bundle reference when there is nothing to merge.
18
+ *
19
+ * ── PHASE 1 = PIN ONLY ────────────────────────────────────────────────────
20
+ * `mode='experiment'` (local weighted arm draw) and `mode='live'` (strict per-
21
+ * call read) are TYPED here so the wire shape is complete, but they are INERT:
22
+ * `applyCallConfig` acts on `mode='pin'` only and passes everything else through
23
+ * unchanged. Experiments ship in LRE-2.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.parseCallConfig = parseCallConfig;
27
+ exports.narrowPin = narrowPin;
28
+ exports.pinFromResolved = pinFromResolved;
29
+ exports.applyCallConfig = applyCallConfig;
30
+ exports.armName = armName;
31
+ exports.narrowExperiment = narrowExperiment;
32
+ exports.hashUnitKey = hashUnitKey;
33
+ exports.drawArm = drawArm;
34
+ exports.resolveCallConfigForRun = resolveCallConfigForRun;
35
+ exports.createCallConfigResolver = createCallConfigResolver;
36
+ exports.createApiCallConfigFetcher = createApiCallConfigFetcher;
37
+ exports.getCallConfig = getCallConfig;
38
+ function isMode(v) {
39
+ return v === "pin" || v === "experiment" || v === "live";
40
+ }
41
+ /** Narrow an unknown endpoint body into a `ResolvedCallConfig`, or null when the
42
+ * shape is not what the endpoint promises (a malformed 200 is a miss, not
43
+ * trusted — we never fabricate an override from junk). */
44
+ function parseCallConfig(body) {
45
+ if (typeof body !== "object" || body === null)
46
+ return null;
47
+ const b = body;
48
+ if (typeof b.version !== "number" || !Number.isInteger(b.version))
49
+ return null;
50
+ if (typeof b.etag !== "string")
51
+ return null;
52
+ const mode = b.mode === null ? null : isMode(b.mode) ? b.mode : undefined;
53
+ if (mode === undefined)
54
+ return null;
55
+ return { version: b.version, mode, config: b.config ?? null, etag: b.etag };
56
+ }
57
+ /** Narrow a config to a `CallConfigPin`, or null (T7 — jsonb is not proof of
58
+ * shape). The single place a pin's shape is trusted on the SDK side. */
59
+ function narrowPin(config) {
60
+ if (typeof config !== "object" || config === null || Array.isArray(config))
61
+ return null;
62
+ const c = config;
63
+ if (typeof c.provider !== "string" || c.provider === "")
64
+ return null;
65
+ if (typeof c.model !== "string" || c.model === "")
66
+ return null;
67
+ const pin = {
68
+ provider: c.provider,
69
+ model: c.model,
70
+ };
71
+ if (typeof c.sampling === "object" && c.sampling !== null && !Array.isArray(c.sampling)) {
72
+ const s = c.sampling;
73
+ const sampling = {};
74
+ if (typeof s.temperature === "number")
75
+ sampling.temperature = s.temperature;
76
+ if (typeof s.top_p === "number")
77
+ sampling.top_p = s.top_p;
78
+ if (typeof s.max_output_tokens === "number")
79
+ sampling.max_output_tokens = s.max_output_tokens;
80
+ pin.sampling = sampling;
81
+ }
82
+ if (typeof c.variant === "string")
83
+ pin.variant = c.variant;
84
+ return pin;
85
+ }
86
+ /** The pin carried by a resolved config, or null when there is no pin override
87
+ * (no override, or a non-pin mode — inert in Phase 1). */
88
+ function pinFromResolved(resolved) {
89
+ if (resolved === null || resolved.mode !== "pin")
90
+ return null;
91
+ return narrowPin(resolved.config);
92
+ }
93
+ /** Map the wire sampling (snake_case) to the SDK's `SamplingParams` (camelCase),
94
+ * merged over the bundle's existing sampling. Only present keys override. */
95
+ function mergeSampling(base, over) {
96
+ if (over === undefined)
97
+ return base;
98
+ const out = { ...(base ?? {}) };
99
+ if (over.temperature !== undefined)
100
+ out.temperature = over.temperature;
101
+ if (over.top_p !== undefined)
102
+ out.topP = over.top_p;
103
+ if (over.max_output_tokens !== undefined)
104
+ out.maxOutputTokens = over.max_output_tokens;
105
+ return out;
106
+ }
107
+ /** Merge a single narrowed pin over a bundle — REPLACES the candidate set with
108
+ * the pinned model (eligibility was gated at write) and folds its sampling into
109
+ * the policy. The one place a pin becomes a runnable bundle; shared by the pin
110
+ * path and the experiment arm-draw so both merge identically. */
111
+ function applyPin(bundle, pin) {
112
+ const pinnedCandidate = {
113
+ provider: pin.provider,
114
+ model: pin.model,
115
+ trial: false,
116
+ };
117
+ const sampling = mergeSampling(bundle.policy.sampling, pin.sampling);
118
+ return {
119
+ ...bundle,
120
+ candidates: [pinnedCandidate],
121
+ policy: { ...bundle.policy, ...(sampling !== undefined ? { sampling } : {}) },
122
+ };
123
+ }
124
+ /**
125
+ * Merge a resolved call-config over a bundle, returning a runnable bundle.
126
+ *
127
+ * INERT BY DEFAULT: when `resolved` is null, or its mode is not `pin`, or the pin
128
+ * does not narrow, the SAME `bundle` reference is returned — byte-identical to
129
+ * today (the golden invariant). Otherwise the pin REPLACES the candidate set with
130
+ * the single pinned model (eligibility was gated at write, so it satisfies the
131
+ * route `require`) and merges its sampling into the policy.
132
+ *
133
+ * `mode='experiment'` is INERT here (returns the bundle unchanged): an experiment
134
+ * resolves through {@link resolveCallConfigForRun}, which draws an arm and returns
135
+ * the arm name for `inferences.meta.arm`. `applyCallConfig` stays pin-only so the
136
+ * pin/golden path is byte-identical.
137
+ */
138
+ function applyCallConfig(bundle, resolved) {
139
+ const pin = pinFromResolved(resolved);
140
+ if (pin === null)
141
+ return bundle;
142
+ return applyPin(bundle, pin);
143
+ }
144
+ /** The stable arm name stamped on `inferences.meta.arm` — the caller-declared
145
+ * name, else the pin's variant, else `provider/model`. Kept meta-safe (short, no
146
+ * whitespace) by construction: providers/models/variants are identifiers. */
147
+ function armName(arm) {
148
+ return arm.name ?? arm.ref.variant ?? `${arm.ref.provider}/${arm.ref.model}`;
149
+ }
150
+ /** Narrow a jsonb `config` to a `CallConfigExperiment`, or null (T7 — jsonb is
151
+ * not proof of shape). Requires ≥1 well-formed arm (each `ref` a valid pin, each
152
+ * `weight` a positive finite number) and a fallback pin. Fail-open: a malformed
153
+ * experiment narrows to null and the caller serves the mirrored bundle. */
154
+ function narrowExperiment(config) {
155
+ if (typeof config !== "object" || config === null || Array.isArray(config))
156
+ return null;
157
+ const c = config;
158
+ if (!Array.isArray(c.arms) || c.arms.length === 0)
159
+ return null;
160
+ const fallback = narrowPin(c.fallback);
161
+ if (fallback === null)
162
+ return null;
163
+ const arms = [];
164
+ for (const raw of c.arms) {
165
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
166
+ return null;
167
+ const r = raw;
168
+ const ref = narrowPin(r.ref);
169
+ if (ref === null)
170
+ return null;
171
+ if (typeof r.weight !== "number" || !Number.isFinite(r.weight) || r.weight <= 0)
172
+ return null;
173
+ const arm = { ref, weight: r.weight };
174
+ if (typeof r.name === "string" && r.name !== "")
175
+ arm.name = r.name;
176
+ arms.push(arm);
177
+ }
178
+ const exp = { arms, fallback };
179
+ if (typeof c.stickyKey === "string" && c.stickyKey !== "")
180
+ exp.stickyKey = c.stickyKey;
181
+ return exp;
182
+ }
183
+ /** FNV-1a 32-bit hash of a string → a stable uint32. Pure, dependency-free — the
184
+ * sticky-assignment key so the SAME unit key always draws the SAME arm (an A/B is
185
+ * measured honestly). Not cryptographic; a fast, well-distributed bucket hash. */
186
+ function hashUnitKey(key) {
187
+ let h = 0x811c9dc5;
188
+ for (let i = 0; i < key.length; i += 1) {
189
+ h ^= key.charCodeAt(i);
190
+ h = Math.imul(h, 0x01000193);
191
+ }
192
+ return h >>> 0;
193
+ }
194
+ /**
195
+ * Draw one arm from an experiment (spec §4). Weighted by `weight`: an arm's draw
196
+ * probability is `weight / Σweights`. DETERMINISTIC by `hash(unitKey)` when a
197
+ * unit key is supplied (sticky assignment); weighted-random otherwise. Returns
198
+ * null only when there are no positive-weight arms (⇒ the caller serves the
199
+ * fallback). Pure over its inputs (the RNG/hash are the only entropy).
200
+ */
201
+ function drawArm(experiment, opts = {}) {
202
+ const arms = experiment.arms.filter((a) => a.weight > 0);
203
+ if (arms.length === 0)
204
+ return null;
205
+ const total = arms.reduce((sum, a) => sum + a.weight, 0);
206
+ // A point in [0,total): a deterministic fraction of `total` from the unit-key
207
+ // hash (sticky), else a random fraction. NEVER lands exactly on `total` (the
208
+ // hash is divided by 2^32, and rng() < 1), so the last arm is not over-picked.
209
+ const fraction = opts.unitKey !== undefined ? hashUnitKey(opts.unitKey) / 0x100000000 : (opts.rng ?? Math.random)();
210
+ let point = fraction * total;
211
+ for (const arm of arms) {
212
+ point -= arm.weight;
213
+ if (point < 0)
214
+ return arm;
215
+ }
216
+ return arms[arms.length - 1]; // floating-point guard — the last arm owns the tail.
217
+ }
218
+ /**
219
+ * Resolve a route's call-config into a runnable bundle for ONE call, drawing an
220
+ * experiment arm locally when `mode='experiment'` (spec §4, R-1). FAIL-OPEN and
221
+ * byte-identical for pin: `mode='pin'` merges exactly like {@link applyCallConfig}
222
+ * (arm null), a null/`live`/malformed resolution returns the bundle unchanged
223
+ * (arm null), and a malformed experiment falls back to the experiment's own
224
+ * fallback pin, else the mirrored bundle. NEVER throws.
225
+ *
226
+ * For an experiment, the stickiness unit key is `opts.unitKey` when given, else
227
+ * the value of `input[experiment.stickyKey]` (resolved from the run's input) —
228
+ * so the SAME unit is served the SAME arm across calls.
229
+ */
230
+ function resolveCallConfigForRun(bundle, resolved, opts = {}) {
231
+ try {
232
+ if (resolved === null || resolved.mode === "live")
233
+ return { bundle, arm: null };
234
+ if (resolved.mode === "pin")
235
+ return { bundle: applyCallConfig(bundle, resolved), arm: null };
236
+ // mode === "experiment"
237
+ const experiment = narrowExperiment(resolved.config);
238
+ if (experiment === null)
239
+ return { bundle, arm: null }; // fail-open: inert.
240
+ const unitKey = opts.unitKey ??
241
+ (experiment.stickyKey !== undefined && opts.input !== undefined && opts.input[experiment.stickyKey] !== undefined
242
+ ? String(opts.input[experiment.stickyKey])
243
+ : undefined);
244
+ const arm = drawArm(experiment, {
245
+ ...(unitKey !== undefined ? { unitKey } : {}),
246
+ ...(opts.rng !== undefined ? { rng: opts.rng } : {}),
247
+ });
248
+ if (arm === null)
249
+ return { bundle: applyPin(bundle, experiment.fallback), arm: null };
250
+ return { bundle: applyPin(bundle, arm.ref), arm: armName(arm) };
251
+ }
252
+ catch {
253
+ // The hot path never throws — an unexpected shape serves the mirrored bundle.
254
+ return { bundle, arm: null };
255
+ }
256
+ }
257
+ /**
258
+ * Build the cached call-config resolver. Holds a per-route cache with a fetch
259
+ * timestamp + ETag; a read within `ttlMs` returns the cache with no network; a
260
+ * stale read revalidates (If-None-Match) and folds the result in. Every fetch is
261
+ * fail-open: a rejection or a null leaves the last-known value in place, and a
262
+ * route never yet resolved returns null (⇒ serve the mirrored bundle unchanged).
263
+ */
264
+ function createCallConfigResolver(config) {
265
+ const now = config.now ?? (() => Date.now());
266
+ const cache = new Map();
267
+ return {
268
+ async getCallConfig(route) {
269
+ const entry = cache.get(route);
270
+ if (entry !== undefined && now() - entry.fetchedAt < config.ttlMs) {
271
+ return entry.value;
272
+ }
273
+ try {
274
+ const result = await config.fetcher.fetch(route, entry?.value.etag);
275
+ if (result === "not-modified") {
276
+ // Fresh again — bump the timestamp, keep the value.
277
+ if (entry !== undefined)
278
+ cache.set(route, { value: entry.value, fetchedAt: now() });
279
+ return entry?.value ?? null;
280
+ }
281
+ if (result === null) {
282
+ // Miss/error — fail-open to last-known-good (or null the first time).
283
+ return entry?.value ?? null;
284
+ }
285
+ cache.set(route, { value: result, fetchedAt: now() });
286
+ return result;
287
+ }
288
+ catch {
289
+ // The hot path never throws — fail-open to last-known-good.
290
+ return entry?.value ?? null;
291
+ }
292
+ },
293
+ snapshot() {
294
+ const out = new Map();
295
+ for (const [route, entry] of cache)
296
+ out.set(route, entry.value);
297
+ return out;
298
+ },
299
+ };
300
+ }
301
+ /**
302
+ * The api-mode call-config fetcher: one authenticated, ETag-aware GET per route.
303
+ * Mirrors the api sink's leak discipline (status only, never the body/URL in an
304
+ * error — C2). NEVER throws: a network/HTTP failure resolves null, a 304 resolves
305
+ * "not-modified", a malformed 200 resolves null — so the resolver stays fail-open.
306
+ */
307
+ function createApiCallConfigFetcher(config) {
308
+ const base = config.ingestUrl.replace(/\/$/, "");
309
+ const fetchFn = config.fetchFn ?? fetch;
310
+ return {
311
+ async fetch(route, etag) {
312
+ const url = `${base}/v1/routes/${encodeURIComponent(route)}/call-config`;
313
+ let response;
314
+ try {
315
+ response = await fetchFn(url, {
316
+ method: "GET",
317
+ headers: {
318
+ authorization: `Bearer ${config.apiKey}`,
319
+ ...(etag !== undefined ? { "if-none-match": etag } : {}),
320
+ },
321
+ });
322
+ }
323
+ catch {
324
+ return null; // network throw — fail-open, keep last-known.
325
+ }
326
+ if (response.status === 304)
327
+ return "not-modified";
328
+ if (!response.ok)
329
+ return null;
330
+ const body = (await response.json().catch(() => null));
331
+ return parseCallConfig(body);
332
+ },
333
+ };
334
+ }
335
+ /**
336
+ * Convenience: resolve `route`'s config through the resolver, then merge it over
337
+ * `bundle`. NEVER throws (delegates to `getCallConfig`'s fail-open) and returns
338
+ * the SAME bundle reference when there is no override (byte-identical).
339
+ */
340
+ async function getCallConfig(route, deps) {
341
+ const resolved = await deps.resolver.getCallConfig(route);
342
+ return applyCallConfig(deps.bundle, resolved);
343
+ }
@@ -404,10 +404,10 @@ function loadBundleFile(filePath) {
404
404
  // RB-1 (near-optimal spec §3): objective knob, reference arm + judge, value
405
405
  // checks — all additive; candidate-set invariant enforced in parseRb1Fields.
406
406
  const rb1 = (0, objective_loader_1.parseRb1Fields)(doc, route, candidates, problems);
407
- // CS-1 Proven Masking (spec §3): resolve the `mask:` block + its local
408
- // dictionary here so an unreadable dictionary or unknown type is a LOAD error.
409
- // Absent / `mode: off` ⇒ undefined ⇒ byte-identical (the golden test).
407
+ // CS-1 `mask:` (spec §3) + SC-1a `screen:` (spec §4.1) + SC-2 `classification:`/`phi` posture gate (spec-sc2 §4): resolved here so an unreadable dictionary / unknown detector-or-action / loosening sources key / mis-postured PHI route is a LOAD error (absent / `mode: off` ⇒ undefined ⇒ byte-identical). Classification + phi-gate live in screen-loader.ts (LOC cap).
410
408
  const mask = (0, mask_loader_1.parseMaskBlock)(doc.mask, bundleDir, problems);
409
+ const screen = (0, screen_loader_1.parseScreenBlock)(doc.screen, problems);
410
+ const classification = (0, screen_loader_1.parseClassificationAndPhiGate)(doc.classification, { mask, screen, require }, problems);
411
411
  let systemTemplate = "";
412
412
  let variables = [];
413
413
  let variableTrust;
@@ -480,6 +480,8 @@ function loadBundleFile(filePath) {
480
480
  ...(groundingAssembly !== undefined ? { groundingAssembly } : {}),
481
481
  ...(variableTrust !== undefined ? { variableTrust } : {}),
482
482
  ...(mask !== undefined ? { mask } : {}),
483
+ ...(screen !== undefined ? { screen } : {}),
484
+ ...(classification !== undefined ? { classification } : {}),
483
485
  // Network-effect instrumentation (spec WS6): computed once at load,
484
486
  // stamped on every telemetry row for this route.
485
487
  fingerprintCell: (0, fingerprint_1.fingerprintSchema)(outputSchema)?.cellKey ?? null,
@@ -13,7 +13,19 @@ export declare function parseRb1Fields(doc: Record<string, unknown>, route: stri
13
13
  objective?: RouteObjective;
14
14
  bakeoff?: BakeoffConfig;
15
15
  valueChecks?: readonly ValueCheck[];
16
+ grade?: {
17
+ passThreshold: number;
18
+ };
16
19
  };
20
+ /** Default grade pass threshold when a route declares no `grade:` block (R2). */
21
+ export declare const DEFAULT_GRADE_PASS_THRESHOLD = 0.5;
22
+ /** Parse the optional `grade:` block (grade primitive, R2). Currently one knob,
23
+ * `pass_threshold` (0–1). Declared + validated here but UNREAD until GP-2 wires
24
+ * verdict derivation. Absent ⇒ undefined (⇒ the default 0.5 is applied downstream).
25
+ * A malformed value pushes a teaching problem, exactly like every other field. */
26
+ export declare function parseGradeBlock(raw: unknown, problems: string[]): {
27
+ passThreshold: number;
28
+ } | undefined;
17
29
  /** Parse the `objective:` block. Absent ⇒ undefined (the engine defaults to
18
30
  * quality-first with no bound: regret shown, nothing proposed). */
19
31
  export declare function parseObjective(raw: unknown, problems: string[]): RouteObjective | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"objective-loader.d.ts","sourceRoot":"","sources":["../../src/routes/objective-loader.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EACV,aAAa,EACb,aAAa,EAGb,cAAc,EACd,UAAU,EACX,MAAM,gBAAgB,CAAC;AAExB;;2EAE2E;AAC3E,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,SAAS,aAAa,EAAE,EACpC,QAAQ,EAAE,MAAM,EAAE,GACjB;IAAE,SAAS,CAAC,EAAE,cAAc,CAAC;IAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IAAC,WAAW,CAAC,EAAE,SAAS,UAAU,EAAE,CAAA;CAAE,CAe9F;AAoCD;mEACmE;AACnE,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,cAAc,GAAG,SAAS,CAqD3F;AAqCD;;2EAE2E;AAC3E,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,OAAO,EACZ,UAAU,EAAE,SAAS,aAAa,EAAE,EACpC,QAAQ,EAAE,MAAM,EAAE,GACjB,aAAa,GAAG,SAAS,CAwE3B;AAiCD;uCACuC;AACvC,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAS,UAAU,EAAE,GAAG,SAAS,CAqCpG;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAUvG"}
1
+ {"version":3,"file":"objective-loader.d.ts","sourceRoot":"","sources":["../../src/routes/objective-loader.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EACV,aAAa,EACb,aAAa,EAGb,cAAc,EACd,UAAU,EACX,MAAM,gBAAgB,CAAC;AAExB;;2EAE2E;AAC3E,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,SAAS,aAAa,EAAE,EACpC,QAAQ,EAAE,MAAM,EAAE,GACjB;IACD,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,WAAW,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IACpC,KAAK,CAAC,EAAE;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC;CACnC,CAmBA;AAYD,iFAAiF;AACjF,eAAO,MAAM,4BAA4B,MAAM,CAAC;AAEhD;;;kFAGkF;AAClF,wBAAgB,eAAe,CAC7B,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,MAAM,EAAE,GACjB;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAavC;AA0BD;mEACmE;AACnE,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,cAAc,GAAG,SAAS,CAqD3F;AAqCD;;2EAE2E;AAC3E,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,OAAO,EACZ,UAAU,EAAE,SAAS,aAAa,EAAE,EACpC,QAAQ,EAAE,MAAM,EAAE,GACjB,aAAa,GAAG,SAAS,CA+F3B;AAoED;uCACuC;AACvC,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAS,UAAU,EAAE,GAAG,SAAS,CAqCpG;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAUvG"}