modelrig 0.8.1 → 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 (245) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +4 -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 +95 -7
  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
@@ -16,13 +16,17 @@ exports.jsonModeCoaching = jsonModeCoaching;
16
16
  exports.buildAdapterRequest = buildAdapterRequest;
17
17
  exports.buildRepairAdapterRequest = buildRepairAdapterRequest;
18
18
  exports.dispatchCandidate = dispatchCandidate;
19
+ exports.buildRunGuards = buildRunGuards;
19
20
  exports.buildDispatchMaybeMasked = buildDispatchMaybeMasked;
20
21
  exports.emitToolCallResult = emitToolCallResult;
22
+ const provider_1 = require("../types/provider");
21
23
  const node_crypto_1 = require("node:crypto");
22
24
  const attempt_meta_1 = require("../telemetry/attempt-meta");
23
25
  const cache_stamp_1 = require("./cache-stamp");
24
26
  const cache_handle_1 = require("./cache-handle");
25
27
  const seam_1 = require("../mask/seam");
28
+ const grounding_1 = require("./grounding");
29
+ const screen_1 = require("../screen/screen");
26
30
  /** Assemble the recorder's `metaInputs` from a completed dispatch (WI-1) — the
27
31
  * adapter's native meta plus the rendered request (system+user+schema, hashed)
28
32
  * and the output text (hashed). One helper so the routed lane's several record
@@ -181,36 +185,114 @@ async function dispatchCandidate(adapter, req, onDelta) {
181
185
  },
182
186
  });
183
187
  }
188
+ /** Build the per-run guards for a route (called ONCE at run start). The mask
189
+ * vault is run-scoped (shared across attempts); the screen policy is resolved at
190
+ * load and reused. Null members ⇒ that guard is inert (byte-identical). */
191
+ function buildRunGuards(route, newVault) {
192
+ return {
193
+ mask: route.mask !== undefined ? { policy: route.mask, vault: newVault() } : null,
194
+ screen: route.screen ?? null,
195
+ };
196
+ }
197
+ function invariantResult(parts, why) {
198
+ return {
199
+ ok: false,
200
+ failure: { class: "invariant_violation", message: why, provider: parts.candidate.provider, model: parts.candidate.model },
201
+ };
202
+ }
184
203
  /**
185
- * Build the primary AdapterRequest, mask its outgoing user slot when the route
186
- * has an active policy (spec §1.2 — "rewrites the AdapterRequest before dispatch
187
- * and rehydrates the AdapterResult after"), dispatch, and time it. With no mask
188
- * context this is byte-identical to `buildAdapterRequest` + `dispatchCandidate`
189
- * (the golden test). The trusted system slot is never masked (B3 keeps untrusted
190
- * content in the user path).
204
+ * Assemble the untrusted block, SCREEN it (spec §4 — the request-side seam),
205
+ * MASK the user slot (CS-1, screening runs BEFORE masking so mask tokens never
206
+ * confuse a detector), dispatch, and apply the output-side screen (canary /
207
+ * citations / Unicode strip + quarantine drop-count). With both guards null this
208
+ * is byte-identical to `buildAdapterRequest(assembled)` + `dispatchCandidate`
209
+ * (the golden test) — the untrusted-block assembly moved here from run.ts so the
210
+ * loop stays under the LOC cap (spec §10(i)). The trusted system slot is never
211
+ * masked (B3 keeps untrusted content in the user path).
191
212
  */
192
- async function buildDispatchMaybeMasked(adapter, parts, onDelta, mask) {
193
- const built = buildAdapterRequest(parts);
194
- if (mask === null) {
195
- const started = Date.now();
196
- const result = await dispatchCandidate(adapter, built, onDelta);
197
- return { request: built, result, latencyMs: Date.now() - started, seam: null };
213
+ async function buildDispatchMaybeMasked(adapter, parts, onDelta, guards) {
214
+ const provider = parts.candidate.provider;
215
+ // The untrusted segments this route+input contribute (search / x-trust
216
+ // variable / proxy_user). Empty on a plain route ⇒ empty appends below.
217
+ const segments = (0, grounding_1.collectUntrustedSegments)(parts.route, parts.opts.input, parts.injectedSources ?? null, parts.opts.proxyTrust);
218
+ // Request-side screening (spec §4.2/§4.3). A `block` outcome fails closed
219
+ // pre-dispatch (no request sent). `strip` filters segments; `quarantine` gates
220
+ // tool calls below. Screening runs on the RAW untrusted content, before mask.
221
+ const screenSeam = guards.screen !== null ? (0, screen_1.createScreenSeam)(guards.screen) : null;
222
+ let keep = segments;
223
+ let quarantine = false;
224
+ if (screenSeam !== null) {
225
+ const decided = await screenSeam.screenRequest(segments);
226
+ if (decided.kind === "block") {
227
+ return {
228
+ request: buildAdapterRequest(parts),
229
+ result: invariantResult(parts, "screening resolved block (untrusted content matched an enforce rule)"),
230
+ latencyMs: 0,
231
+ seam: null,
232
+ screenRefusal: "block",
233
+ screenReceipt: screenSeam.receipt(),
234
+ };
235
+ }
236
+ keep = decided.keepSegments;
237
+ quarantine = decided.quarantine;
198
238
  }
199
- const seam = (0, seam_1.createMaskSeam)(mask.policy, mask.vault);
200
- const outcome = seam.maskRequest(built);
201
- if (outcome.kind !== "ok") {
202
- // Fail closed: no request is sent. The run loop refuses pre-dispatch.
203
- return {
204
- request: built,
205
- result: { ok: false, failure: { class: "invariant_violation", message: `masking failed closed (${outcome.kind})`, provider: parts.candidate.provider, model: parts.candidate.model } },
206
- latencyMs: 0,
207
- seam,
208
- maskRefusal: outcome.kind,
209
- };
239
+ // Assemble the (post-strip) untrusted appends + the optional canary into the
240
+ // trusted system slot. Screening-OFF ⇒ `keep === segments` ⇒ byte-identical.
241
+ const { systemAppend, userAppend } = (0, grounding_1.assembleUntrustedAppends)(keep, provider);
242
+ const canaryAppend = screenSeam?.canaryToken != null ? `\n\n${screenSeam.canaryToken}` : "";
243
+ const assembledParts = {
244
+ ...parts,
245
+ systemPrompt: parts.systemPrompt + systemAppend + canaryAppend,
246
+ userDirective: parts.userDirective + userAppend,
247
+ };
248
+ let built = buildAdapterRequest(assembledParts);
249
+ // Quarantine (spec §4.2): prevent the flagged turn from actuating a tool call
250
+ // via `tool_choice: "none"` where the served model supports it; otherwise fall
251
+ // back to dropping + counting tool calls in the response (A7, honest).
252
+ const capable = (0, provider_1.toolChoiceNoneCapable)(provider);
253
+ if (quarantine && capable)
254
+ built = { ...built, toolChoice: "none" };
255
+ // Mask the (screened) user slot. Fail-closed refusal is unchanged (CS-1 §9).
256
+ let request = built;
257
+ let seam = null;
258
+ if (guards.mask !== null) {
259
+ seam = (0, seam_1.createMaskSeam)(guards.mask.policy, guards.mask.vault);
260
+ const outcome = seam.maskRequest(built);
261
+ if (outcome.kind !== "ok") {
262
+ return {
263
+ request: built,
264
+ result: invariantResult(parts, `masking failed closed (${outcome.kind})`),
265
+ latencyMs: 0,
266
+ seam,
267
+ maskRefusal: outcome.kind,
268
+ ...(screenSeam !== null ? { screenReceipt: screenSeam.receipt() } : {}),
269
+ };
270
+ }
271
+ request = outcome.request;
210
272
  }
211
273
  const started = Date.now();
212
- const result = await dispatchCandidate(adapter, outcome.request, onDelta);
213
- return { request: outcome.request, result, latencyMs: Date.now() - started, seam };
274
+ let result = await dispatchCandidate(adapter, request, onDelta);
275
+ const latencyMs = Date.now() - started;
276
+ // Output-side screening (spec §2.3): strip Unicode tags, allowlist citations,
277
+ // detect canary leak, and — on a quarantined turn without tool_choice parity —
278
+ // DROP + COUNT any tool calls the model emitted (A7).
279
+ if (screenSeam !== null && result.ok) {
280
+ if (quarantine && result.toolCalls !== undefined && result.toolCalls.length > 0) {
281
+ screenSeam.noteQuarantineDrop(result.toolCalls.length);
282
+ result = { ...result, toolCalls: [] };
283
+ }
284
+ const injectedUrls = (parts.injectedSources ?? []).map((s) => s.url);
285
+ const cleaned = screenSeam.screenResponse(result.text, injectedUrls);
286
+ if (cleaned !== result.text)
287
+ result = { ...result, text: cleaned };
288
+ }
289
+ return {
290
+ request,
291
+ result,
292
+ latencyMs,
293
+ seam,
294
+ ...(screenSeam !== null ? { screenReceipt: screenSeam.receipt() } : {}),
295
+ };
214
296
  }
215
297
  /**
216
298
  * Record a tool-call turn and build its RunResult (spec §4.4). A tool call is
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import type { RouteBundle } from "../types/route";
18
18
  import { type ProviderId } from "../types/provider";
19
+ import { type ContentSegment } from "../screen/provenance";
19
20
  export interface SearchResultItem {
20
21
  readonly title: string;
21
22
  readonly url: string;
@@ -85,11 +86,36 @@ export declare const SEARCH_SNIPPET_MAX_CHARS = 500;
85
86
  * untrusted-input policy sentence is owned by the provenance assembler and added
86
87
  * to the system slot via `untrustedSystemAppend` — screening F2, single owner). */
87
88
  export declare function groundingSystemAppend(assembly: RouteBundle["groundingAssembly"], results: readonly SearchResultItem[], retrievedAt: string): string;
89
+ /** S3 (SC-0, spec §11.3): the effective system-slot removal set for the template
90
+ * renderer = `keys(variableTrust)` ∪ `proxyTrust`, expressed in the SAME shape the
91
+ * x-trust removal path consumes (`Record<name, "untrusted">`, routes/template.ts).
92
+ * A name in `proxyTrust` therefore renders EMPTY in the trusted system slot,
93
+ * reusing that path rather than forking it. Absent/empty `proxyTrust` ⇒ the
94
+ * `variableTrust` reference is returned UNCHANGED, so the render is byte-identical. */
95
+ export declare function systemSlotRemoval(variableTrust: Readonly<Record<string, "untrusted">> | undefined, proxyTrust: readonly string[] | undefined): Readonly<Record<string, "untrusted">> | undefined;
96
+ /** Screening S1 + S2 (SC-0 B3) + S3 (SC-0): the untrusted segments a route+input
97
+ * contribute — (a) each name in `proxyTrust` present in input, tagged
98
+ * `proxy_user` (S3); (b) each `x-trust: untrusted` variable (S2), tagged
99
+ * `variable`; and (c), when `grounding.assembly: spotlight`, the search results
100
+ * (S1), tagged `search`. Shared by the system- and user-slot builders so the two
101
+ * slots agree on "is anything untrusted present" (screening F2). Empty ⇒ nothing
102
+ * untrusted (byte-identical default — `proxyTrust` is unset in S3). */
103
+ export declare function collectUntrustedSegments(route: Pick<RouteBundle, "groundingAssembly" | "variableTrust">, input: Record<string, unknown>, injectedSources: readonly SearchResultItem[] | null, proxyTrust?: readonly string[]): ContentSegment[];
88
104
  /** Screening S1 + S2 (SC-0 B3): the USER-slot untrusted block appended after the
89
105
  * user directive. Assembles the untrusted segments into a single spotlighted,
90
106
  * JSON-encoded, delimiter-wrapped block via the provenance assembler. Returns ""
91
107
  * when there is nothing untrusted (byte-identical default). */
92
- export declare function untrustedUserBlock(route: Pick<RouteBundle, "groundingAssembly" | "variableTrust">, input: Record<string, unknown>, injectedSources: readonly SearchResultItem[] | null, provider: ProviderId): string;
108
+ export declare function untrustedUserBlock(route: Pick<RouteBundle, "groundingAssembly" | "variableTrust">, input: Record<string, unknown>, injectedSources: readonly SearchResultItem[] | null, provider: ProviderId, proxyTrust?: readonly string[]): string;
109
+ /** SC-1a: assemble a GIVEN segment list into the {systemAppend, userAppend} the
110
+ * dispatch helper appends. Byte-identical to `untrustedSystemAppend` +
111
+ * `untrustedUserBlock` when `segments` is the full collected set (same
112
+ * assemblePrompt calls, same nonce semantics) — so the screening-OFF path stays
113
+ * golden. The screening path passes the POST-strip `keepSegments`, so a stripped
114
+ * segment vanishes from both slots. Returns empty appends for an empty list. */
115
+ export declare function assembleUntrustedAppends(segments: readonly ContentSegment[], provider: ProviderId): {
116
+ readonly systemAppend: string;
117
+ readonly userAppend: string;
118
+ };
93
119
  /** Screening F2 (SC-0 B3): the SYSTEM-slot untrusted-input policy sentence. The
94
120
  * provenance assembler is the SINGLE owner of this sentence (spec §2.2); this
95
121
  * surfaces its system-slot output whenever ANY untrusted segment is delivered —
@@ -97,5 +123,5 @@ export declare function untrustedUserBlock(route: Pick<RouteBundle, "groundingAs
97
123
  * template still gets its "this is data" framing. Returns "" when there is
98
124
  * nothing untrusted (byte-identical default). The assembler's system slot does
99
125
  * not depend on the nonce or the block shape, so fixed placeholders suffice. */
100
- export declare function untrustedSystemAppend(route: Pick<RouteBundle, "groundingAssembly" | "variableTrust">, input: Record<string, unknown>, injectedSources: readonly SearchResultItem[] | null): string;
126
+ export declare function untrustedSystemAppend(route: Pick<RouteBundle, "groundingAssembly" | "variableTrust">, input: Record<string, unknown>, injectedSources: readonly SearchResultItem[] | null, proxyTrust?: readonly string[]): string;
101
127
  //# sourceMappingURL=grounding.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"grounding.d.ts","sourceRoot":"","sources":["../../src/run/grounding.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"grounding.d.ts","sourceRoot":"","sources":["../../src/run/grounding.ts"],"names":[],"mappings":"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"}
@@ -24,7 +24,10 @@ exports.extractCitations = extractCitations;
24
24
  exports.countDroppedCitations = countDroppedCitations;
25
25
  exports.groundingMeta = groundingMeta;
26
26
  exports.groundingSystemAppend = groundingSystemAppend;
27
+ exports.systemSlotRemoval = systemSlotRemoval;
28
+ exports.collectUntrustedSegments = collectUntrustedSegments;
27
29
  exports.untrustedUserBlock = untrustedUserBlock;
30
+ exports.assembleUntrustedAppends = assembleUntrustedAppends;
28
31
  exports.untrustedSystemAppend = untrustedSystemAppend;
29
32
  const node_crypto_1 = require("node:crypto");
30
33
  const provider_1 = require("../types/provider");
@@ -141,17 +144,45 @@ function groundingSystemAppend(assembly, results, retrievedAt) {
141
144
  return "";
142
145
  return searchInjectionBlock(results, retrievedAt); // legacy default — byte-identical
143
146
  }
144
- /** Screening S1 + S2 (SC-0 B3): the untrusted segments a route+input contribute
145
- * — (a) each `x-trust: untrusted` variable (S2) and (b), when
146
- * `grounding.assembly: spotlight`, the search results (S1). Shared by the system-
147
- * and user-slot builders so the two slots agree on "is anything untrusted
148
- * present" (screening F2). Empty ⇒ nothing untrusted (byte-identical default). */
149
- function collectUntrustedSegments(route, input, injectedSources) {
147
+ /** S3 (SC-0, spec §11.3): the effective system-slot removal set for the template
148
+ * renderer = `keys(variableTrust)` ∪ `proxyTrust`, expressed in the SAME shape the
149
+ * x-trust removal path consumes (`Record<name, "untrusted">`, routes/template.ts).
150
+ * A name in `proxyTrust` therefore renders EMPTY in the trusted system slot,
151
+ * reusing that path rather than forking it. Absent/empty `proxyTrust` ⇒ the
152
+ * `variableTrust` reference is returned UNCHANGED, so the render is byte-identical. */
153
+ function systemSlotRemoval(variableTrust, proxyTrust) {
154
+ if (proxyTrust === undefined || proxyTrust.length === 0)
155
+ return variableTrust;
156
+ const merged = { ...(variableTrust ?? {}) };
157
+ for (const name of proxyTrust)
158
+ merged[name] = "untrusted";
159
+ return merged;
160
+ }
161
+ /** Screening S1 + S2 (SC-0 B3) + S3 (SC-0): the untrusted segments a route+input
162
+ * contribute — (a) each name in `proxyTrust` present in input, tagged
163
+ * `proxy_user` (S3); (b) each `x-trust: untrusted` variable (S2), tagged
164
+ * `variable`; and (c), when `grounding.assembly: spotlight`, the search results
165
+ * (S1), tagged `search`. Shared by the system- and user-slot builders so the two
166
+ * slots agree on "is anything untrusted present" (screening F2). Empty ⇒ nothing
167
+ * untrusted (byte-identical default — `proxyTrust` is unset in S3). */
168
+ function collectUntrustedSegments(route, input, injectedSources, proxyTrust) {
150
169
  const segments = [];
170
+ // S3: each per-call proxyTrust name present in input is `proxy_user` untrusted
171
+ // content — the request-side isolation seam for the slug/proxy lane.
172
+ const proxyNames = new Set(proxyTrust ?? []);
173
+ for (const name of proxyNames) {
174
+ const value = input[name];
175
+ if (value === undefined)
176
+ continue;
177
+ const text = typeof value === "string" ? value : JSON.stringify(value);
178
+ segments.push({ source: "proxy_user", tier: "untrusted", text });
179
+ }
151
180
  // S2: each variable annotated `x-trust: untrusted` moves out of the system slot.
152
181
  for (const [name, trust] of Object.entries(route.variableTrust ?? {})) {
153
182
  if (trust !== "untrusted")
154
183
  continue;
184
+ if (proxyNames.has(name))
185
+ continue; // proxy_user takes precedence — never double-emit
155
186
  const value = input[name];
156
187
  if (value === undefined)
157
188
  continue;
@@ -173,13 +204,26 @@ function collectUntrustedSegments(route, input, injectedSources) {
173
204
  * user directive. Assembles the untrusted segments into a single spotlighted,
174
205
  * JSON-encoded, delimiter-wrapped block via the provenance assembler. Returns ""
175
206
  * when there is nothing untrusted (byte-identical default). */
176
- function untrustedUserBlock(route, input, injectedSources, provider) {
177
- const segments = collectUntrustedSegments(route, input, injectedSources);
207
+ function untrustedUserBlock(route, input, injectedSources, provider, proxyTrust) {
208
+ const segments = collectUntrustedSegments(route, input, injectedSources, proxyTrust);
178
209
  if (segments.length === 0)
179
210
  return "";
180
211
  const assembled = (0, provenance_1.assemblePrompt)(segments, { nonce: (0, node_crypto_1.randomUUID)(), untrustedBlock: (0, provider_1.untrustedBlockFor)(provider) });
181
212
  return `\n\n${assembled.user}`;
182
213
  }
214
+ /** SC-1a: assemble a GIVEN segment list into the {systemAppend, userAppend} the
215
+ * dispatch helper appends. Byte-identical to `untrustedSystemAppend` +
216
+ * `untrustedUserBlock` when `segments` is the full collected set (same
217
+ * assemblePrompt calls, same nonce semantics) — so the screening-OFF path stays
218
+ * golden. The screening path passes the POST-strip `keepSegments`, so a stripped
219
+ * segment vanishes from both slots. Returns empty appends for an empty list. */
220
+ function assembleUntrustedAppends(segments, provider) {
221
+ if (segments.length === 0)
222
+ return { systemAppend: "", userAppend: "" };
223
+ const sys = (0, provenance_1.assemblePrompt)(segments, { nonce: "", untrustedBlock: "user_json" }).system;
224
+ const { user } = (0, provenance_1.assemblePrompt)(segments, { nonce: (0, node_crypto_1.randomUUID)(), untrustedBlock: (0, provider_1.untrustedBlockFor)(provider) });
225
+ return { systemAppend: sys === "" ? "" : `\n\n${sys}`, userAppend: `\n\n${user}` };
226
+ }
183
227
  /** Screening F2 (SC-0 B3): the SYSTEM-slot untrusted-input policy sentence. The
184
228
  * provenance assembler is the SINGLE owner of this sentence (spec §2.2); this
185
229
  * surfaces its system-slot output whenever ANY untrusted segment is delivered —
@@ -187,8 +231,8 @@ function untrustedUserBlock(route, input, injectedSources, provider) {
187
231
  * template still gets its "this is data" framing. Returns "" when there is
188
232
  * nothing untrusted (byte-identical default). The assembler's system slot does
189
233
  * not depend on the nonce or the block shape, so fixed placeholders suffice. */
190
- function untrustedSystemAppend(route, input, injectedSources) {
191
- const segments = collectUntrustedSegments(route, input, injectedSources);
234
+ function untrustedSystemAppend(route, input, injectedSources, proxyTrust) {
235
+ const segments = collectUntrustedSegments(route, input, injectedSources, proxyTrust);
192
236
  if (segments.length === 0)
193
237
  return "";
194
238
  const { system } = (0, provenance_1.assemblePrompt)(segments, { nonce: "", untrustedBlock: "user_json" });
@@ -38,6 +38,10 @@ export interface RecordAttemptDeps {
38
38
  * dropped candidate's later rows carry no customer key (they dispatched
39
39
  * uncached), exactly matching the request builder's suppression. */
40
40
  readonly droppedCacheHandle: ReadonlySet<string>;
41
+ /** LRE-2: the drawn A/B arm (RunOptions.arm) — stamped onto every row's
42
+ * `meta.arm` so the A/B readout groups `inferences` by arm. Absent ⇒ no `arm`
43
+ * key on `meta` (byte-identical to a non-experiment run). */
44
+ readonly arm?: string;
41
45
  }
42
46
  /**
43
47
  * Classify a recorded row's ladder rung (R1 review, F1). `primaryDispatched`
@@ -1 +1 @@
1
- {"version":3,"file":"record-attempt.d.ts","sourceRoot":"","sources":["../../src/run/record-attempt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACvE,OAAO,KAAK,EAAgB,aAAa,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGtF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B;;;gDAG4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC;;qFAEiF;IACjF,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;IACrC;;;wEAGoE;IACpE,QAAQ,CAAC,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CAClD;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CACpB,OAAO,EAAE,gBAAgB,EACzB,WAAW,EAAE,OAAO,EACpB,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,iBAAiB,EAAE,GAAG,CAAC,MAAM,CAAC,GAC7B,aAAa,CAUf;AAED;;yCAEyC;AACzC,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,CA8HlG"}
1
+ {"version":3,"file":"record-attempt.d.ts","sourceRoot":"","sources":["../../src/run/record-attempt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACvE,OAAO,KAAK,EAAgB,aAAa,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGtF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B;;;gDAG4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC;;qFAEiF;IACjF,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;IACrC;;;wEAGoE;IACpE,QAAQ,CAAC,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACjD;;iEAE6D;IAC7D,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CACpB,OAAO,EAAE,gBAAgB,EACzB,WAAW,EAAE,OAAO,EACpB,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,iBAAiB,EAAE,GAAG,CAAC,MAAM,CAAC,GAC7B,aAAa,CAUf;AAED;;yCAEyC;AACzC,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,MAAM,CAkIlG"}
@@ -99,6 +99,10 @@ function createRecordAttempt(deps) {
99
99
  // meta (metaInputs) plus THIS row's provider/model/usage.
100
100
  const meta = (0, attempt_meta_1.summarizeAttemptMeta)({
101
101
  ...(attempt.metaInputs ?? {}),
102
+ ...(deps.arm !== undefined ? { arm: deps.arm } : {}),
103
+ // SC-1a (spec §5, §8): the counts-only screen receipt rides meta.screen —
104
+ // ZERO migration. The single producer passes it through verbatim.
105
+ ...(attempt.screen !== undefined ? { screen: attempt.screen } : {}),
102
106
  provider: attempt.candidate.provider,
103
107
  model: attempt.candidate.model,
104
108
  usage: {
@@ -96,7 +96,7 @@ exports.ROUTING_BOUNDARY = [
96
96
  id: "B7",
97
97
  status: "by_design",
98
98
  capability: "Automatic model swap",
99
- statement: "Nothing changes what serves a route without a bake-off and an explicit human approval (`modelrig swap execute <id> --approve`).",
99
+ statement: "No automated change to what serves a route happens without a bake-off and human approval (`modelrig swap execute <id> --approve`); you (or your agent) can change it directly at any time — instant, versioned, reversible.",
100
100
  tracking: "optimization-loop.md — human-approved swaps only",
101
101
  // No file tripwire: the P6 never-silent-swap invariant is covered by its
102
102
  // own dev-rule tests (spec §5.3).
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/run/run.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAMrD,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,mBAAmB,CAAC;AAGvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAS3D,OAAO,KAAK,EAAE,cAAc,EAAoC,MAAM,aAAa,CAAC;AAUpF,OAAO,KAAK,EAAe,eAAe,EAAE,MAAM,YAAY,CAAC;AAQ/D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAEtE,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACxD,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC;IACxC,+EAA+E;IAC/E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC;;gDAE4C;IAC5C,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IACzC,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACvC;2EACuE;IACvE,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IACxC;;0BAEsB;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACxC,qDAAqD;IACrD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C;;;mFAG+E;IAC/E,QAAQ,CAAC,aAAa,CAAC,EAAE,eAAe,CAAC;IACzC;;;;;uDAKmD;IACnD,QAAQ,CAAC,QAAQ,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAChD;;;;2EAIuE;IACvE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAsBD,wBAAsB,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAwkBhG"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/run/run.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAMrD,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,mBAAmB,CAAC;AAGvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAS3D,OAAO,KAAK,EAAE,cAAc,EAAoC,MAAM,aAAa,CAAC;AAWpF,OAAO,KAAK,EAAa,eAAe,EAAE,MAAM,YAAY,CAAC;AAS7D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAEtE,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACxD,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC;IACxC,+EAA+E;IAC/E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC;;gDAE4C;IAC5C,QAAQ,CAAC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IACzC,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACvC;2EACuE;IACvE,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IACxC;;0BAEsB;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,IAAI,CAAC;IACxC,qDAAqD;IACrD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C;;;mFAG+E;IAC/E,QAAQ,CAAC,aAAa,CAAC,EAAE,eAAe,CAAC;IACzC;;;;;uDAKmD;IACnD,QAAQ,CAAC,QAAQ,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAChD;;;;2EAIuE;IACvE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAsBD,wBAAsB,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAmkBhG"}
package/dist/run/run.js CHANGED
@@ -96,18 +96,16 @@ async function run(deps, routeName, opts) {
96
96
  autoStepKey,
97
97
  cache: opts.cache,
98
98
  droppedCacheHandle,
99
- ladder,
99
+ ladder, ...(opts.arm !== undefined ? { arm: opts.arm } : {}),
100
100
  });
101
- function failRun(candidate, failure, latencyMs) {
101
+ function failRun(candidate, failure, latencyMs, screen) {
102
+ // N1 (spec §5, SC-1b req 2): a pre-dispatch enforce `block` must still record
103
+ // its counts-only screen receipt on `meta.screen`, or screen-stats misses
104
+ // every pre-dispatch block. Absent ⇒ no key ⇒ byte-identical to a plain fail.
102
105
  recordAttempt({
103
- candidate,
104
- servedTier: route.policy.tier,
105
- tokensIn: 0,
106
- tokensOut: 0,
107
- tokensCached: 0,
108
- costUsd: 0,
109
- latencyMs,
110
- failure,
106
+ candidate, servedTier: route.policy.tier, tokensIn: 0, tokensOut: 0,
107
+ tokensCached: 0, costUsd: 0, latencyMs, failure,
108
+ ...(screen !== undefined ? { screen } : {}),
111
109
  });
112
110
  throw new failures_1.RigFailureError(failure);
113
111
  }
@@ -262,7 +260,7 @@ async function run(deps, routeName, opts) {
262
260
  // retries; cost accrues to the envelope and its own telemetry row.
263
261
  let searchResponse = null;
264
262
  let injectedSources = null;
265
- const maskCtx = route.mask !== undefined ? { policy: route.mask, vault: (0, vault_1.createRunVault)() } : null; // CS-1: run-scoped vault; null ⇒ byte-identical
263
+ const guards = (0, dispatch_1.buildRunGuards)(route, vault_1.createRunVault); // CS-1 mask (run-scoped vault) + SC-1a screen; both null ⇒ byte-identical
266
264
  while (candidate !== null) {
267
265
  // Runtime layer of the candidate-set invariant: even a compromised
268
266
  // iterator cannot reach an adapter with a pair outside the declared list.
@@ -291,7 +289,7 @@ async function run(deps, routeName, opts) {
291
289
  const flags = deps.capabilities(candidate.provider, candidate.model);
292
290
  let systemPrompt = (0, template_1.renderTemplate)(route.systemTemplate, {
293
291
  variables: opts.input,
294
- capabilities: flags, variableTrust: route.variableTrust,
292
+ capabilities: flags, variableTrust: (0, grounding_1.systemSlotRemoval)(route.variableTrust, opts.proxyTrust),
295
293
  });
296
294
  // Variant augmentation (WS1): appended after capability-conditionals,
297
295
  // before json coaching — promptAppend first, then the reasoning scaffold.
@@ -364,19 +362,16 @@ async function run(deps, routeName, opts) {
364
362
  // CE-1 (spec §2.2, run/cache-handle.ts): `suppressed` drops the customer handle after a cache_invalid.
365
363
  const candidateKey = `${candidate.provider} ${candidate.model}`;
366
364
  const suppressed = droppedCacheHandle.has(candidateKey);
367
- // Build + mask (spec §1.2 — user slot only) + dispatch as one seam; null mask ⇒ byte-identical.
365
+ // Assemble untrusted block + screen (spec §4) + mask (spec §1.2 — user slot)
366
+ // + dispatch as ONE seam; both guards null ⇒ byte-identical. The untrusted
367
+ // appends + screening live in the seam so run.ts stays under the LOC cap.
368
368
  const dispatched = await (0, dispatch_1.buildDispatchMaybeMasked)(adapter, {
369
- route,
370
- candidate,
371
- systemPrompt: systemPrompt + (0, grounding_1.untrustedSystemAppend)(route, opts.input, injectedSources),
372
- userDirective: USER_DIRECTIVE + (0, grounding_1.untrustedUserBlock)(route, opts.input, injectedSources, candidate.provider),
373
- nativeGrounded,
374
- opts,
375
- suppressCustomerCache: suppressed,
376
- }, deps.onStreamDelta, maskCtx);
377
- if (!dispatched.result.ok && dispatched.maskRefusal !== undefined)
378
- failRun(candidate, dispatched.result.failure, 0);
379
- const { result, latencyMs, seam: maskSeam } = dispatched;
369
+ route, candidate, systemPrompt, userDirective: USER_DIRECTIVE,
370
+ nativeGrounded, opts, suppressCustomerCache: suppressed, injectedSources,
371
+ }, deps.onStreamDelta, guards);
372
+ if (!dispatched.result.ok && (dispatched.maskRefusal !== undefined || dispatched.screenRefusal !== undefined))
373
+ failRun(candidate, dispatched.result.failure, 0, dispatched.screenReceipt);
374
+ const { result, latencyMs, seam: maskSeam, screenReceipt } = dispatched;
380
375
  if (result.ok) {
381
376
  const { costUsd, envelopeChargeUsd, pricingMissing } = (0, cost_1.attemptCost)(candidate.provider, candidate.model, result.usage, unpricedRatePerTok);
382
377
  guard?.settle(envelopeChargeUsd);
@@ -426,14 +421,14 @@ async function run(deps, routeName, opts) {
426
421
  const acceptTags = gate !== null ? (0, quality_gate_1.qualityTags)(gate) : undefined;
427
422
  const output = maskSeam !== null ? maskSeam.rehydrateOutput(outcome.parsed) : outcome.parsed; // rehydrate post-validate (spec §5)
428
423
  const maskReceipt = maskSeam?.receipt(); // read AFTER rehydrate so counts are final
429
- const inferenceId = recordAttempt({ ...(0, accept_1.successAttemptRow)(candidate, result.usage, costUsd, latencyMs, pricingMissing, null, metaInputs, acceptTags), ...(maskReceipt !== undefined ? { mask: maskReceipt } : {}) });
424
+ const inferenceId = recordAttempt({ ...(0, accept_1.successAttemptRow)(candidate, result.usage, costUsd, latencyMs, pricingMissing, null, metaInputs, acceptTags), ...(maskReceipt !== undefined ? { mask: maskReceipt } : {}), ...(screenReceipt !== undefined ? { screen: screenReceipt } : {}) });
430
425
  captureAttempt(inferenceId, result.text);
431
426
  const meta = (0, accept_1.buildSuccessMeta)({
432
427
  inferenceId, route, candidate, usage: result.usage, costUsd, latencyMs,
433
428
  attemptsByClass: ledger.snapshot(), servedVariant, callId: ladder.callId,
434
429
  ...(injectedSources !== null ? (0, grounding_1.groundingMeta)(result.text, injectedSources) : {}),
435
430
  ...(gate !== null ? { quality: { score: gate.score, verdict: "pass", judgedBy: null } } : {}),
436
- ...(maskReceipt !== undefined ? { mask: maskReceipt } : {}),
431
+ ...(maskReceipt !== undefined ? { mask: maskReceipt } : {}), ...(screenReceipt !== undefined ? { screen: screenReceipt } : {}),
437
432
  });
438
433
  return { output, meta };
439
434
  }
@@ -23,6 +23,10 @@ export interface SwapSavings {
23
23
  readonly actualUsdPerConformant: number | null;
24
24
  readonly baselineUsdPerConformant: number;
25
25
  readonly verifiedUsd: number;
26
+ /** N4 (spec §4.4): this row is a cost-raising up-swap — a quality investment,
27
+ * EXCLUDED from the verified-savings sum (`verifiedUsd` forced to 0) but still
28
+ * present in the audit trail. Absent/false ⇒ an ordinary cost-reducing swap. */
29
+ readonly upSwap?: boolean;
26
30
  }
27
31
  export interface Odometer {
28
32
  readonly totalVerifiedUsd: number;
@@ -1 +1 @@
1
- {"version":3,"file":"odometer.d.ts","sourceRoot":"","sources":["../../src/savings/odometer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,wBAAwB,EAAE,MAAM,CAAC;IAC1C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;CAC1C;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,eAAe,EAAE,EACjC,IAAI,EAAE,SAAS,YAAY,EAAE,GAC5B,QAAQ,CAoDV"}
1
+ {"version":3,"file":"odometer.d.ts","sourceRoot":"","sources":["../../src/savings/odometer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEtD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,wBAAwB,EAAE,MAAM,CAAC;IAC1C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;oFAEgF;IAChF,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAaD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,SAAS,WAAW,EAAE,CAAC;CAC1C;AAED,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,eAAe,EAAE,EACjC,IAAI,EAAE,SAAS,YAAY,EAAE,GAC5B,QAAQ,CA2DV"}
@@ -15,8 +15,24 @@
15
15
  */
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.computeOdometer = computeOdometer;
18
+ /** Classify a swap as an up-swap (cost-raising) from its baseline jsonb — zero
19
+ * DDL. Prefer the recorded `swap_direction`; fall back to comparing the recorded
20
+ * to-cost against the from-cost (the odometer read "sums only rows where to ≤
21
+ * from", spec §4.4). A pre-N4 row carries neither and is a down-swap by default. */
22
+ function isUpSwap(baseline) {
23
+ if (baseline.swap_direction === "up")
24
+ return true;
25
+ if (baseline.swap_direction === "down" || baseline.swap_direction === "lateral")
26
+ return false;
27
+ const to = baseline.to_effective_usd_per_1k_conformant;
28
+ return typeof to === "number" && Number.isFinite(to) && to > baseline.effective_usd_per_1k_conformant;
29
+ }
18
30
  function computeOdometer(swaps, rows) {
19
31
  const perSwap = swaps.map((swap) => {
32
+ // N4: a cost-raising up-swap is a QUALITY investment — it contributes 0 to
33
+ // the verified-savings figure (never a negative saving that would deflate the
34
+ // headline; L5 never-round-in-our-favor) while staying in the audit trail.
35
+ const up = isUpSwap(swap.baseline);
20
36
  const from = Date.parse(swap.executedAt);
21
37
  const until = swap.revertedAt !== null ? Date.parse(swap.revertedAt) : Number.POSITIVE_INFINITY;
22
38
  const window = rows.filter((r) => {
@@ -49,6 +65,7 @@ function computeOdometer(swaps, rows) {
49
65
  actualUsdPerConformant: null,
50
66
  baselineUsdPerConformant: baselinePerConformant,
51
67
  verifiedUsd: 0,
68
+ ...(up ? { upSwap: true } : {}),
52
69
  };
53
70
  }
54
71
  const actualPerConformant = spentUsd / conformant;
@@ -60,7 +77,9 @@ function computeOdometer(swaps, rows) {
60
77
  conformantVolume: conformant,
61
78
  actualUsdPerConformant: actualPerConformant,
62
79
  baselineUsdPerConformant: baselinePerConformant,
63
- verifiedUsd: (baselinePerConformant - actualPerConformant) * conformant,
80
+ // Up-swap: excluded from the savings sum (0), never shown as a negative.
81
+ verifiedUsd: up ? 0 : (baselinePerConformant - actualPerConformant) * conformant,
82
+ ...(up ? { upSwap: true } : {}),
64
83
  };
65
84
  });
66
85
  return {
@@ -53,6 +53,13 @@ export interface SwapExecuteResult {
53
53
  readonly cacheVerdict: CacheVerdict | null;
54
54
  /** The [Briefing §30] one-line explanation for the approval prompt, or null. */
55
55
  readonly cacheExplanation: string | null;
56
+ /** N4 (spec §4.3): the swap's direction, surfaced so the `--approve` prompt
57
+ * shows the cost is going UP before the human says yes. NO gate change — the
58
+ * human decision stays the only gate (invariant `no-silent-promotion`). */
59
+ readonly swapDirection: "up" | "down" | "lateral";
60
+ /** Signed effective-cost delta (winner − incumbent, $/1k conformant); null when
61
+ * a cost could not be computed. Positive on an up-swap. */
62
+ readonly costDelta: number | null;
56
63
  }
57
64
  export declare function executeSwap(deps: SwapExecuteDeps): Promise<SwapExecuteResult>;
58
65
  //# sourceMappingURL=swap-execute.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"swap-execute.d.ts","sourceRoot":"","sources":["../../src/savings/swap-execute.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD;;;;mCAImC;AACnC,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,YAAY,GAAG,IAAI,CAmB9F;AASD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE;QAClB,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;QAChD,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KAChF,CAAC;IACF,QAAQ,CAAC,KAAK,EAAE;QAAE,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;IAClE,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,WAAW,CAAC;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM,MAAM,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;iFAE6E;IAC7E,QAAQ,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAC3C,gFAAgF;IAChF,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1C;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAwGnF"}
1
+ {"version":3,"file":"swap-execute.d.ts","sourceRoot":"","sources":["../../src/savings/swap-execute.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD;;;;mCAImC;AACnC,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,YAAY,GAAG,IAAI,CAmB9F;AASD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE;QAClB,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;QAChD,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KAChF,CAAC;IACF,QAAQ,CAAC,KAAK,EAAE;QAAE,MAAM,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAC;IAClE,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,WAAW,CAAC;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM,MAAM,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;iFAE6E;IAC7E,QAAQ,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAC3C,gFAAgF;IAChF,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IACzC;;+EAE2E;IAC3E,QAAQ,CAAC,aAAa,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CAAC;IAClD;+DAC2D;IAC3D,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAKD,wBAAsB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAwInF"}