okengine 0.23.2 → 0.24.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 (75) hide show
  1. package/AGENTS.md +5 -1
  2. package/manifest.v1.schema.json +53 -3
  3. package/package.json +2 -1
  4. package/site/content/docs/ai/mcp.mdx +1 -1
  5. package/site/content/docs/ai/skills.mdx +1 -1
  6. package/site/content/docs/elements/ai/decide.mdx +95 -417
  7. package/site/content/docs/elements/ai/deciders.mdx +154 -0
  8. package/site/content/docs/elements/ai/index.mdx +2 -1
  9. package/site/content/docs/elements/ai/meta.json +1 -1
  10. package/site/content/docs/reference/cli.mdx +2 -2
  11. package/site/content/docs/reference/configuration.mdx +2 -1
  12. package/site/content/docs/reference/fx.mdx +11 -9
  13. package/src/cli/agents-md.test.ts +8 -2
  14. package/src/cli/decide-certify.test.ts +166 -0
  15. package/src/cli/decide.ts +170 -3
  16. package/src/cli/decision-lock-watch.test.ts +4 -4
  17. package/src/cli/eval.ts +38 -59
  18. package/src/cli/mcp-from-console.ts +1 -1
  19. package/src/cli/registry.ts +45 -9
  20. package/src/compiler/decisions.extract.test.ts +86 -31
  21. package/src/compiler/effects-infer.ts +1 -0
  22. package/src/compiler/extract.ts +239 -24
  23. package/src/config/index.ts +6 -0
  24. package/src/console/server/decisions.test.ts +6 -1
  25. package/src/console/server/decisions.ts +32 -1
  26. package/src/console/ui-next/dist/assets/{access-page-zBMhZnFm.js → access-page-DX2dKLmq.js} +1 -1
  27. package/src/console/ui-next/dist/assets/{agent-disclosure-CBj9f7Ju.js → agent-disclosure-CiRvA3M9.js} +1 -1
  28. package/src/console/ui-next/dist/assets/{cache-glyph-D3MY49h3.js → cache-glyph-CC1yFJ8A.js} +1 -1
  29. package/src/console/ui-next/dist/assets/{call-pii-button-L9JxdBhp.js → call-pii-button-CLXQgIDD.js} +1 -1
  30. package/src/console/ui-next/dist/assets/{collapsible-BvFaX-Zt.js → collapsible-C0AThBZg.js} +1 -1
  31. package/src/console/ui-next/dist/assets/{decisions-page-DgB76m-X.js → decisions-page-Bf3AhTAa.js} +1 -1
  32. package/src/console/ui-next/dist/assets/{duration-tone-l1DSJ2Vz.js → duration-tone-Bptur2gX.js} +1 -1
  33. package/src/console/ui-next/dist/assets/{flows-page-Bb9t1lu6.js → flows-page-DRZVeTZU.js} +1 -1
  34. package/src/console/ui-next/dist/assets/{highlighted-json-DMvsi3Ti.js → highlighted-json-BKf0PjBK.js} +1 -1
  35. package/src/console/ui-next/dist/assets/{http-method-BwhJBTcQ.js → http-method-uhrnWrGC.js} +1 -1
  36. package/src/console/ui-next/dist/assets/{index-RhV2jT_7.js → index-Duxus_sG.js} +3 -3
  37. package/src/console/ui-next/dist/assets/{observability-page-BRfUILBq.js → observability-page-B-QM8X0O.js} +1 -1
  38. package/src/console/ui-next/dist/assets/{replica-lag-BsceAzIG.js → replica-lag-DuqHCANi.js} +1 -1
  39. package/src/console/ui-next/dist/assets/{request-meta-C6lTyCXp.js → request-meta-BTBZ3Ulc.js} +1 -1
  40. package/src/console/ui-next/dist/assets/{store-page-YmE806bJ.js → store-page-fpo7VxYb.js} +1 -1
  41. package/src/console/ui-next/dist/assets/{trace-detail-sheet-Di4irS49.js → trace-detail-sheet-DKtr1-K1.js} +1 -1
  42. package/src/console/ui-next/dist/assets/{tree-expand-toggle-DYeL6Rye.js → tree-expand-toggle-QeTceKKI.js} +1 -1
  43. package/src/console/ui-next/dist/assets/{units-page-07agasRA.js → units-page-ChCp8mOy.js} +1 -1
  44. package/src/console/ui-next/dist/assets/{vault-page-CHBck4n_.js → vault-page-CEYLXhUX.js} +1 -1
  45. package/src/console/ui-next/dist/index.html +1 -1
  46. package/src/console/ui-next/src/client.ts +8 -0
  47. package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +16 -0
  48. package/src/elements/ai/deciders/presets.ts +182 -0
  49. package/src/elements/ai/decisions/bind.ts +6 -5
  50. package/src/elements/ai/decisions/catalog.ts +93 -0
  51. package/src/elements/ai/decisions/certificate.test.ts +47 -19
  52. package/src/elements/ai/decisions/certificate.ts +107 -24
  53. package/src/elements/ai/decisions/certify.ts +104 -9
  54. package/src/elements/ai/decisions/codec.test.ts +142 -0
  55. package/src/elements/ai/decisions/codec.ts +302 -0
  56. package/src/elements/ai/decisions/decide.test.ts +108 -31
  57. package/src/elements/ai/decisions/decider-runtime.test.ts +415 -0
  58. package/src/elements/ai/decisions/decider.types.test.ts +62 -0
  59. package/src/elements/ai/decisions/e2e.test.ts +20 -16
  60. package/src/elements/ai/decisions/http.ts +15 -7
  61. package/src/elements/ai/decisions/labels.test.ts +6 -1
  62. package/src/elements/ai/decisions/labels.ts +4 -3
  63. package/src/elements/ai/decisions/openai.live.test.ts +42 -0
  64. package/src/elements/ai/decisions/openrouter.live.test.ts +5 -1
  65. package/src/elements/ai/decisions/provider.ts +22 -1
  66. package/src/elements/ai/declare.ts +208 -51
  67. package/src/elements/ai.ts +1 -0
  68. package/src/index.ts +1 -0
  69. package/src/kernel/app.ts +8 -0
  70. package/src/kernel/decision-budget-entry.ts +11 -0
  71. package/src/kernel/element-registries.ts +3 -0
  72. package/src/kernel/fx-decide.ts +276 -88
  73. package/src/manifest/types.ts +38 -2
  74. package/src/release/measure.ts +47 -8
  75. package/src/test/create-test-app.ts +21 -1
@@ -118,6 +118,17 @@ export function DecisionsPage(): JSX.Element {
118
118
  >
119
119
  <span className="min-w-0 flex-1 truncate">{decision.name}</span>
120
120
  <span className={EXPLORER_COUNT_CLASS}>{decisionStateLabel(decision.state)}</span>
121
+ {decision.certificates?.map((certificate) => (
122
+ <span
123
+ key={certificate.decider}
124
+ className={EXPLORER_COUNT_CLASS}
125
+ data-slot="decision-certificate"
126
+ data-pinned={certificate.pinned ? "true" : "false"}
127
+ >
128
+ {certificate.decider} {certificate.model}
129
+ {certificate.pinned ? "" : " unpinned"}
130
+ </span>
131
+ ))}
121
132
  {decision.metrics ? (
122
133
  <span className={EXPLORER_COUNT_CLASS} data-slot="decision-metrics">
123
134
  {Object.entries(decision.metrics)
@@ -151,6 +162,11 @@ export function DecisionsPage(): JSX.Element {
151
162
  <span className="min-w-0 flex-1 truncate">
152
163
  {row.decision}
153
164
  {row.labelOnly ? " · label" : ""}
165
+ {row.why === "refused" && row.by
166
+ ? ` · refused by ${row.by}`
167
+ : row.why
168
+ ? ` · ${row.why}`
169
+ : ""}
154
170
  </span>
155
171
  <span className={EXPLORER_COUNT_CLASS} data-slot="decision-age">
156
172
  {formatAge(row.ageMs)}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Verified decision hosts. Anything else is `driverId` + `baseUrl` + `capabilities`.
3
+ * Codecs stay out of this module so the edge graph does not load them.
4
+ */
5
+
6
+ /** Wire a decider speaks. */
7
+ export type DeciderProtocol = "systemone" | "openai-decisions";
8
+
9
+ /** How a certificate binds to a model id. */
10
+ export type DeciderPinning = "dated" | "alias";
11
+
12
+ /** What one decider can answer. Numeric limits apply only when present. */
13
+ export interface DeciderCapabilities {
14
+ readonly boolean: boolean;
15
+ readonly choice: boolean;
16
+ readonly score: boolean;
17
+ readonly refusal: boolean;
18
+ readonly maxChoices?: number;
19
+ readonly minLevels?: number;
20
+ readonly maxLevels?: number;
21
+ readonly maxContext?: number;
22
+ }
23
+
24
+ /** One verified host. */
25
+ export interface DeciderPreset {
26
+ readonly protocol: DeciderProtocol;
27
+ readonly baseUrl: string;
28
+ readonly secret: string;
29
+ readonly pinning: DeciderPinning;
30
+ readonly capabilities: DeciderCapabilities;
31
+ }
32
+
33
+ /** Literal capabilities for the OpenRouter preset. */
34
+ type OpenRouterPreset = {
35
+ readonly protocol: "systemone";
36
+ readonly baseUrl: "https://openrouter.ai/api/alpha/decisions";
37
+ readonly secret: "OPENROUTER_API_KEY";
38
+ readonly pinning: "dated";
39
+ readonly capabilities: {
40
+ readonly boolean: true;
41
+ readonly choice: true;
42
+ readonly score: true;
43
+ readonly refusal: false;
44
+ };
45
+ };
46
+
47
+ /** Literal capabilities for the OpenAI preset. */
48
+ type OpenAIPreset = {
49
+ readonly protocol: "openai-decisions";
50
+ readonly baseUrl: "https://api.openai.com/v1/decisions";
51
+ readonly secret: "OPENAI_API_KEY";
52
+ readonly pinning: "alias";
53
+ readonly capabilities: {
54
+ readonly boolean: true;
55
+ readonly choice: true;
56
+ readonly score: true;
57
+ readonly refusal: true;
58
+ };
59
+ };
60
+
61
+ /** OpenRouter System One and OpenAI Decisions. No other host is filled in. */
62
+ export const DECIDER_PRESETS: {
63
+ readonly openrouter: OpenRouterPreset;
64
+ readonly openai: OpenAIPreset;
65
+ } = {
66
+ openrouter: {
67
+ protocol: "systemone",
68
+ baseUrl: "https://openrouter.ai/api/alpha/decisions",
69
+ secret: "OPENROUTER_API_KEY",
70
+ pinning: "dated",
71
+ capabilities: { boolean: true, choice: true, score: true, refusal: false },
72
+ },
73
+ openai: {
74
+ protocol: "openai-decisions",
75
+ baseUrl: "https://api.openai.com/v1/decisions",
76
+ secret: "OPENAI_API_KEY",
77
+ pinning: "alias",
78
+ capabilities: { boolean: true, choice: true, score: true, refusal: true },
79
+ },
80
+ };
81
+
82
+ /** Preset provider id. */
83
+ export type DeciderPresetName = keyof typeof DECIDER_PRESETS;
84
+
85
+ /** Alias certificates expire this long after certify. */
86
+ export const ALIAS_CERTIFICATE_MS: number = 30 * 24 * 60 * 60 * 1000;
87
+
88
+ /**
89
+ * Whether `provider` is a shipped preset.
90
+ *
91
+ * @param provider - Author provider id
92
+ */
93
+ export function isDeciderPreset(provider: string): provider is DeciderPresetName {
94
+ return Object.prototype.hasOwnProperty.call(DECIDER_PRESETS, provider);
95
+ }
96
+
97
+ /** Fields shared by a preset and a custom host. */
98
+ export interface ResolvedDecider {
99
+ readonly protocol: DeciderProtocol;
100
+ readonly baseUrl: string;
101
+ readonly model: string;
102
+ readonly secret: string;
103
+ readonly pinning: DeciderPinning;
104
+ readonly capabilities: DeciderCapabilities;
105
+ readonly provider?: string;
106
+ readonly region?: string;
107
+ readonly regionStatus?: "declared";
108
+ readonly zdr?: boolean;
109
+ readonly zdrStatus?: "declared";
110
+ readonly timeout?: number | string;
111
+ readonly concurrency?: number;
112
+ }
113
+
114
+ /**
115
+ * Fill a preset, or require the custom host fields.
116
+ * Region and ZDR are the app's declaration. Neither preset publishes a verified value.
117
+ *
118
+ * @param name - Decider name, for the error
119
+ * @param options - Author options
120
+ */
121
+ export function resolveDecider(
122
+ name: string,
123
+ options: {
124
+ readonly provider?: string;
125
+ readonly driverId?: string;
126
+ readonly baseUrl?: string;
127
+ readonly model?: string;
128
+ readonly secret?: string;
129
+ readonly capabilities?: DeciderCapabilities;
130
+ readonly region?: string;
131
+ readonly zdr?: boolean;
132
+ readonly timeout?: number | string;
133
+ readonly concurrency?: number;
134
+ },
135
+ ): ResolvedDecider {
136
+ if (!options.model) throw new TypeError(`ai.decider("${name}"): model is required`);
137
+ const declared = {
138
+ ...(options.region !== undefined
139
+ ? { region: options.region, regionStatus: "declared" as const }
140
+ : {}),
141
+ ...(options.zdr !== undefined ? { zdr: options.zdr, zdrStatus: "declared" as const } : {}),
142
+ ...(options.timeout !== undefined ? { timeout: options.timeout } : {}),
143
+ ...(options.concurrency !== undefined ? { concurrency: options.concurrency } : {}),
144
+ };
145
+ if (options.provider && isDeciderPreset(options.provider)) {
146
+ const preset = DECIDER_PRESETS[options.provider];
147
+ return {
148
+ protocol: preset.protocol,
149
+ baseUrl: preset.baseUrl,
150
+ model: options.model,
151
+ secret: options.secret ?? preset.secret,
152
+ pinning: preset.pinning,
153
+ capabilities: preset.capabilities,
154
+ provider: options.provider,
155
+ ...declared,
156
+ };
157
+ }
158
+ if (options.driverId !== "systemone" && options.driverId !== "openai-decisions") {
159
+ throw new TypeError(
160
+ `ai.decider("${name}"): provider "${options.provider ?? ""}" needs driverId "systemone" or "openai-decisions", baseUrl, and capabilities`,
161
+ );
162
+ }
163
+ if (!options.baseUrl) {
164
+ throw new TypeError(`ai.decider("${name}"): baseUrl is required`);
165
+ }
166
+ if (!options.secret) {
167
+ throw new TypeError(`ai.decider("${name}"): secret is required`);
168
+ }
169
+ if (!options.capabilities) {
170
+ throw new TypeError(`ai.decider("${name}"): capabilities are required`);
171
+ }
172
+ return {
173
+ protocol: options.driverId,
174
+ baseUrl: options.baseUrl,
175
+ model: options.model,
176
+ secret: options.secret,
177
+ pinning: options.driverId === "openai-decisions" ? "alias" : "dated",
178
+ capabilities: options.capabilities,
179
+ ...(options.provider !== undefined ? { provider: options.provider } : {}),
180
+ ...declared,
181
+ };
182
+ }
@@ -63,15 +63,16 @@ export function bindDecisionFlows(adopt: (binding: Binding) => void, manifest: M
63
63
  const decl = aiDecisionRegistry.find((item) => item.name === name);
64
64
  const pinned = pinnedDecision(name);
65
65
  const rows = await loadDecisionLabels(name);
66
- const fitted = aggregateDecisionCandidate(name, () =>
67
- certifyLabels({
68
- model: pinned?.model ?? decl?.model ?? "",
66
+ const fitted = aggregateDecisionCandidate(name, () => {
67
+ const cert = certifyLabels({
68
+ model: pinned?.model ?? "",
69
69
  maxError: decl?.autonomy?.maxError ?? 0.05,
70
70
  delta: decl?.autonomy?.risk ?? 0.1,
71
71
  ask: decl?.ask ?? {},
72
72
  labels: rows,
73
- }),
74
- );
73
+ });
74
+ return { decider: decl?.decider ?? "", ...cert };
75
+ });
75
76
  await persistDecisionCandidate(name, fitted);
76
77
  }
77
78
  return { ok: true };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * OpenRouter decision-model catalog. Only fields the catalog actually returns.
3
+ */
4
+
5
+ const CATALOG_URL = "https://openrouter.ai/api/v1/models?output_modalities=decisions";
6
+
7
+ interface CatalogRow {
8
+ readonly id?: unknown;
9
+ readonly canonical_slug?: unknown;
10
+ readonly context_length?: unknown;
11
+ readonly architecture?: { readonly input_modalities?: unknown };
12
+ readonly alias_target?: { readonly slug?: unknown };
13
+ }
14
+
15
+ /**
16
+ * Lines for `oke decide models`: id, canonical slug, context, input modalities.
17
+ *
18
+ * @param body - Catalog JSON
19
+ */
20
+ export function formatDecisionModels(body: unknown): string {
21
+ const rows = catalogRows(body);
22
+ if (rows.length === 0) return "oke decide models: no decision models\n";
23
+ const lines = ["id\tcanonical_slug\tcontext\tinput"];
24
+ for (const row of rows) {
25
+ const id = typeof row.id === "string" ? row.id : "";
26
+ const canonical = typeof row.canonical_slug === "string" ? row.canonical_slug : "";
27
+ const context = typeof row.context_length === "number" ? String(row.context_length) : "";
28
+ const input = Array.isArray(row.architecture?.input_modalities)
29
+ ? row.architecture.input_modalities.filter((item) => typeof item === "string").join(",")
30
+ : "";
31
+ lines.push(`${id}\t${canonical}\t${context}\t${input}`);
32
+ }
33
+ return `${lines.join("\n")}\n`;
34
+ }
35
+
36
+ /**
37
+ * Fetch the public decision catalog and format it.
38
+ *
39
+ * @param fetcher - Injected fetch
40
+ */
41
+ export async function fetchDecisionModels(
42
+ fetcher: (input: string, init?: RequestInit) => Promise<Response> = fetch,
43
+ ): Promise<string> {
44
+ const res = await fetcher(CATALOG_URL);
45
+ if (!res.ok) throw new Error(`oke decide models: ${res.status}`);
46
+ return formatDecisionModels(await res.json());
47
+ }
48
+
49
+ /**
50
+ * Dated slug the catalog lists for `model`, when it differs from `model`.
51
+ * Missing catalog, unknown id, or an undated canonical slug returns undefined.
52
+ *
53
+ * @param model - Decider model id
54
+ * @param baseUrl - Decider URL. Only OpenRouter is queried.
55
+ * @param pinning - `dated` looks up a slug. `alias` does not.
56
+ * @param fetcher - Injected fetch
57
+ */
58
+ export async function requiredDatedModel(
59
+ model: string,
60
+ baseUrl: string,
61
+ pinning: "dated" | "alias",
62
+ fetcher: (input: string, init?: RequestInit) => Promise<Response> = fetch,
63
+ ): Promise<string | undefined> {
64
+ if (pinning !== "dated" || !baseUrl.includes("openrouter.ai")) return undefined;
65
+ try {
66
+ const res = await fetcher(CATALOG_URL);
67
+ if (!res.ok) return undefined;
68
+ return datedSlug(catalogRows(await res.json()), model);
69
+ } catch {
70
+ return undefined;
71
+ }
72
+ }
73
+
74
+ function catalogRows(body: unknown): CatalogRow[] {
75
+ if (!body || typeof body !== "object") return [];
76
+ const data = (body as { data?: unknown }).data;
77
+ return Array.isArray(data) ? (data as CatalogRow[]) : [];
78
+ }
79
+
80
+ function datedSlug(rows: readonly CatalogRow[], model: string): string | undefined {
81
+ const row = rows.find((item) => item.id === model);
82
+ if (!row) return undefined;
83
+ const target = typeof row.alias_target?.slug === "string" ? row.alias_target.slug : undefined;
84
+ const followed = target ? rows.find((item) => item.id === target) : undefined;
85
+ const canonical =
86
+ typeof followed?.canonical_slug === "string"
87
+ ? followed.canonical_slug
88
+ : typeof row.canonical_slug === "string"
89
+ ? row.canonical_slug
90
+ : undefined;
91
+ if (!canonical || !/-\d{8}$/.test(canonical) || canonical === model) return undefined;
92
+ return canonical;
93
+ }
@@ -36,6 +36,7 @@ function answer() {
36
36
  answers: {
37
37
  team: {
38
38
  type: "choice",
39
+ choice: "technical",
39
40
  probabilities: { billing: 0.08, technical: 0.9, none_of_these: 0.02 },
40
41
  },
41
42
  },
@@ -54,22 +55,29 @@ async function run(
54
55
  ) {
55
56
  const question = ai.choice("which team", { billing: "Billing", technical: "Technical" });
56
57
  const decl = ai.decision("triage", {
57
- onUncertain: "abstain",
58
+ decider: ai.decider("jev", { provider: "openrouter", model: "typesafe/jev-1.13.0" }),
59
+ otherwise: "abstain",
58
60
  autonomy: { maxError: 0.05, audit: 0 },
59
61
  ...(extra?.locale ? { locale: () => "ar" } : {}),
60
62
  ask: { team: question },
61
63
  });
62
64
  if (lock) {
63
65
  setDecisionLock({
66
+ version: 2,
64
67
  decisions: {
65
68
  triage: {
66
- model: extra?.mismatch ? "other" : "typesafe/jev-1.13.0",
67
- questions: {
68
- team: {
69
- "": {
70
- hash: extra?.stale ? "nope" : questionHash(question),
71
- calibrator: { kind: "temperature" as const, t: 1 },
72
- threshold: 0.5,
69
+ deciders: {
70
+ jev: {
71
+ model: extra?.mismatch ? "other" : "typesafe/jev-1.13.0",
72
+ pinned: true,
73
+ questions: {
74
+ team: {
75
+ "": {
76
+ hash: extra?.stale ? "nope" : questionHash(question),
77
+ calibrator: { kind: "temperature" as const, t: 1 },
78
+ threshold: 0.5,
79
+ },
80
+ },
73
81
  },
74
82
  },
75
83
  },
@@ -133,7 +141,9 @@ describe("decision certificates", () => {
133
141
 
134
142
  test("promote writes the fetched candidate", async () => {
135
143
  const candidate = {
144
+ decider: "jev",
136
145
  model: "typesafe/jev-1.13.0",
146
+ pinned: true,
137
147
  questions: {
138
148
  team: {
139
149
  "": {
@@ -144,34 +154,45 @@ describe("decision certificates", () => {
144
154
  },
145
155
  },
146
156
  };
157
+ const slot = {
158
+ deciders: {
159
+ jev: {
160
+ model: candidate.model,
161
+ pinned: true,
162
+ questions: candidate.questions,
163
+ },
164
+ },
165
+ };
147
166
  const dir = await mkdtemp(join(tmpdir(), "oke-decide-"));
148
167
  const lockPath = join(dir, "oke-decisions.lock.json");
149
168
  const written = await promoteDecision({
150
169
  name: "triage",
151
170
  origin: "http://127.0.0.1:6530",
152
171
  lockPath,
153
- current: { decisions: {} },
172
+ current: { version: 2, decisions: {} },
154
173
  fetcher: async () => new Response(JSON.stringify(candidate), { status: 200 }),
155
174
  });
156
- expect(written.decisions.triage).toEqual(candidate);
175
+ expect(written.version).toBe(2);
176
+ expect(written.decisions.triage).toEqual(slot);
157
177
  const disk = JSON.parse(await readFile(lockPath, "utf8")) as { decisions: { triage: unknown } };
158
- expect(disk.decisions.triage).toEqual(candidate);
159
- await Bun.write(lockPath, `${JSON.stringify({ decisions: { other: candidate } })}\n`);
178
+ expect(disk.decisions.triage).toEqual(slot);
179
+ await Bun.write(lockPath, `${JSON.stringify({ version: 2, decisions: { other: slot } })}\n`);
160
180
  const merged = await promoteDecision({
161
181
  name: "triage",
162
182
  origin: "http://127.0.0.1:6530",
163
183
  lockPath,
164
184
  fetcher: async () => new Response(JSON.stringify(candidate), { status: 200 }),
165
185
  });
166
- expect(merged.decisions.other).toEqual(candidate);
167
- expect(merged.decisions.triage).toEqual(candidate);
186
+ expect(merged.decisions.other).toEqual(slot);
187
+ expect(merged.decisions.triage).toEqual(slot);
168
188
  expect(await loadDecisionLockfile(dir)).toEqual(merged);
169
189
  });
170
190
 
171
191
  test("certify fits a threshold from the provider and hashes the question", async () => {
172
192
  const question = ai.choice("which team", { billing: "Billing", technical: "Technical" });
173
193
  const decl = ai.decision("triage", {
174
- onUncertain: "abstain",
194
+ decider: ai.decider("jev", { provider: "openrouter", model: "typesafe/jev-1.13.0" }),
195
+ otherwise: "abstain",
175
196
  autonomy: { maxError: 0.05, audit: 0 },
176
197
  evals: "seed.jsonl",
177
198
  ask: { team: question },
@@ -192,14 +213,15 @@ describe("decision certificates", () => {
192
213
  answers: {
193
214
  team: {
194
215
  type: "choice",
216
+ choice: "technical",
195
217
  probabilities: { billing: 0.05, technical: 0.93, none_of_these: 0.02 },
196
218
  },
197
219
  },
198
220
  usage: {},
199
221
  }),
200
222
  });
201
- expect(entry.questions.team?.[""]?.hash).toBe(questionHash(question));
202
- expect(entry.questions.team?.[""]?.threshold).toBeGreaterThan(0);
223
+ expect(entry.cert.questions.team?.[""]?.hash).toBe(questionHash(question));
224
+ expect(entry.cert.questions.team?.[""]?.threshold).toBeGreaterThan(0);
203
225
  const dir = await mkdtemp(join(tmpdir(), "oke-cert-"));
204
226
  const seedPath = join(dir, "seed.jsonl");
205
227
  await Bun.write(seedPath, seed);
@@ -212,6 +234,8 @@ describe("decision certificates", () => {
212
234
  decisions: {
213
235
  triage: {
214
236
  mode: "abstain",
237
+ decider: "jev",
238
+ otherwise: "abstain",
215
239
  questions: ["team"],
216
240
  evals: seedPath,
217
241
  autonomy: { maxError: 0.05, audit: 0 },
@@ -224,6 +248,7 @@ describe("decision certificates", () => {
224
248
  answers: {
225
249
  team: {
226
250
  type: "choice",
251
+ choice: "technical",
227
252
  probabilities: { billing: 0.05, technical: 0.93, none_of_these: 0.02 },
228
253
  },
229
254
  },
@@ -232,7 +257,9 @@ describe("decision certificates", () => {
232
257
  });
233
258
  expect(code).toBe(0);
234
259
  const loaded = await loadDecisionLockfile(dir);
235
- expect(loaded?.decisions.triage?.questions.team?.[""]?.hash).toBe(questionHash(question));
260
+ expect(loaded?.decisions.triage?.deciders.jev?.questions.team?.[""]?.hash).toBe(
261
+ questionHash(question),
262
+ );
236
263
  });
237
264
 
238
265
  test("one seed row does not certify a slice", async () => {
@@ -247,13 +274,14 @@ describe("decision certificates", () => {
247
274
  answers: {
248
275
  team: {
249
276
  type: "choice",
277
+ choice: "technical",
250
278
  probabilities: { billing: 0.05, technical: 0.93, none_of_these: 0.02 },
251
279
  },
252
280
  },
253
281
  usage: {},
254
282
  }),
255
283
  });
256
- expect(entry.questions.team).toBeUndefined();
284
+ expect(entry.cert.questions.team).toBeUndefined();
257
285
  });
258
286
 
259
287
  test("choice and score labels certify from the stored distribution", () => {