@warmdrift/kgauto-compiler 2.0.0-alpha.41 → 2.0.0-alpha.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -31,8 +31,9 @@ import {
31
31
  isModelReachable,
32
32
  isProviderReachable,
33
33
  loadChainsFromBrain,
34
+ readBrainReadEnv,
34
35
  resolveProviderKey
35
- } from "./chunk-XJZRDGHF.mjs";
36
+ } from "./chunk-6MSJQCAP.mjs";
36
37
  import {
37
38
  ALIASES,
38
39
  _setProfileBrainHook,
@@ -41,7 +42,7 @@ import {
41
42
  getProfile,
42
43
  profilesByProvider,
43
44
  tryGetProfile
44
- } from "./chunk-JQGRWJZO.mjs";
45
+ } from "./chunk-ZHUD3I52.mjs";
45
46
  import {
46
47
  emitAdvisoryFired,
47
48
  emitCompileDone,
@@ -51,123 +52,497 @@ import {
51
52
  emitFallbackWalked
52
53
  } from "./chunk-NBO4R5PC.mjs";
53
54
 
54
- // src/tokenizer.ts
55
- var tokenizerImpl = defaultCharBasedCounter;
56
- function defaultCharBasedCounter(text) {
57
- if (!text) return 0;
58
- return Math.max(1, Math.ceil(text.length / 4));
59
- }
60
- function setTokenizer(impl) {
61
- tokenizerImpl = impl;
55
+ // src/models-brain.ts
56
+ function isModelRow(x) {
57
+ if (!x || typeof x !== "object") return false;
58
+ const r = x;
59
+ return typeof r.model_id === "string" && typeof r.provider === "string";
62
60
  }
63
- function resetTokenizer() {
64
- tokenizerImpl = defaultCharBasedCounter;
61
+ function isAliasRow(x) {
62
+ if (!x || typeof x !== "object") return false;
63
+ const r = x;
64
+ return typeof r.alias_id === "string" && typeof r.canonical_id === "string";
65
65
  }
66
- function countTokens(text) {
67
- if (!text) return 0;
66
+ function rowToProfile(row) {
68
67
  try {
69
- const n = tokenizerImpl(text);
70
- return Number.isFinite(n) && n >= 0 ? n : defaultCharBasedCounter(text);
68
+ if (row.cliffs !== void 0 && row.cliffs !== null && !Array.isArray(row.cliffs)) {
69
+ return null;
70
+ }
71
+ if (row.recovery !== void 0 && row.recovery !== null && !Array.isArray(row.recovery)) {
72
+ return null;
73
+ }
74
+ if (row.lowering !== void 0 && row.lowering !== null && (typeof row.lowering !== "object" || Array.isArray(row.lowering))) {
75
+ return null;
76
+ }
77
+ return {
78
+ id: row.model_id,
79
+ provider: row.provider,
80
+ status: row.status ?? "current",
81
+ maxContextTokens: row.max_context_tokens ?? 0,
82
+ maxOutputTokens: row.max_output_tokens ?? 0,
83
+ maxTools: row.max_tools ?? 0,
84
+ parallelToolCalls: row.parallel_tool_calls ?? false,
85
+ structuredOutput: row.structured_output ?? "none",
86
+ systemPromptMode: row.system_prompt_mode ?? "inline",
87
+ streaming: row.streaming ?? true,
88
+ cliffs: row.cliffs ?? [],
89
+ costInputPer1m: row.cost_input_per_1m ?? 0,
90
+ costOutputPer1m: row.cost_output_per_1m ?? 0,
91
+ lowering: row.lowering ?? { system: { mode: "inline" }, cache: { strategy: "unsupported" } },
92
+ recovery: row.recovery ?? [],
93
+ strengths: row.strengths ?? [],
94
+ weaknesses: row.weaknesses ?? [],
95
+ notes: row.notes ?? void 0,
96
+ verifiedAgainstDocs: row.verified_against_docs ?? void 0,
97
+ archetypePerf: row.archetype_perf ?? void 0,
98
+ // alpha.41 — family-resolution fields. `family` may be null pre-
99
+ // migration 024; runtime falls back to deriveFamilyFromModelId at
100
+ // resolution time (see family-resolution.ts, G1).
101
+ family: row.family ?? void 0,
102
+ versionAdded: row.version_added ?? void 0,
103
+ active: row.active ?? void 0
104
+ };
71
105
  } catch {
72
- return defaultCharBasedCounter(text);
106
+ return null;
73
107
  }
74
108
  }
75
- function countToolTokens(tool) {
76
- const namePart = countTokens(tool.name);
77
- const descPart = tool.description ? countTokens(tool.description) : 0;
78
- const paramPart = tool.parameters ? countTokens(JSON.stringify(tool.parameters)) : 0;
79
- return namePart + descPart + paramPart + 8;
109
+ function profileToRow(profile, opts = {}) {
110
+ const row = {
111
+ model_id: profile.id,
112
+ provider: profile.provider,
113
+ status: profile.status,
114
+ max_context_tokens: profile.maxContextTokens,
115
+ max_output_tokens: profile.maxOutputTokens,
116
+ max_tools: profile.maxTools,
117
+ parallel_tool_calls: profile.parallelToolCalls,
118
+ structured_output: profile.structuredOutput,
119
+ system_prompt_mode: profile.systemPromptMode,
120
+ streaming: profile.streaming,
121
+ cliffs: profile.cliffs,
122
+ cost_input_per_1m: profile.costInputPer1m,
123
+ cost_output_per_1m: profile.costOutputPer1m,
124
+ lowering: profile.lowering,
125
+ recovery: profile.recovery,
126
+ strengths: profile.strengths,
127
+ weaknesses: profile.weaknesses,
128
+ notes: profile.notes ?? null,
129
+ archetype_perf: profile.archetypePerf ?? null,
130
+ active: opts.active ?? profile.active ?? true,
131
+ // alpha.41 — round-trip family + version_added when present on profile.
132
+ // version_added is operator-controlled via opts; profile-side value is
133
+ // used only when opts didn't override.
134
+ family: profile.family ?? null
135
+ };
136
+ if (opts.verifiedAgainstDocs !== void 0) {
137
+ row.verified_against_docs = opts.verifiedAgainstDocs;
138
+ } else if (profile.verifiedAgainstDocs !== void 0) {
139
+ const v = profile.verifiedAgainstDocs;
140
+ row.verified_against_docs = /^\d{4}-\d{2}-\d{2}/.test(v) ? v : null;
141
+ }
142
+ if (opts.versionAdded !== void 0) row.version_added = opts.versionAdded;
143
+ else if (profile.versionAdded !== void 0) row.version_added = profile.versionAdded;
144
+ if (opts.versionRemoved !== void 0) row.version_removed = opts.versionRemoved;
145
+ return row;
80
146
  }
81
- function countMessagesTokens(messages) {
82
- let total = 0;
83
- for (const m of messages) {
84
- total += countTokens(m.content) + 4;
147
+ function mapRowsToModels(rows) {
148
+ const out = /* @__PURE__ */ new Map();
149
+ for (const row of rows) {
150
+ if (!isModelRow(row)) continue;
151
+ const profile = rowToProfile(row);
152
+ if (profile) out.set(profile.id, profile);
85
153
  }
86
- return total;
154
+ return out;
155
+ }
156
+ function mapRowsToAliases(rows) {
157
+ const out = {};
158
+ for (const row of rows) {
159
+ if (!isAliasRow(row)) continue;
160
+ out[row.alias_id] = row.canonical_id;
161
+ }
162
+ return out;
163
+ }
164
+ function bundledModels() {
165
+ return new Map(allProfilesRaw().map((p) => [p.id, p]));
87
166
  }
167
+ function bundledAliases() {
168
+ return { ...ALIASES };
169
+ }
170
+ var loadModelsFromBrain = createBrainQueryCache({
171
+ table: "kgauto_models",
172
+ mapRows: mapRowsToModels,
173
+ bundledFallback: bundledModels
174
+ });
175
+ var loadAliasesFromBrain = createBrainQueryCache({
176
+ table: "kgauto_aliases",
177
+ mapRows: mapRowsToAliases,
178
+ bundledFallback: bundledAliases
179
+ });
180
+ _setProfileBrainHook({
181
+ getProfile: (canonical) => loadModelsFromBrain().get(canonical),
182
+ resolveAlias: (id) => loadAliasesFromBrain()[id]
183
+ });
88
184
 
89
- // src/passes.ts
90
- function passSlice(ir) {
91
- const intent = ir.intent.archetype;
92
- const before = ir.sections.length;
93
- const kept = ir.sections.filter((s) => !s.intents || s.intents.length === 0 || s.intents.includes(intent));
94
- const dropped = before - kept.length;
95
- if (dropped === 0) return { value: ir, mutations: [] };
96
- return {
97
- value: { ...ir, sections: kept },
98
- mutations: [
99
- {
100
- id: `slice-${dropped}`,
101
- source: "static_pass",
102
- passName: "slice",
103
- description: `Dropped ${dropped} of ${before} sections not tagged for intent=${intent}`
104
- }
105
- ]
106
- };
185
+ // src/exclusion-findings-brain.ts
186
+ function isValidVerdict(v) {
187
+ return v === "recommend-probe" || v === "unblock" || v === "stay-excluded" || v === "inconclusive";
107
188
  }
108
- function passDedupe(ir) {
109
- const seen = /* @__PURE__ */ new Map();
110
- const order = [];
111
- for (const s of ir.sections) {
112
- const key = simpleHash(s.text.trim());
113
- if (!seen.has(key)) {
114
- seen.set(key, s);
115
- order.push(key);
116
- }
117
- }
118
- const deduped = order.map((k) => seen.get(k));
119
- const dropped = ir.sections.length - deduped.length;
120
- if (dropped === 0) return { value: ir, mutations: [] };
121
- return {
122
- value: { ...ir, sections: deduped },
123
- mutations: [
124
- {
125
- id: `dedupe-${dropped}`,
126
- source: "static_pass",
127
- passName: "dedupe",
128
- description: `Removed ${dropped} duplicate section(s) (by text hash)`
129
- }
130
- ]
131
- };
189
+ function isValidConfidence(v) {
190
+ return v === "high" || v === "medium" || v === "low";
132
191
  }
133
- function passToolRelevance(ir, opts = {}) {
134
- if (!ir.tools || ir.tools.length === 0) return { value: ir, mutations: [] };
135
- const threshold = opts.threshold ?? 0.2;
136
- const intent = ir.intent.archetype;
137
- const scored = ir.tools.map((t) => {
138
- const score = t.relevanceByIntent?.[intent] ?? 0.5;
139
- return { tool: t, score };
140
- });
141
- const kept = scored.filter((s) => s.score >= threshold).sort((a, b) => b.score - a.score).map((s) => s.tool);
142
- const limited = opts.maxKeep ? kept.slice(0, opts.maxKeep) : kept;
143
- const dropped = ir.tools.length - limited.length;
144
- if (dropped === 0) return { value: ir, mutations: [] };
145
- return {
146
- value: { ...ir, tools: limited },
147
- mutations: [
148
- {
149
- id: `tool-relevance-${dropped}`,
150
- source: "static_pass",
151
- passName: "tool_relevance",
152
- description: `Dropped ${dropped} of ${ir.tools.length} tools below relevance ${threshold} for intent=${intent}`
153
- }
154
- ]
155
- };
192
+ function isRawFindingRow(x) {
193
+ if (!x || typeof x !== "object") return false;
194
+ const r = x;
195
+ return typeof r.intent_archetype === "string" && typeof r.excluded_model === "string" && typeof r.excluded_provider === "string" && typeof r.verdict === "string" && typeof r.message === "string" && typeof r.suggestion === "string" && typeof r.confidence === "string";
156
196
  }
157
- function totalHistoryTokens(history) {
158
- let total = 0;
159
- for (const m of history) {
160
- if (typeof m.content === "string") total += countTokens(m.content);
197
+ function mapRowsToFindings(rows) {
198
+ const out = [];
199
+ for (const row of rows) {
200
+ if (!isRawFindingRow(row)) continue;
201
+ if (!isValidVerdict(row.verdict)) continue;
202
+ if (!isValidConfidence(row.confidence)) continue;
203
+ let savings = null;
204
+ if (typeof row.estimated_savings_usd_30d === "number") {
205
+ savings = Number.isFinite(row.estimated_savings_usd_30d) ? row.estimated_savings_usd_30d : null;
206
+ } else if (typeof row.estimated_savings_usd_30d === "string") {
207
+ const n = Number(row.estimated_savings_usd_30d);
208
+ savings = Number.isFinite(n) ? n : null;
209
+ }
210
+ out.push({
211
+ archetype: row.intent_archetype,
212
+ excludedModel: row.excluded_model,
213
+ excludedProvider: row.excluded_provider,
214
+ verdict: row.verdict,
215
+ estimatedSavingsUsd30d: savings,
216
+ message: row.message,
217
+ suggestion: row.suggestion,
218
+ confidence: row.confidence,
219
+ evidence: row.evidence && typeof row.evidence === "object" ? row.evidence : void 0
220
+ });
161
221
  }
162
- return total;
222
+ return out;
163
223
  }
164
- function passCompressHistory(ir, opts = {}) {
165
- const history = ir.history;
166
- if (!history || history.length === 0) {
167
- return { value: ir, mutations: [], historyTokensTotal: 0 };
168
- }
169
- const keepRecent = opts.keepRecent ?? 4;
170
- const summarizeOlderThan = opts.summarizeOlderThan ?? 8;
224
+ var snapshots = /* @__PURE__ */ new Map();
225
+ var runtime;
226
+ var warnedOnce = false;
227
+ var DEFAULT_FINDINGS_ENDPOINT = "https://kgauto-dashboard.vercel.app/api/kgauto-v2/findings/exclusions";
228
+ function configureExclusionFindingsBrain(rt) {
229
+ runtime = rt;
230
+ snapshots.clear();
231
+ warnedOnce = false;
232
+ }
233
+ function isExclusionFindingsBrainActive() {
234
+ return runtime !== void 0;
235
+ }
236
+ function getStaleExclusionFindings(opts) {
237
+ const rt = runtime;
238
+ if (!rt) return [];
239
+ const appId = opts.appId;
240
+ if (!appId) return [];
241
+ let snap = snapshots.get(appId);
242
+ if (!snap) {
243
+ snap = { data: [], expiresAt: 0, refreshing: false };
244
+ snapshots.set(appId, snap);
245
+ }
246
+ const now = Date.now();
247
+ const stale = snap.expiresAt <= now;
248
+ if (stale && !snap.refreshing) {
249
+ snap.refreshing = true;
250
+ void asyncRefresh(rt, appId);
251
+ }
252
+ if (opts.archetype) {
253
+ return snap.data.filter((f) => f.archetype === opts.archetype);
254
+ }
255
+ return snap.data;
256
+ }
257
+ var pendingRefreshes = /* @__PURE__ */ new Map();
258
+ async function asyncRefresh(rt, appId) {
259
+ const promise = doRefresh(rt, appId);
260
+ pendingRefreshes.set(appId, promise);
261
+ try {
262
+ await promise;
263
+ } finally {
264
+ if (pendingRefreshes.get(appId) === promise) {
265
+ pendingRefreshes.delete(appId);
266
+ }
267
+ }
268
+ }
269
+ async function doRefresh(rt, appId) {
270
+ const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}`;
271
+ let snap = snapshots.get(appId);
272
+ if (!snap) {
273
+ snap = { data: [], expiresAt: 0, refreshing: false };
274
+ snapshots.set(appId, snap);
275
+ }
276
+ try {
277
+ const res = await rt.fetchImpl(url, { method: "GET" });
278
+ if (!res.ok) {
279
+ throw new Error(`findings ${res.status}: ${res.statusText}`);
280
+ }
281
+ const body = await res.json();
282
+ if (runtime !== rt) return;
283
+ const rows = Array.isArray(body) ? mapRowsToFindings(body) : [];
284
+ snap.data = rows;
285
+ snap.expiresAt = Date.now() + rt.ttlMs;
286
+ snap.refreshing = false;
287
+ } catch (err) {
288
+ if (runtime !== rt) return;
289
+ snap.refreshing = false;
290
+ snap.expiresAt = Date.now() + rt.ttlMs;
291
+ if (!warnedOnce) {
292
+ warnedOnce = true;
293
+ (rt.onError ?? defaultOnError)(err);
294
+ }
295
+ }
296
+ }
297
+ function defaultOnError(err) {
298
+ console.warn(
299
+ "[kgauto] exclusion-findings fetch failed (using empty fallback):",
300
+ err
301
+ );
302
+ }
303
+
304
+ // src/family-resolution.ts
305
+ var FamilyResolutionError = class extends Error {
306
+ family;
307
+ cause;
308
+ constructor(family, cause) {
309
+ super(
310
+ `Family "${family}" did not resolve to any current+active model. ${cause}. Pass a literal model id in ir.models, or call getRecommendedPrimary({ family, fallback }) at IR-construction time.`
311
+ );
312
+ this.name = "FamilyResolutionError";
313
+ this.family = family;
314
+ this.cause = cause;
315
+ }
316
+ };
317
+ function deriveFamilyFromModelId(modelId) {
318
+ if (typeof modelId !== "string" || modelId.length === 0) return null;
319
+ if (modelId.startsWith("claude-opus-")) return "claude-opus";
320
+ if (modelId.startsWith("claude-sonnet-")) return "claude-sonnet";
321
+ if (modelId.startsWith("claude-haiku-")) return "claude-haiku";
322
+ if (modelId.startsWith("gemini-") && modelId.includes("flash-lite")) {
323
+ return "gemini-flash-lite";
324
+ }
325
+ if (modelId.startsWith("gemini-") && modelId.includes("flash")) {
326
+ return "gemini-flash";
327
+ }
328
+ if (modelId.startsWith("gemini-") && modelId.includes("pro")) {
329
+ return "gemini-pro";
330
+ }
331
+ if (modelId.startsWith("deepseek-") && modelId.includes("pro")) {
332
+ return "deepseek-reasoner";
333
+ }
334
+ if (modelId.startsWith("deepseek-")) return "deepseek-chat";
335
+ if (modelId.startsWith("gpt-")) return "openai-gpt";
336
+ return null;
337
+ }
338
+ function familyOf(profile) {
339
+ return profile.family ?? deriveFamilyFromModelId(profile.id);
340
+ }
341
+ function archetypePerfFor(profile, archetype) {
342
+ if (!archetype) return 0;
343
+ const score = profile.archetypePerf?.[archetype];
344
+ return typeof score === "number" ? score : 5;
345
+ }
346
+ function selectCandidates(registry, family, archetype, appId) {
347
+ const out = [];
348
+ const excluded = /* @__PURE__ */ new Set();
349
+ if (appId && archetype) {
350
+ const findings = getStaleExclusionFindings({ appId, archetype });
351
+ for (const f of findings) {
352
+ if (f.verdict === "stay-excluded") excluded.add(f.excludedModel);
353
+ }
354
+ }
355
+ for (const profile of registry.values()) {
356
+ if (familyOf(profile) !== family) continue;
357
+ if (profile.status !== "current") continue;
358
+ if (profile.active === false) continue;
359
+ if (archetype) {
360
+ if (archetypePerfFor(profile, archetype) < ARCHETYPE_FLOOR_DEFAULT) continue;
361
+ }
362
+ if (excluded.has(profile.id)) continue;
363
+ out.push(profile);
364
+ }
365
+ return out;
366
+ }
367
+ function sortCandidates(candidates, archetype) {
368
+ const sorted = [...candidates];
369
+ sorted.sort((a, b) => {
370
+ const perfA = archetypePerfFor(a, archetype);
371
+ const perfB = archetypePerfFor(b, archetype);
372
+ if (perfA !== perfB) return perfB - perfA;
373
+ const vA = a.versionAdded ?? "";
374
+ const vB = b.versionAdded ?? "";
375
+ if (vA !== vB) return vA < vB ? 1 : -1;
376
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
377
+ });
378
+ return sorted;
379
+ }
380
+ function getRecommendedPrimary(opts) {
381
+ if (opts.posture === "locked") return opts.fallback;
382
+ const registry = loadModelsFromBrain();
383
+ const candidates = selectCandidates(
384
+ registry,
385
+ opts.family,
386
+ opts.archetype,
387
+ opts.appId
388
+ );
389
+ if (candidates.length === 0) return opts.fallback;
390
+ const sorted = sortCandidates(candidates, opts.archetype);
391
+ return sorted[0]?.id ?? opts.fallback;
392
+ }
393
+ function resolveFamilyEntry(family, ctx) {
394
+ if (typeof family !== "string" || family.length === 0) {
395
+ throw new FamilyResolutionError(
396
+ String(family),
397
+ "Family tag must be a non-empty string"
398
+ );
399
+ }
400
+ const registry = loadModelsFromBrain();
401
+ const candidates = selectCandidates(
402
+ registry,
403
+ family,
404
+ ctx.archetype,
405
+ ctx.appId
406
+ );
407
+ if (candidates.length === 0) {
408
+ let anyInFamily = false;
409
+ for (const profile of registry.values()) {
410
+ if (familyOf(profile) === family) {
411
+ anyInFamily = true;
412
+ break;
413
+ }
414
+ }
415
+ const cause = anyInFamily ? `Family has registered models but none are current+active${ctx.archetype ? ` at archetype-perf >= ${ARCHETYPE_FLOOR_DEFAULT} for "${ctx.archetype}"` : ""}${ctx.appId ? ` and not excluded for app "${ctx.appId}"` : ""}` : `No models in brain registry carry family="${family}". Check the taxonomy table in family-resolution.ts or migration 024`;
416
+ throw new FamilyResolutionError(family, cause);
417
+ }
418
+ const sorted = sortCandidates(candidates, ctx.archetype);
419
+ const winner = sorted[0];
420
+ if (!winner) {
421
+ throw new FamilyResolutionError(
422
+ family,
423
+ "Candidates non-empty but sort returned undefined \u2014 internal invariant violation"
424
+ );
425
+ }
426
+ return winner.id;
427
+ }
428
+
429
+ // src/tokenizer.ts
430
+ var tokenizerImpl = defaultCharBasedCounter;
431
+ function defaultCharBasedCounter(text) {
432
+ if (!text) return 0;
433
+ return Math.max(1, Math.ceil(text.length / 4));
434
+ }
435
+ function setTokenizer(impl) {
436
+ tokenizerImpl = impl;
437
+ }
438
+ function resetTokenizer() {
439
+ tokenizerImpl = defaultCharBasedCounter;
440
+ }
441
+ function countTokens(text) {
442
+ if (!text) return 0;
443
+ try {
444
+ const n = tokenizerImpl(text);
445
+ return Number.isFinite(n) && n >= 0 ? n : defaultCharBasedCounter(text);
446
+ } catch {
447
+ return defaultCharBasedCounter(text);
448
+ }
449
+ }
450
+ function countToolTokens(tool) {
451
+ const namePart = countTokens(tool.name);
452
+ const descPart = tool.description ? countTokens(tool.description) : 0;
453
+ const paramPart = tool.parameters ? countTokens(JSON.stringify(tool.parameters)) : 0;
454
+ return namePart + descPart + paramPart + 8;
455
+ }
456
+ function countMessagesTokens(messages) {
457
+ let total = 0;
458
+ for (const m of messages) {
459
+ total += countTokens(m.content) + 4;
460
+ }
461
+ return total;
462
+ }
463
+
464
+ // src/passes.ts
465
+ function passSlice(ir) {
466
+ const intent = ir.intent.archetype;
467
+ const before = ir.sections.length;
468
+ const kept = ir.sections.filter((s) => !s.intents || s.intents.length === 0 || s.intents.includes(intent));
469
+ const dropped = before - kept.length;
470
+ if (dropped === 0) return { value: ir, mutations: [] };
471
+ return {
472
+ value: { ...ir, sections: kept },
473
+ mutations: [
474
+ {
475
+ id: `slice-${dropped}`,
476
+ source: "static_pass",
477
+ passName: "slice",
478
+ description: `Dropped ${dropped} of ${before} sections not tagged for intent=${intent}`
479
+ }
480
+ ]
481
+ };
482
+ }
483
+ function passDedupe(ir) {
484
+ const seen = /* @__PURE__ */ new Map();
485
+ const order = [];
486
+ for (const s of ir.sections) {
487
+ const key = simpleHash(s.text.trim());
488
+ if (!seen.has(key)) {
489
+ seen.set(key, s);
490
+ order.push(key);
491
+ }
492
+ }
493
+ const deduped = order.map((k) => seen.get(k));
494
+ const dropped = ir.sections.length - deduped.length;
495
+ if (dropped === 0) return { value: ir, mutations: [] };
496
+ return {
497
+ value: { ...ir, sections: deduped },
498
+ mutations: [
499
+ {
500
+ id: `dedupe-${dropped}`,
501
+ source: "static_pass",
502
+ passName: "dedupe",
503
+ description: `Removed ${dropped} duplicate section(s) (by text hash)`
504
+ }
505
+ ]
506
+ };
507
+ }
508
+ function passToolRelevance(ir, opts = {}) {
509
+ if (!ir.tools || ir.tools.length === 0) return { value: ir, mutations: [] };
510
+ const threshold = opts.threshold ?? 0.2;
511
+ const intent = ir.intent.archetype;
512
+ const scored = ir.tools.map((t) => {
513
+ const score = t.relevanceByIntent?.[intent] ?? 0.5;
514
+ return { tool: t, score };
515
+ });
516
+ const kept = scored.filter((s) => s.score >= threshold).sort((a, b) => b.score - a.score).map((s) => s.tool);
517
+ const limited = opts.maxKeep ? kept.slice(0, opts.maxKeep) : kept;
518
+ const dropped = ir.tools.length - limited.length;
519
+ if (dropped === 0) return { value: ir, mutations: [] };
520
+ return {
521
+ value: { ...ir, tools: limited },
522
+ mutations: [
523
+ {
524
+ id: `tool-relevance-${dropped}`,
525
+ source: "static_pass",
526
+ passName: "tool_relevance",
527
+ description: `Dropped ${dropped} of ${ir.tools.length} tools below relevance ${threshold} for intent=${intent}`
528
+ }
529
+ ]
530
+ };
531
+ }
532
+ function totalHistoryTokens(history) {
533
+ let total = 0;
534
+ for (const m of history) {
535
+ if (typeof m.content === "string") total += countTokens(m.content);
536
+ }
537
+ return total;
538
+ }
539
+ function passCompressHistory(ir, opts = {}) {
540
+ const history = ir.history;
541
+ if (!history || history.length === 0) {
542
+ return { value: ir, mutations: [], historyTokensTotal: 0 };
543
+ }
544
+ const keepRecent = opts.keepRecent ?? 4;
545
+ const summarizeOlderThan = opts.summarizeOlderThan ?? 8;
171
546
  const summarizeAboveTokens = opts.summarizeAboveTokens;
172
547
  const historyTokensTotal = totalHistoryTokens(history);
173
548
  const countThresholdHit = history.length > summarizeOlderThan;
@@ -449,6 +824,175 @@ function simpleHash(s) {
449
824
  }
450
825
  return (h >>> 0).toString(36);
451
826
  }
827
+ function resolveConventionsForProfile(profile) {
828
+ const own = profile.archetypeConventions ?? [];
829
+ const family = profile.family ?? deriveFamilyFromModelId(profile.id);
830
+ if (!family) return own;
831
+ return own;
832
+ }
833
+ function applyArchetypeConvention(promptText, archetype, family) {
834
+ if (typeof promptText !== "string" || promptText.length === 0) return promptText;
835
+ const repId = familyRepId(family);
836
+ if (!repId) return promptText;
837
+ const profile = tryGetProfile(repId);
838
+ if (!profile || !profile.archetypeConventions) return promptText;
839
+ const conventions = profile.archetypeConventions.filter((c) => c.archetype === archetype);
840
+ if (conventions.length === 0) return promptText;
841
+ let next = promptText;
842
+ for (const c of conventions) {
843
+ if (c.promptPrefix) {
844
+ if (!next.startsWith(c.promptPrefix)) {
845
+ next = c.promptPrefix + next;
846
+ }
847
+ }
848
+ if (c.promptSuffix) {
849
+ if (!next.endsWith(c.promptSuffix)) {
850
+ next = next + c.promptSuffix;
851
+ }
852
+ }
853
+ }
854
+ return next;
855
+ }
856
+ function familyRepId(family) {
857
+ switch (family) {
858
+ case "deepseek-reasoner":
859
+ return "deepseek-v4-pro";
860
+ case "deepseek-chat":
861
+ return "deepseek-v4-flash";
862
+ default:
863
+ return void 0;
864
+ }
865
+ }
866
+ function passApplyConventions(ir, profile) {
867
+ const mutations = [];
868
+ const cliffWarnings = [];
869
+ const structuredOutputHints = [];
870
+ const own = profile.archetypeConventions ?? [];
871
+ let effective = own;
872
+ if (effective.length === 0) {
873
+ const family2 = profile.family ?? deriveFamilyFromModelId(profile.id);
874
+ if (family2) {
875
+ const repId = familyRepId(family2);
876
+ if (repId && repId !== profile.id) {
877
+ const repProfile = tryGetProfile(repId);
878
+ if (repProfile && repProfile.archetypeConventions) {
879
+ effective = repProfile.archetypeConventions;
880
+ }
881
+ }
882
+ }
883
+ }
884
+ if (effective.length === 0) {
885
+ return {
886
+ value: { ir, cliffWarnings, structuredOutputHints },
887
+ mutations
888
+ };
889
+ }
890
+ const archetype = ir.intent.archetype;
891
+ const matching = effective.filter((c) => c.archetype === archetype);
892
+ if (matching.length === 0) {
893
+ return {
894
+ value: { ir, cliffWarnings, structuredOutputHints },
895
+ mutations
896
+ };
897
+ }
898
+ const family = profile.family ?? deriveFamilyFromModelId(profile.id) ?? "unknown";
899
+ let nextIR = ir;
900
+ for (const convention of matching) {
901
+ let touched = false;
902
+ if (convention.promptPrefix) {
903
+ const sections = nextIR.sections ?? [];
904
+ const alreadyPresent = sections.some(
905
+ (s) => s.text.includes(convention.promptPrefix)
906
+ );
907
+ if (!alreadyPresent) {
908
+ const prefixSection = {
909
+ id: `convention-prefix-${family}-${archetype}`,
910
+ text: convention.promptPrefix
911
+ };
912
+ nextIR = { ...nextIR, sections: [prefixSection, ...sections] };
913
+ touched = true;
914
+ }
915
+ }
916
+ if (convention.promptSuffix) {
917
+ const history = nextIR.history ?? [];
918
+ let lastUserIdx = -1;
919
+ for (let i = history.length - 1; i >= 0; i--) {
920
+ if (history[i]?.role === "user") {
921
+ lastUserIdx = i;
922
+ break;
923
+ }
924
+ }
925
+ if (lastUserIdx >= 0) {
926
+ const target = history[lastUserIdx];
927
+ if (!target.content.endsWith(convention.promptSuffix)) {
928
+ const newHistory = history.slice();
929
+ newHistory[lastUserIdx] = {
930
+ ...target,
931
+ content: target.content + convention.promptSuffix
932
+ };
933
+ nextIR = { ...nextIR, history: newHistory };
934
+ touched = true;
935
+ }
936
+ } else if (nextIR.currentTurn && nextIR.currentTurn.role === "user") {
937
+ const target = nextIR.currentTurn;
938
+ if (!target.content.endsWith(convention.promptSuffix)) {
939
+ nextIR = {
940
+ ...nextIR,
941
+ currentTurn: {
942
+ ...target,
943
+ content: target.content + convention.promptSuffix
944
+ }
945
+ };
946
+ touched = true;
947
+ }
948
+ } else {
949
+ nextIR = {
950
+ ...nextIR,
951
+ currentTurn: {
952
+ role: "user",
953
+ content: convention.promptSuffix.trim()
954
+ }
955
+ };
956
+ touched = true;
957
+ }
958
+ }
959
+ if (convention.structuredOutputHint) {
960
+ const wantsStructured = !!nextIR.constraints?.structuredOutput;
961
+ const shouldSurface = convention.structuredOutputHint === "avoid" && wantsStructured || convention.structuredOutputHint === "enforce" && !wantsStructured;
962
+ if (shouldSurface) {
963
+ structuredOutputHints.push({
964
+ archetype,
965
+ hint: convention.structuredOutputHint,
966
+ reason: convention.reason
967
+ });
968
+ if (convention.cliffWarning) {
969
+ cliffWarnings.push(`${profile.id}: ${convention.cliffWarning}`);
970
+ }
971
+ touched = true;
972
+ }
973
+ }
974
+ if (convention.cliffWarning && !convention.structuredOutputHint) {
975
+ const toolCount = nextIR.tools?.length ?? 0;
976
+ const thresholdOk = convention.whenToolCountAtLeast === void 0 || toolCount >= convention.whenToolCountAtLeast;
977
+ if (thresholdOk) {
978
+ cliffWarnings.push(`${profile.id}: ${convention.cliffWarning}`);
979
+ touched = true;
980
+ }
981
+ }
982
+ if (touched) {
983
+ mutations.push({
984
+ id: `apply-convention-${family}-${archetype}`,
985
+ source: "archetype_convention",
986
+ passName: "apply_conventions",
987
+ description: `${profile.id}: applied ${family}-family convention for archetype=${archetype} \u2014 ${convention.reason}`
988
+ });
989
+ }
990
+ }
991
+ return {
992
+ value: { ir: nextIR, cliffWarnings, structuredOutputHints },
993
+ mutations
994
+ };
995
+ }
452
996
 
453
997
  // src/lower.ts
454
998
  function lower(ir, profile, hints = {}) {
@@ -826,125 +1370,6 @@ function getArchetypePerfScore(modelId, archetype) {
826
1370
  return { score, n, grounding };
827
1371
  }
828
1372
 
829
- // src/exclusion-findings-brain.ts
830
- function isValidVerdict(v) {
831
- return v === "recommend-probe" || v === "unblock" || v === "stay-excluded" || v === "inconclusive";
832
- }
833
- function isValidConfidence(v) {
834
- return v === "high" || v === "medium" || v === "low";
835
- }
836
- function isRawFindingRow(x) {
837
- if (!x || typeof x !== "object") return false;
838
- const r = x;
839
- return typeof r.intent_archetype === "string" && typeof r.excluded_model === "string" && typeof r.excluded_provider === "string" && typeof r.verdict === "string" && typeof r.message === "string" && typeof r.suggestion === "string" && typeof r.confidence === "string";
840
- }
841
- function mapRowsToFindings(rows) {
842
- const out = [];
843
- for (const row of rows) {
844
- if (!isRawFindingRow(row)) continue;
845
- if (!isValidVerdict(row.verdict)) continue;
846
- if (!isValidConfidence(row.confidence)) continue;
847
- let savings = null;
848
- if (typeof row.estimated_savings_usd_30d === "number") {
849
- savings = Number.isFinite(row.estimated_savings_usd_30d) ? row.estimated_savings_usd_30d : null;
850
- } else if (typeof row.estimated_savings_usd_30d === "string") {
851
- const n = Number(row.estimated_savings_usd_30d);
852
- savings = Number.isFinite(n) ? n : null;
853
- }
854
- out.push({
855
- archetype: row.intent_archetype,
856
- excludedModel: row.excluded_model,
857
- excludedProvider: row.excluded_provider,
858
- verdict: row.verdict,
859
- estimatedSavingsUsd30d: savings,
860
- message: row.message,
861
- suggestion: row.suggestion,
862
- confidence: row.confidence,
863
- evidence: row.evidence && typeof row.evidence === "object" ? row.evidence : void 0
864
- });
865
- }
866
- return out;
867
- }
868
- var snapshots = /* @__PURE__ */ new Map();
869
- var runtime;
870
- var warnedOnce = false;
871
- var DEFAULT_FINDINGS_ENDPOINT = "https://kgauto-dashboard.vercel.app/api/kgauto-v2/findings/exclusions";
872
- function configureExclusionFindingsBrain(rt) {
873
- runtime = rt;
874
- snapshots.clear();
875
- warnedOnce = false;
876
- }
877
- function isExclusionFindingsBrainActive() {
878
- return runtime !== void 0;
879
- }
880
- function getStaleExclusionFindings(opts) {
881
- const rt = runtime;
882
- if (!rt) return [];
883
- const appId = opts.appId;
884
- if (!appId) return [];
885
- let snap = snapshots.get(appId);
886
- if (!snap) {
887
- snap = { data: [], expiresAt: 0, refreshing: false };
888
- snapshots.set(appId, snap);
889
- }
890
- const now = Date.now();
891
- const stale = snap.expiresAt <= now;
892
- if (stale && !snap.refreshing) {
893
- snap.refreshing = true;
894
- void asyncRefresh(rt, appId);
895
- }
896
- if (opts.archetype) {
897
- return snap.data.filter((f) => f.archetype === opts.archetype);
898
- }
899
- return snap.data;
900
- }
901
- var pendingRefreshes = /* @__PURE__ */ new Map();
902
- async function asyncRefresh(rt, appId) {
903
- const promise = doRefresh(rt, appId);
904
- pendingRefreshes.set(appId, promise);
905
- try {
906
- await promise;
907
- } finally {
908
- if (pendingRefreshes.get(appId) === promise) {
909
- pendingRefreshes.delete(appId);
910
- }
911
- }
912
- }
913
- async function doRefresh(rt, appId) {
914
- const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}`;
915
- let snap = snapshots.get(appId);
916
- if (!snap) {
917
- snap = { data: [], expiresAt: 0, refreshing: false };
918
- snapshots.set(appId, snap);
919
- }
920
- try {
921
- const res = await rt.fetchImpl(url, { method: "GET" });
922
- if (!res.ok) {
923
- throw new Error(`findings ${res.status}: ${res.statusText}`);
924
- }
925
- const body = await res.json();
926
- if (runtime !== rt) return;
927
- const rows = Array.isArray(body) ? mapRowsToFindings(body) : [];
928
- snap.data = rows;
929
- snap.expiresAt = Date.now() + rt.ttlMs;
930
- snap.refreshing = false;
931
- } catch (err) {
932
- if (runtime !== rt) return;
933
- snap.refreshing = false;
934
- snap.expiresAt = Date.now() + rt.ttlMs;
935
- if (!warnedOnce) {
936
- warnedOnce = true;
937
- (rt.onError ?? defaultOnError)(err);
938
- }
939
- }
940
- }
941
- function defaultOnError(err) {
942
- console.warn(
943
- "[kgauto] exclusion-findings fetch failed (using empty fallback):",
944
- err
945
- );
946
- }
947
-
948
1373
  // src/promote-ready-brain.ts
949
1374
  function isRawPromoteReadyRow(x) {
950
1375
  if (!x || typeof x !== "object") return false;
@@ -970,964 +1395,795 @@ function mapRowsToFindings2(rows) {
970
1395
  out.push({
971
1396
  archetype: row.intent_archetype,
972
1397
  family: row.family,
973
- candidateModel: row.candidate_model,
974
- currentModel: row.current_model,
975
- sampleN,
976
- judgePassRate: passRate,
977
- judgeAvgScore: avgScore,
978
- costDeltaPct: coerceNumber(row.cost_delta_pct),
979
- detectedAt: row.detected_at
980
- });
981
- }
982
- return out;
983
- }
984
- var snapshots2 = /* @__PURE__ */ new Map();
985
- var runtime2;
986
- var warnedOnce2 = false;
987
- function isPromoteReadyBrainActive() {
988
- return runtime2 !== void 0;
989
- }
990
- function loadPromoteReadyFindings(opts) {
991
- const rt = runtime2;
992
- if (!rt) return [];
993
- const appId = opts.appId;
994
- if (!appId) return [];
995
- let snap = snapshots2.get(appId);
996
- if (!snap) {
997
- snap = { data: [], expiresAt: 0, refreshing: false };
998
- snapshots2.set(appId, snap);
999
- }
1000
- const now = Date.now();
1001
- const stale = snap.expiresAt <= now;
1002
- if (stale && !snap.refreshing) {
1003
- snap.refreshing = true;
1004
- void asyncRefresh2(rt, appId);
1005
- }
1006
- let rows = snap.data;
1007
- if (opts.archetype) {
1008
- rows = rows.filter((f) => f.archetype === opts.archetype);
1009
- }
1010
- if (opts.family) {
1011
- rows = rows.filter((f) => f.family === opts.family);
1012
- }
1013
- return rows;
1014
- }
1015
- var pendingRefreshes2 = /* @__PURE__ */ new Map();
1016
- async function asyncRefresh2(rt, appId) {
1017
- const promise = doRefresh2(rt, appId);
1018
- pendingRefreshes2.set(appId, promise);
1019
- try {
1020
- await promise;
1021
- } finally {
1022
- if (pendingRefreshes2.get(appId) === promise) {
1023
- pendingRefreshes2.delete(appId);
1024
- }
1025
- }
1026
- }
1027
- async function doRefresh2(rt, appId) {
1028
- const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}`;
1029
- let snap = snapshots2.get(appId);
1030
- if (!snap) {
1031
- snap = { data: [], expiresAt: 0, refreshing: false };
1032
- snapshots2.set(appId, snap);
1033
- }
1034
- try {
1035
- const res = await rt.fetchImpl(url, { method: "GET" });
1036
- if (!res.ok) {
1037
- throw new Error(`promote-ready ${res.status}: ${res.statusText}`);
1038
- }
1039
- const body = await res.json();
1040
- if (runtime2 !== rt) return;
1041
- const rows = Array.isArray(body) ? mapRowsToFindings2(body) : [];
1042
- snap.data = rows;
1043
- snap.expiresAt = Date.now() + rt.ttlMs;
1044
- snap.refreshing = false;
1045
- } catch (err) {
1046
- if (runtime2 !== rt) return;
1047
- snap.refreshing = false;
1048
- snap.expiresAt = Date.now() + rt.ttlMs;
1049
- if (!warnedOnce2) {
1050
- warnedOnce2 = true;
1051
- (rt.onError ?? defaultOnError2)(err);
1052
- }
1053
- }
1054
- }
1055
- function defaultOnError2(err) {
1056
- console.warn(
1057
- "[kgauto] promote-ready fetch failed (using empty fallback):",
1058
- err
1059
- );
1060
- }
1061
- function resolveFetchImpl(injected) {
1062
- return injected ?? ((...args) => globalThis.fetch(...args));
1063
- }
1064
- function normalizeEndpoint(endpoint) {
1065
- return endpoint.replace(/\/+$/, "");
1066
- }
1067
- async function markPromoteReadyHandled(opts) {
1068
- const {
1069
- appId,
1070
- archetype,
1071
- family,
1072
- resolution,
1073
- resolutionNote,
1074
- brainEndpoint,
1075
- brainJwt,
1076
- brainAnonKey,
1077
- fetch: injectedFetch
1078
- } = opts;
1079
- if (!appId) return { ok: false, reason: "app_id_required" };
1080
- if (!archetype) return { ok: false, reason: "archetype_required" };
1081
- if (!family) return { ok: false, reason: "family_required" };
1082
- if (resolution !== "promoted" && resolution !== "declined" && resolution !== "still-evaluating") {
1083
- return { ok: false, reason: "resolution_invalid" };
1084
- }
1085
- const doFetch = resolveFetchImpl(injectedFetch);
1086
- const base = normalizeEndpoint(brainEndpoint);
1087
- const url = `${base}/rest/v1/promote_ready_findings?app_id=eq.${encodeURIComponent(appId)}&intent_archetype=eq.${encodeURIComponent(archetype)}&family=eq.${encodeURIComponent(family)}&resolved_at=is.null`;
1088
- const patchBody = {
1089
- resolved_at: (/* @__PURE__ */ new Date()).toISOString(),
1090
- resolution
1091
- };
1092
- if (resolutionNote !== void 0) {
1093
- patchBody.resolution_note = resolutionNote;
1094
- }
1095
- let res;
1096
- try {
1097
- res = await doFetch(url, {
1098
- method: "PATCH",
1099
- headers: {
1100
- Authorization: `Bearer ${brainJwt}`,
1101
- apikey: brainAnonKey,
1102
- "Content-Type": "application/json",
1103
- Accept: "application/json",
1104
- Prefer: "return=minimal"
1105
- },
1106
- body: JSON.stringify(patchBody)
1107
- });
1108
- } catch (err) {
1109
- const msg = err instanceof Error ? err.message : String(err);
1110
- return { ok: false, reason: `network_error:${msg}` };
1111
- }
1112
- if (res.status === 401 || res.status === 403) {
1113
- return { ok: false, reason: "brain_auth_misconfig" };
1114
- }
1115
- if (res.status >= 500) {
1116
- return { ok: false, reason: "brain_unavailable" };
1117
- }
1118
- if (!res.ok) {
1119
- return { ok: false, reason: `patch_failed:${res.status}` };
1120
- }
1121
- return { ok: true };
1122
- }
1123
-
1124
- // src/advisor-rules/promote-ready.ts
1125
- var PROMOTE_READY_THRESHOLDS = {
1126
- minPassRate: 0.8,
1127
- minAvgScore: 4
1128
- };
1129
- function shouldFirePromoteReady(finding, resolvedPrimary) {
1130
- if (finding.currentModel !== resolvedPrimary) return false;
1131
- if (finding.judgePassRate < PROMOTE_READY_THRESHOLDS.minPassRate) return false;
1132
- if (finding.judgeAvgScore < PROMOTE_READY_THRESHOLDS.minAvgScore) return false;
1133
- return true;
1134
- }
1135
- function deriveFamilyLocal(modelId) {
1136
- if (modelId.startsWith("claude-opus-")) return "claude-opus";
1137
- if (modelId.startsWith("claude-sonnet-")) return "claude-sonnet";
1138
- if (modelId.startsWith("claude-haiku-")) return "claude-haiku";
1139
- if (/^gemini-.*-flash-lite/.test(modelId)) return "gemini-flash-lite";
1140
- if (/^gemini-.*-flash/.test(modelId)) return "gemini-flash";
1141
- if (/^gemini-.*-pro/.test(modelId)) return "gemini-pro";
1142
- if (/^deepseek-.*-pro/.test(modelId)) return "deepseek-reasoner";
1143
- if (modelId.startsWith("deepseek-")) return "deepseek-chat";
1144
- if (modelId.startsWith("gpt-")) return "openai-gpt";
1145
- return null;
1146
- }
1147
- function advisorRulePromoteReady(ctx) {
1148
- if (!isPromoteReadyBrainActive()) return [];
1149
- if (!ctx.appId) return [];
1150
- if (!ctx.resolvedPrimary) return [];
1151
- const family = deriveFamilyLocal(ctx.resolvedPrimary);
1152
- if (!family) return [];
1153
- const findings = loadPromoteReadyFindings({
1154
- appId: ctx.appId,
1155
- archetype: ctx.archetype,
1156
- family
1157
- });
1158
- if (findings.length === 0) return [];
1159
- const qualifying = findings.filter(
1160
- (f) => shouldFirePromoteReady(f, ctx.resolvedPrimary)
1161
- );
1162
- if (qualifying.length === 0) return [];
1163
- qualifying.sort((a, b) => {
1164
- if (a.judgeAvgScore !== b.judgeAvgScore) {
1165
- return b.judgeAvgScore - a.judgeAvgScore;
1166
- }
1167
- return b.judgePassRate - a.judgePassRate;
1168
- });
1169
- const top = qualifying[0];
1170
- const pctPass = Math.round(top.judgePassRate * 100);
1171
- const score = top.judgeAvgScore.toFixed(2);
1172
- let costClause = "";
1173
- if (top.costDeltaPct !== null) {
1174
- const sign = top.costDeltaPct < 0 ? "cheaper" : "more expensive";
1175
- const magnitude = Math.abs(top.costDeltaPct * 100).toFixed(1);
1176
- costClause = `, cost ${magnitude}% ${sign}`;
1177
- }
1178
- const message = `Probe found ${top.candidateModel} produces equivalent-or-better outputs vs ${top.currentModel} on ${top.sampleN} recent ${top.archetype} prompts (pass rate ${pctPass}%, avg score ${score}/5${costClause}). Consider promoting via markPromoteReadyHandled.`;
1179
- return [
1180
- {
1181
- level: "info",
1182
- code: "promote-ready",
1183
- message,
1184
- suggestion: `Migrate ${top.archetype} traffic from ${top.currentModel} to ${top.candidateModel}, then call markPromoteReadyHandled({ appId, archetype: '${top.archetype}', family: '${top.family}', resolution: 'promoted' }) to silence this advisory.`,
1185
- // alpha.36 architectural field — not a no-ai-needed case.
1186
- recommendedArchitecture: void 0,
1187
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1188
- }
1189
- ];
1190
- }
1191
-
1192
- // src/advisor-rules/consumer-on-stale-model.ts
1193
- function isStaleStatus(v) {
1194
- return v === "legacy" || v === "deprecated";
1195
- }
1196
- function asString(v) {
1197
- return typeof v === "string" && v.length > 0 ? v : void 0;
1198
- }
1199
- function mapRowsToFindings3(rows) {
1200
- const out = [];
1201
- for (const raw of rows) {
1202
- if (!raw || typeof raw !== "object") continue;
1203
- const r = raw;
1204
- const archetype = asString(r.intent_archetype) ?? asString(r.applies_to_archetype);
1205
- const staleModel = asString(r.stale_model) ?? asString(r.applies_to_model);
1206
- const staleProvider = asString(r.stale_provider);
1207
- const recommendedModel = asString(r.recommended_model);
1208
- const family = asString(r.family);
1209
- const message = asString(r.message);
1210
- if (!archetype || !staleModel || !recommendedModel || !family || !message) {
1211
- continue;
1212
- }
1213
- if (!isStaleStatus(r.stale_status)) continue;
1214
- const row = {
1215
- archetype,
1216
- staleModel,
1217
- staleProvider: staleProvider ?? "unknown",
1218
- staleStatus: r.stale_status,
1219
- recommendedModel,
1220
- family,
1221
- message
1222
- };
1223
- const suggestion = asString(r.suggestion);
1224
- if (suggestion) row.suggestion = suggestion;
1225
- if (typeof r.observation_count === "number" && Number.isFinite(r.observation_count)) {
1226
- row.observationCount = r.observation_count;
1227
- }
1228
- out.push(row);
1398
+ candidateModel: row.candidate_model,
1399
+ currentModel: row.current_model,
1400
+ sampleN,
1401
+ judgePassRate: passRate,
1402
+ judgeAvgScore: avgScore,
1403
+ costDeltaPct: coerceNumber(row.cost_delta_pct),
1404
+ detectedAt: row.detected_at
1405
+ });
1229
1406
  }
1230
1407
  return out;
1231
1408
  }
1232
- var snapshots3 = /* @__PURE__ */ new Map();
1233
- var runtime3;
1234
- var warnedOnce3 = false;
1235
- var pendingRefreshes3 = /* @__PURE__ */ new Map();
1236
- function isStaleModelFindingsBrainActive() {
1237
- return runtime3 !== void 0;
1409
+ var snapshots2 = /* @__PURE__ */ new Map();
1410
+ var runtime2;
1411
+ var warnedOnce2 = false;
1412
+ function isPromoteReadyBrainActive() {
1413
+ return runtime2 !== void 0;
1238
1414
  }
1239
- function getStaleModelFindings(opts) {
1240
- const rt = runtime3;
1415
+ function loadPromoteReadyFindings(opts) {
1416
+ const rt = runtime2;
1241
1417
  if (!rt) return [];
1242
1418
  const appId = opts.appId;
1243
1419
  if (!appId) return [];
1244
- let snap = snapshots3.get(appId);
1420
+ let snap = snapshots2.get(appId);
1245
1421
  if (!snap) {
1246
1422
  snap = { data: [], expiresAt: 0, refreshing: false };
1247
- snapshots3.set(appId, snap);
1423
+ snapshots2.set(appId, snap);
1248
1424
  }
1249
1425
  const now = Date.now();
1250
1426
  const stale = snap.expiresAt <= now;
1251
1427
  if (stale && !snap.refreshing) {
1252
1428
  snap.refreshing = true;
1253
- void asyncRefresh3(rt, appId);
1429
+ void asyncRefresh2(rt, appId);
1254
1430
  }
1431
+ let rows = snap.data;
1255
1432
  if (opts.archetype) {
1256
- return snap.data.filter((f) => f.archetype === opts.archetype);
1433
+ rows = rows.filter((f) => f.archetype === opts.archetype);
1257
1434
  }
1258
- return snap.data;
1435
+ if (opts.family) {
1436
+ rows = rows.filter((f) => f.family === opts.family);
1437
+ }
1438
+ return rows;
1259
1439
  }
1260
- async function asyncRefresh3(rt, appId) {
1261
- const promise = doRefresh3(rt, appId);
1262
- pendingRefreshes3.set(appId, promise);
1440
+ var pendingRefreshes2 = /* @__PURE__ */ new Map();
1441
+ async function asyncRefresh2(rt, appId) {
1442
+ const promise = doRefresh2(rt, appId);
1443
+ pendingRefreshes2.set(appId, promise);
1263
1444
  try {
1264
1445
  await promise;
1265
1446
  } finally {
1266
- if (pendingRefreshes3.get(appId) === promise) {
1267
- pendingRefreshes3.delete(appId);
1268
- }
1269
- }
1270
- }
1271
- async function doRefresh3(rt, appId) {
1272
- const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}`;
1273
- let snap = snapshots3.get(appId);
1274
- if (!snap) {
1275
- snap = { data: [], expiresAt: 0, refreshing: false };
1276
- snapshots3.set(appId, snap);
1277
- }
1278
- try {
1279
- const res = await rt.fetchImpl(url, { method: "GET" });
1280
- if (!res.ok) {
1281
- throw new Error(`stale-model findings ${res.status}: ${res.statusText}`);
1282
- }
1283
- const body = await res.json();
1284
- if (runtime3 !== rt) return;
1285
- const rows = Array.isArray(body) ? mapRowsToFindings3(body) : [];
1286
- snap.data = rows;
1287
- snap.expiresAt = Date.now() + rt.ttlMs;
1288
- snap.refreshing = false;
1289
- } catch (err) {
1290
- if (runtime3 !== rt) return;
1291
- snap.refreshing = false;
1292
- snap.expiresAt = Date.now() + rt.ttlMs;
1293
- if (!warnedOnce3) {
1294
- warnedOnce3 = true;
1295
- (rt.onError ?? defaultOnError3)(err);
1296
- }
1297
- }
1298
- }
1299
- function defaultOnError3(err) {
1300
- console.warn(
1301
- "[kgauto] stale-model findings fetch failed (using empty fallback):",
1302
- err
1303
- );
1304
- }
1305
- var CONSUMER_ON_STALE_MODEL_RULE_CODE = "consumer-on-stale-model";
1306
- function advisorRuleConsumerOnStaleModel(ir) {
1307
- if (!isStaleModelFindingsBrainActive()) return [];
1308
- if (!ir.appId) return [];
1309
- const findings = getStaleModelFindings({
1310
- appId: ir.appId,
1311
- archetype: ir.intent.archetype
1312
- });
1313
- if (findings.length === 0) return [];
1314
- const ranked = [...findings].sort((a, b) => {
1315
- if (a.staleStatus !== b.staleStatus) {
1316
- return a.staleStatus === "deprecated" ? -1 : 1;
1317
- }
1318
- return a.staleModel.localeCompare(b.staleModel);
1319
- });
1320
- const top = ranked[0];
1321
- const extraCount = findings.length - 1;
1322
- const extraNote = extraCount > 0 ? ` (+ ${extraCount} more stale model${extraCount === 1 ? "" : "s"} for this archetype)` : "";
1323
- return [
1324
- {
1325
- level: "warn",
1326
- code: CONSUMER_ON_STALE_MODEL_RULE_CODE,
1327
- message: `${top.message}${extraNote}`,
1328
- suggestion: top.suggestion ?? `Migrate ${top.staleModel} \u2192 ${top.recommendedModel} for archetype "${top.archetype}". The newer model is the current latest in the "${top.family}" family; the stale one is ${top.staleStatus}.`,
1329
- recommendationType: "model-swap",
1330
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1331
- }
1332
- ];
1333
- }
1334
-
1335
- // src/advisor.ts
1336
- var QUALITY_FLOOR_FOR_RECOMMENDATION = 6;
1337
- var TIER_DOWN_COST_RATIO = 0.5;
1338
- var COST_MISMATCHED_CHOSEN_SCORE_CEILING = 7;
1339
- function runAdvisor(ir, result, profile, policy, phase2) {
1340
- const out = [];
1341
- out.push(...detectCachingOff(ir, profile));
1342
- out.push(...detectSingleChunkSystem(ir, profile));
1343
- out.push(...detectToolBloat(ir, result));
1344
- out.push(...detectHistoryUncached(ir, profile));
1345
- out.push(...detectSingleModelArray(ir, policy));
1346
- if (policy?.posture !== "locked") {
1347
- out.push(...detectCostMismatchedArchetype(ir, profile, phase2));
1348
- out.push(...detectModelStaleEvidence(ir, profile));
1349
- out.push(...detectTierDown(ir, profile, phase2));
1350
- }
1351
- if (!translatorClearedToolCallCliff(phase2)) {
1352
- out.push(...detectArchetypePerfFloorBreach(ir, profile));
1353
- }
1354
- if (policy?.posture !== "locked") {
1355
- out.push(...detectStaleExclusionCandidate(ir));
1356
- }
1357
- if (policy?.posture !== "locked" && ir.appId) {
1358
- out.push(
1359
- ...advisorRulePromoteReady({
1360
- appId: ir.appId,
1361
- archetype: ir.intent.archetype,
1362
- resolvedPrimary: profile.id
1363
- })
1364
- );
1365
- out.push(...advisorRuleConsumerOnStaleModel(ir));
1366
- }
1367
- return out;
1368
- }
1369
- function translatorClearedToolCallCliff(phase2) {
1370
- const rewrites = phase2?.sectionRewritesApplied;
1371
- if (!rewrites || rewrites.length === 0) return false;
1372
- for (const rw of rewrites) {
1373
- if (rw.kind === "tool_call_contract") return true;
1374
- }
1375
- return false;
1376
- }
1377
- function detectCachingOff(ir, profile) {
1378
- if (profile.provider !== "anthropic") return [];
1379
- const totalChars = ir.sections.reduce((s, sec) => s + sec.text.length, 0);
1380
- if (totalChars < 2e3) return [];
1381
- const anyCacheable = ir.sections.some((s) => s.cacheable === true);
1382
- if (anyCacheable) return [];
1383
- return [
1384
- {
1385
- level: "warn",
1386
- code: "caching-off-on-claude",
1387
- message: `System prompt is ${totalChars} chars on Anthropic but no PromptSection has cacheable=true. Anthropic prompt caching cuts cached-prefix input cost by ~90% on subsequent calls; without it, every turn re-pays full price for the static system context.`,
1388
- suggestion: "Mark stable system sections (role, persona, tool policy) with `cacheable: true`. The lowering pass concatenates cacheable sections into a single cache-controlled block before the dynamic ones.",
1389
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1390
- }
1391
- ];
1392
- }
1393
- function detectSingleChunkSystem(ir, profile) {
1394
- if (profile.provider !== "anthropic") return [];
1395
- if (ir.sections.length !== 1) return [];
1396
- const only = ir.sections[0];
1397
- if (!only || only.text.length <= 1e3) return [];
1398
- return [
1399
- {
1400
- level: "info",
1401
- code: "single-chunk-system",
1402
- message: `System prompt is a single ${only.text.length}-char chunk. Splitting into NamedChunks (static role/persona vs dynamic context) gives the lowering pass a finer cache-marker boundary \u2014 only the static portion needs to be byte-stable for the cache to hit.`,
1403
- suggestion: "Refactor the system builder to return an array of `PromptSection` shaped { id, text, cacheable?: boolean }. Static chunks (role, persona, tool policy) get `cacheable: true`; dynamic ones (current context, today's date) don't.",
1404
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1405
- }
1406
- ];
1407
- }
1408
- function detectToolBloat(ir, result) {
1409
- const SHORT_OUTPUT = /* @__PURE__ */ new Set([
1410
- "classify",
1411
- "extract",
1412
- "summarize",
1413
- "transform",
1414
- "critique"
1415
- ]);
1416
- if (!ir.tools || ir.tools.length === 0) return [];
1417
- const toolsKept = result.diagnostics.toolsKept;
1418
- if (toolsKept <= 10) return [];
1419
- if (!SHORT_OUTPUT.has(ir.intent.archetype)) return [];
1420
- return [
1421
- {
1422
- level: "warn",
1423
- code: "tool-bloat",
1424
- message: `${toolsKept} tools kept after the relevance pass for archetype="${ir.intent.archetype}" (consumer declared ${ir.tools.length}). This archetype is short-output and rarely needs more than 3 tools; each tool definition eats ~350 tokens of context budget.`,
1425
- suggestion: "Tighten `relevanceByIntent: { [archetype]: 0..1 }` per ToolDefinition. Tools below `toolRelevanceThreshold` (default 0.2) get dropped. Without `relevanceByIntent`, every tool defaults to neutral (0.5) and stays.",
1426
- docsUrl: "https://github.com/stue/kgauto/blob/main/v2/README.md#tools"
1427
- }
1428
- ];
1429
- }
1430
- function detectHistoryUncached(ir, profile) {
1431
- if (profile.provider !== "anthropic") return [];
1432
- if (!ir.history || ir.history.length < 2) return [];
1433
- if (ir.historyCachePolicy && ir.historyCachePolicy.strategy !== "none") {
1434
- return [];
1435
- }
1436
- return [
1437
- {
1438
- level: "warn",
1439
- code: "history-uncached-on-claude",
1440
- message: `${ir.history.length} history messages on Anthropic with no historyCachePolicy. Every turn re-pays for the full conversation context; with caching, subsequent turns hit the cache at ~10% the input cost.`,
1441
- suggestion: "Set `historyCachePolicy: { strategy: 'all-but-latest' }` on this IR. The lowering pass marks the message immediately preceding currentTurn with cache_control; subsequent turns whose history prefix matches byte-for-byte hit the cache.",
1442
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1443
- }
1444
- ];
1445
- }
1446
- function detectSingleModelArray(ir, policy) {
1447
- if (ir.models.length !== 1) return [];
1448
- if (policy?.posture === "locked") return [];
1449
- const only = ir.models[0];
1450
- return [
1451
- {
1452
- level: "warn",
1453
- code: "single-model-array",
1454
- message: `\`ir.models\` has length 1 (only "${only}") and posture is not 'locked'. A single-model chain has no safety net \u2014 the first 429 / 5xx / cliff hits the user as a failure. Master plan \xA71.2 closes the reliability gap with a 2-step minimum.`,
1455
- suggestion: "Use `getDefaultFallbackChain({ archetype: ir.intent.archetype, primary: '" + only + "', posture: 'preferred' })` for a user-anchored chain, or `getDefaultFallbackChain({ archetype, posture: 'open' })` for library-picked. If single-model is intentional (compliance/brand promise), set `policy.posture = 'locked'` to silence this rule.",
1456
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#single-model-array"
1457
- }
1458
- ];
1459
- }
1460
- function detectCostMismatchedArchetype(ir, profile, phase2) {
1461
- if (!phase2 || phase2.fallbackChain.length === 0) return [];
1462
- if (!phase2.profileResolver) return [];
1463
- const archetype = ir.intent.archetype;
1464
- const chosenScore = getArchetypePerfScore(profile.id, archetype);
1465
- const chosenHasRoomToGrow = chosenScore.grounding === "judgment" || chosenScore.score < COST_MISMATCHED_CHOSEN_SCORE_CEILING;
1466
- if (!chosenHasRoomToGrow) return [];
1467
- let bestAlt = null;
1468
- for (const altId of phase2.fallbackChain) {
1469
- const altProfile = phase2.profileResolver(altId);
1470
- if (!altProfile) continue;
1471
- if (altProfile.id === profile.id) continue;
1472
- const altScore = getArchetypePerfScore(altProfile.id, archetype);
1473
- if (altScore.score < QUALITY_FLOOR_FOR_RECOMMENDATION) continue;
1474
- if (altScore.score < chosenScore.score) continue;
1475
- if (altProfile.costInputPer1m >= profile.costInputPer1m) continue;
1476
- if (!bestAlt || altScore.score > bestAlt.score.score || altScore.score === bestAlt.score.score && altProfile.costInputPer1m < bestAlt.profile.costInputPer1m) {
1477
- bestAlt = { id: altId, profile: altProfile, score: altScore };
1447
+ if (pendingRefreshes2.get(appId) === promise) {
1448
+ pendingRefreshes2.delete(appId);
1478
1449
  }
1479
1450
  }
1480
- if (!bestAlt) return [];
1481
- const tierDownWouldFire = bestAlt.score.grounding === "measured" && bestAlt.profile.costInputPer1m <= profile.costInputPer1m * TIER_DOWN_COST_RATIO;
1482
- if (tierDownWouldFire) return [];
1483
- const chosenGrounding = chosenScore.grounding === "judgment" ? `archetypePerf.${archetype}=judgment` : `archetypePerf.${archetype}=${chosenScore.score}`;
1484
- const altGrounding = bestAlt.score.grounding === "measured" ? `archetypePerf.${archetype}=${bestAlt.score.score}, measured, n=${bestAlt.score.n}` : `archetypePerf.${archetype}=${bestAlt.score.score}, judgment`;
1485
- return [
1486
- {
1487
- level: "warn",
1488
- code: "cost-mismatched-archetype",
1489
- message: `Cost-mismatched-archetype: target=${profile.id} (${chosenGrounding}) selected for ${archetype}. Alternative ${bestAlt.id} (${altGrounding}) is cheaper ($${bestAlt.profile.costInputPer1m}/$${bestAlt.profile.costOutputPer1m} vs $${profile.costInputPer1m}/$${profile.costOutputPer1m} per 1M) at equal-or-better quality.`,
1490
- suggestion: `Consider declaring \`${bestAlt.id}\` as the primary model for this archetype, or relax to posture='open' to let kgauto select among the chain. If the chosen model is required for compliance/brand reasons, set \`policy.posture = 'locked'\` to silence this rule.`,
1491
- recommendationType: profile.provider === bestAlt.profile.provider ? "tier-down" : "model-swap",
1492
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1493
- }
1494
- ];
1495
1451
  }
1496
- function detectModelStaleEvidence(ir, profile) {
1497
- if (!isBrainQueryActiveFor("kgauto_archetype_perf")) return [];
1498
- const archetype = ir.intent.archetype;
1499
- const chosen = getArchetypePerfScore(profile.id, archetype);
1500
- if (chosen.grounding !== "judgment") return [];
1501
- return [
1502
- {
1503
- level: "info",
1504
- code: "model-stale-evidence",
1505
- message: `Model-stale-evidence: target=${profile.id} archetype=${archetype} is judgment-grounded (n=${chosen.n}) despite brain-query mode being active. Measurement substrate is wired but the brain hasn't accumulated >=10 outcomes for this (model, archetype) tuple yet \u2014 routing decisions remain pre-measured for this slot.`,
1506
- suggestion: "Verify that `record()` is being called on every call() outcome with the appropriate `actualModel` and `mutationsApplied` fields. Once the brain accumulates n>=10 rows on this tuple, the score promotes from judgment to measured automatically (5-min SWR cache). No code change required from your side \u2014 this is the substrate signaling the gap.",
1507
- recommendationType: "prompt-fix",
1508
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1452
+ async function doRefresh2(rt, appId) {
1453
+ const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}`;
1454
+ let snap = snapshots2.get(appId);
1455
+ if (!snap) {
1456
+ snap = { data: [], expiresAt: 0, refreshing: false };
1457
+ snapshots2.set(appId, snap);
1458
+ }
1459
+ try {
1460
+ const res = await rt.fetchImpl(url, { method: "GET" });
1461
+ if (!res.ok) {
1462
+ throw new Error(`promote-ready ${res.status}: ${res.statusText}`);
1509
1463
  }
1510
- ];
1511
- }
1512
- function detectTierDown(ir, profile, phase2) {
1513
- if (!phase2 || phase2.fallbackChain.length === 0) return [];
1514
- if (!phase2.profileResolver) return [];
1515
- const archetype = ir.intent.archetype;
1516
- const chosenScore = getArchetypePerfScore(profile.id, archetype);
1517
- const chosenCost = profile.costInputPer1m;
1518
- let bestAlt = null;
1519
- for (const altId of phase2.fallbackChain) {
1520
- const altProfile = phase2.profileResolver(altId);
1521
- if (!altProfile) continue;
1522
- if (altProfile.id === profile.id) continue;
1523
- const altScore = getArchetypePerfScore(altProfile.id, archetype);
1524
- if (altScore.grounding !== "measured") continue;
1525
- if (altScore.score < QUALITY_FLOOR_FOR_RECOMMENDATION) continue;
1526
- if (altScore.score < chosenScore.score) continue;
1527
- if (altProfile.costInputPer1m > chosenCost * TIER_DOWN_COST_RATIO) continue;
1528
- if (!bestAlt || altProfile.costInputPer1m < bestAlt.profile.costInputPer1m || altProfile.costInputPer1m === bestAlt.profile.costInputPer1m && altScore.score > bestAlt.score.score) {
1529
- bestAlt = { id: altId, profile: altProfile, score: altScore };
1464
+ const body = await res.json();
1465
+ if (runtime2 !== rt) return;
1466
+ const rows = Array.isArray(body) ? mapRowsToFindings2(body) : [];
1467
+ snap.data = rows;
1468
+ snap.expiresAt = Date.now() + rt.ttlMs;
1469
+ snap.refreshing = false;
1470
+ } catch (err) {
1471
+ if (runtime2 !== rt) return;
1472
+ snap.refreshing = false;
1473
+ snap.expiresAt = Date.now() + rt.ttlMs;
1474
+ if (!warnedOnce2) {
1475
+ warnedOnce2 = true;
1476
+ (rt.onError ?? defaultOnError2)(err);
1530
1477
  }
1531
1478
  }
1532
- if (!bestAlt) return [];
1533
- const chosenDesc = chosenScore.grounding === "measured" ? `archetypePerf.${archetype}=${chosenScore.score} (measured, n=${chosenScore.n})` : `archetypePerf.${archetype}=${chosenScore.score} (${chosenScore.grounding})`;
1534
- return [
1535
- {
1536
- level: "warn",
1537
- code: "tier-down",
1538
- message: `Tier-down: target=${profile.id} (${chosenDesc}) selected for ${archetype}. Brain shows ${bestAlt.id} delivers equal-or-better quality (archetypePerf.${archetype}=${bestAlt.score.score}, measured, n=${bestAlt.score.n}) at $${bestAlt.profile.costInputPer1m}/$${bestAlt.profile.costOutputPer1m} per 1M vs $${profile.costInputPer1m}/$${profile.costOutputPer1m} \u2014 a measured tier-down opportunity.`,
1539
- suggestion: `Move \`${bestAlt.id}\` to primary for this archetype. The brain has n=${bestAlt.score.n} measured outcomes backing the recommendation; this is data, not opinion. If posture='locked' is required (compliance/brand promise), set it explicitly to silence this rule.`,
1540
- recommendationType: "tier-down",
1541
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1542
- }
1543
- ];
1544
1479
  }
1545
- function detectArchetypePerfFloorBreach(ir, profile) {
1546
- const compat = getModelCompatibility(profile.id, {
1547
- archetype: ir.intent.archetype,
1548
- toolOrchestration: ir.constraints?.toolOrchestration
1549
- });
1550
- if (compat.status === "compatible") return [];
1551
- if (compat.status === "requires-adapter") {
1552
- return [
1553
- {
1554
- level: "warn",
1555
- code: "archetype-perf-floor-breach",
1556
- message: `${profile.id} sits below the archetype floor for ${ir.intent.archetype} (score ${compat.archetypePerf}/10, floor ${6}). A known adapter would lift it: ${compat.adapter.parameter}=${compat.adapter.value}. ${compat.adapter.consequence}`,
1557
- suggestion: `Pass \`ir.constraints.${compat.adapter.parameter} = '${compat.adapter.value}'\` for this call, OR pick a model whose archetypePerf for ${ir.intent.archetype} already clears the floor (call \`getModelCompatibility(modelId, { archetype: '${ir.intent.archetype}' })\` to check). Estimated post-adapter score: ${compat.archetypePerfWithAdapter}/10.`,
1558
- recommendationType: "prompt-fix",
1559
- suggestedAdaptation: compat.adapter,
1560
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1561
- }
1562
- ];
1480
+ function defaultOnError2(err) {
1481
+ console.warn(
1482
+ "[kgauto] promote-ready fetch failed (using empty fallback):",
1483
+ err
1484
+ );
1485
+ }
1486
+ function resolveFetchImpl(injected) {
1487
+ return injected ?? ((...args) => globalThis.fetch(...args));
1488
+ }
1489
+ function normalizeEndpoint(endpoint) {
1490
+ return endpoint.replace(/\/+$/, "");
1491
+ }
1492
+ async function markPromoteReadyHandled(opts) {
1493
+ const {
1494
+ appId,
1495
+ archetype,
1496
+ family,
1497
+ resolution,
1498
+ resolutionNote,
1499
+ brainEndpoint,
1500
+ brainJwt,
1501
+ brainAnonKey,
1502
+ fetch: injectedFetch
1503
+ } = opts;
1504
+ if (!appId) return { ok: false, reason: "app_id_required" };
1505
+ if (!archetype) return { ok: false, reason: "archetype_required" };
1506
+ if (!family) return { ok: false, reason: "family_required" };
1507
+ if (resolution !== "promoted" && resolution !== "declined" && resolution !== "still-evaluating") {
1508
+ return { ok: false, reason: "resolution_invalid" };
1563
1509
  }
1564
- return [
1565
- {
1566
- level: "critical",
1567
- code: "archetype-perf-floor-breach",
1568
- message: `${profile.id} sits below the archetype floor for ${ir.intent.archetype} (score ${compat.archetypePerf}/10, floor ${6}) and no known adapter would lift it. ${compat.reason}`,
1569
- suggestion: `Swap to a model whose archetypePerf for ${ir.intent.archetype} clears the floor. Use \`getModelCompatibility(candidateId, { archetype: '${ir.intent.archetype}' })\` to vet candidates, or \`getDefaultFallbackChain({ archetype: '${ir.intent.archetype}', posture: 'open' })\` for a library-picked chain that respects the floor by construction.`,
1570
- recommendationType: "model-swap",
1571
- docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1572
- }
1573
- ];
1510
+ const doFetch = resolveFetchImpl(injectedFetch);
1511
+ const base = normalizeEndpoint(brainEndpoint);
1512
+ const url = `${base}/rest/v1/promote_ready_findings?app_id=eq.${encodeURIComponent(appId)}&intent_archetype=eq.${encodeURIComponent(archetype)}&family=eq.${encodeURIComponent(family)}&resolved_at=is.null`;
1513
+ const patchBody = {
1514
+ resolved_at: (/* @__PURE__ */ new Date()).toISOString(),
1515
+ resolution
1516
+ };
1517
+ if (resolutionNote !== void 0) {
1518
+ patchBody.resolution_note = resolutionNote;
1519
+ }
1520
+ let res;
1521
+ try {
1522
+ res = await doFetch(url, {
1523
+ method: "PATCH",
1524
+ headers: {
1525
+ Authorization: `Bearer ${brainJwt}`,
1526
+ apikey: brainAnonKey,
1527
+ "Content-Type": "application/json",
1528
+ Accept: "application/json",
1529
+ Prefer: "return=minimal"
1530
+ },
1531
+ body: JSON.stringify(patchBody)
1532
+ });
1533
+ } catch (err) {
1534
+ const msg = err instanceof Error ? err.message : String(err);
1535
+ return { ok: false, reason: `network_error:${msg}` };
1536
+ }
1537
+ if (res.status === 401 || res.status === 403) {
1538
+ return { ok: false, reason: "brain_auth_misconfig" };
1539
+ }
1540
+ if (res.status >= 500) {
1541
+ return { ok: false, reason: "brain_unavailable" };
1542
+ }
1543
+ if (!res.ok) {
1544
+ return { ok: false, reason: `patch_failed:${res.status}` };
1545
+ }
1546
+ return { ok: true };
1574
1547
  }
1575
- function detectStaleExclusionCandidate(ir) {
1576
- if (!isExclusionFindingsBrainActive()) return [];
1577
- if (!ir.appId) return [];
1578
- const findings = getStaleExclusionFindings({
1579
- appId: ir.appId,
1580
- archetype: ir.intent.archetype
1548
+
1549
+ // src/advisor-rules/promote-ready.ts
1550
+ var PROMOTE_READY_THRESHOLDS = {
1551
+ minPassRate: 0.8,
1552
+ minAvgScore: 4
1553
+ };
1554
+ function shouldFirePromoteReady(finding, resolvedPrimary) {
1555
+ if (finding.currentModel !== resolvedPrimary) return false;
1556
+ if (finding.judgePassRate < PROMOTE_READY_THRESHOLDS.minPassRate) return false;
1557
+ if (finding.judgeAvgScore < PROMOTE_READY_THRESHOLDS.minAvgScore) return false;
1558
+ return true;
1559
+ }
1560
+ function deriveFamilyLocal(modelId) {
1561
+ if (modelId.startsWith("claude-opus-")) return "claude-opus";
1562
+ if (modelId.startsWith("claude-sonnet-")) return "claude-sonnet";
1563
+ if (modelId.startsWith("claude-haiku-")) return "claude-haiku";
1564
+ if (/^gemini-.*-flash-lite/.test(modelId)) return "gemini-flash-lite";
1565
+ if (/^gemini-.*-flash/.test(modelId)) return "gemini-flash";
1566
+ if (/^gemini-.*-pro/.test(modelId)) return "gemini-pro";
1567
+ if (/^deepseek-.*-pro/.test(modelId)) return "deepseek-reasoner";
1568
+ if (modelId.startsWith("deepseek-")) return "deepseek-chat";
1569
+ if (modelId.startsWith("gpt-")) return "openai-gpt";
1570
+ return null;
1571
+ }
1572
+ function advisorRulePromoteReady(ctx) {
1573
+ if (!isPromoteReadyBrainActive()) return [];
1574
+ if (!ctx.appId) return [];
1575
+ if (!ctx.resolvedPrimary) return [];
1576
+ const family = deriveFamilyLocal(ctx.resolvedPrimary);
1577
+ if (!family) return [];
1578
+ const findings = loadPromoteReadyFindings({
1579
+ appId: ctx.appId,
1580
+ archetype: ctx.archetype,
1581
+ family
1581
1582
  });
1582
1583
  if (findings.length === 0) return [];
1583
- const ranked = [...findings].sort((a, b) => {
1584
- const sa = a.estimatedSavingsUsd30d ?? -Infinity;
1585
- const sb = b.estimatedSavingsUsd30d ?? -Infinity;
1586
- if (sa !== sb) return sb - sa;
1587
- return confidenceRank(b.confidence) - confidenceRank(a.confidence);
1584
+ const qualifying = findings.filter(
1585
+ (f) => shouldFirePromoteReady(f, ctx.resolvedPrimary)
1586
+ );
1587
+ if (qualifying.length === 0) return [];
1588
+ qualifying.sort((a, b) => {
1589
+ if (a.judgeAvgScore !== b.judgeAvgScore) {
1590
+ return b.judgeAvgScore - a.judgeAvgScore;
1591
+ }
1592
+ return b.judgePassRate - a.judgePassRate;
1588
1593
  });
1589
- const top = ranked[0];
1590
- const extraCount = findings.length - 1;
1591
- const extraNote = extraCount > 0 ? ` (+ ${extraCount} more excluded model${extraCount === 1 ? "" : "s"} for this archetype)` : "";
1594
+ const top = qualifying[0];
1595
+ const pctPass = Math.round(top.judgePassRate * 100);
1596
+ const score = top.judgeAvgScore.toFixed(2);
1597
+ let costClause = "";
1598
+ if (top.costDeltaPct !== null) {
1599
+ const sign = top.costDeltaPct < 0 ? "cheaper" : "more expensive";
1600
+ const magnitude = Math.abs(top.costDeltaPct * 100).toFixed(1);
1601
+ costClause = `, cost ${magnitude}% ${sign}`;
1602
+ }
1603
+ const message = `Probe found ${top.candidateModel} produces equivalent-or-better outputs vs ${top.currentModel} on ${top.sampleN} recent ${top.archetype} prompts (pass rate ${pctPass}%, avg score ${score}/5${costClause}). Consider promoting via markPromoteReadyHandled.`;
1592
1604
  return [
1593
1605
  {
1594
1606
  level: "info",
1595
- code: "stale-exclusion-candidate",
1596
- message: `${top.message}${extraNote}`,
1597
- suggestion: top.suggestion,
1598
- recommendationType: "tier-down",
1607
+ code: "promote-ready",
1608
+ message,
1609
+ suggestion: `Migrate ${top.archetype} traffic from ${top.currentModel} to ${top.candidateModel}, then call markPromoteReadyHandled({ appId, archetype: '${top.archetype}', family: '${top.family}', resolution: 'promoted' }) to silence this advisory.`,
1610
+ // alpha.36 architectural field — not a no-ai-needed case.
1611
+ recommendedArchitecture: void 0,
1599
1612
  docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1600
1613
  }
1601
1614
  ];
1602
1615
  }
1603
- function confidenceRank(c) {
1604
- if (c === "high") return 3;
1605
- if (c === "medium") return 2;
1606
- return 1;
1607
- }
1608
1616
 
1609
- // src/translator.ts
1610
- var TRANSLATOR_FLOOR = ARCHETYPE_FLOOR_DEFAULT;
1611
- var RULE_SEQUENTIAL_TOOL_CLIFF = "sequential-tool-cliff-below-floor";
1612
- var RULE_NARRATION_DRIFT_ANTHROPIC = "narration-drift-anthropic";
1613
- var RULE_NARRATION_THINKING_LEAK_DEEPSEEK = "narration-thinking-leak-deepseek";
1614
- var SEQUENTIAL_TOOL_PREAMBLE = "IMPORTANT: Use one tool call per response. Wait for the tool result before deciding the next tool. Do NOT batch tool calls in parallel.";
1615
- var NARRATION_DRIFT_ANTHROPIC_PREAMBLE = "Output ONLY the requested content. Do not narrate your thought process. Each line \u2264 12 words.";
1616
- var NARRATION_THINKING_LEAK_DEEPSEEK_PREAMBLE = "Reasoning is internal. Output ONLY the requested content; do not emit <thinking> blocks or internal monologue as user-facing text.";
1617
- function matchRule(kind, profile, archetype) {
1618
- if (kind === "tool_call_contract") {
1619
- if (!profile.archetypePerf) return null;
1620
- const archetypeScore = profile.archetypePerf[archetype];
1621
- if (typeof archetypeScore !== "number" || archetypeScore >= TRANSLATOR_FLOOR) {
1622
- return null;
1617
+ // src/advisor-rules/consumer-on-stale-model.ts
1618
+ function isStaleStatus(v) {
1619
+ return v === "legacy" || v === "deprecated";
1620
+ }
1621
+ function asString(v) {
1622
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1623
+ }
1624
+ function mapRowsToFindings3(rows) {
1625
+ const out = [];
1626
+ for (const raw of rows) {
1627
+ if (!raw || typeof raw !== "object") continue;
1628
+ const r = raw;
1629
+ const archetype = asString(r.intent_archetype) ?? asString(r.applies_to_archetype);
1630
+ const staleModel = asString(r.stale_model) ?? asString(r.applies_to_model);
1631
+ const staleProvider = asString(r.stale_provider);
1632
+ const recommendedModel = asString(r.recommended_model);
1633
+ const family = asString(r.family);
1634
+ const message = asString(r.message);
1635
+ if (!archetype || !staleModel || !recommendedModel || !family || !message) {
1636
+ continue;
1623
1637
  }
1624
- return {
1625
- id: RULE_SEQUENTIAL_TOOL_CLIFF,
1626
- preamble: SEQUENTIAL_TOOL_PREAMBLE,
1627
- wireOverrides: { parallelToolCalls: false }
1638
+ if (!isStaleStatus(r.stale_status)) continue;
1639
+ const row = {
1640
+ archetype,
1641
+ staleModel,
1642
+ staleProvider: staleProvider ?? "unknown",
1643
+ staleStatus: r.stale_status,
1644
+ recommendedModel,
1645
+ family,
1646
+ message
1628
1647
  };
1629
- }
1630
- if (kind === "narration_contract") {
1631
- if (profile.provider === "anthropic") {
1632
- return {
1633
- id: RULE_NARRATION_DRIFT_ANTHROPIC,
1634
- preamble: NARRATION_DRIFT_ANTHROPIC_PREAMBLE
1635
- };
1636
- }
1637
- if (profile.provider === "deepseek") {
1638
- return {
1639
- id: RULE_NARRATION_THINKING_LEAK_DEEPSEEK,
1640
- preamble: NARRATION_THINKING_LEAK_DEEPSEEK_PREAMBLE
1641
- };
1648
+ const suggestion = asString(r.suggestion);
1649
+ if (suggestion) row.suggestion = suggestion;
1650
+ if (typeof r.observation_count === "number" && Number.isFinite(r.observation_count)) {
1651
+ row.observationCount = r.observation_count;
1642
1652
  }
1643
- return null;
1653
+ out.push(row);
1644
1654
  }
1645
- return null;
1655
+ return out;
1646
1656
  }
1647
- function applySectionRewrites(args) {
1648
- const { ir, profile, archetype } = args;
1649
- if (!Array.isArray(ir.sections) || ir.sections.length === 0) {
1650
- return { rewrittenIR: ir, rewrites: [] };
1657
+ var snapshots3 = /* @__PURE__ */ new Map();
1658
+ var runtime3;
1659
+ var warnedOnce3 = false;
1660
+ var pendingRefreshes3 = /* @__PURE__ */ new Map();
1661
+ function isStaleModelFindingsBrainActive() {
1662
+ return runtime3 !== void 0;
1663
+ }
1664
+ function getStaleModelFindings(opts) {
1665
+ const rt = runtime3;
1666
+ if (!rt) return [];
1667
+ const appId = opts.appId;
1668
+ if (!appId) return [];
1669
+ let snap = snapshots3.get(appId);
1670
+ if (!snap) {
1671
+ snap = { data: [], expiresAt: 0, refreshing: false };
1672
+ snapshots3.set(appId, snap);
1651
1673
  }
1652
- const rewrites = [];
1653
- const newSections = ir.sections.map((section) => {
1654
- if (!section.kind || section.kind === "arbitrary") return section;
1655
- const rule = matchRule(section.kind, profile, archetype);
1656
- if (!rule) return section;
1657
- const originalText = section.text;
1658
- const transformedText = `${rule.preamble}
1659
-
1660
- ${originalText}`;
1661
- rewrites.push({
1662
- sectionId: section.id,
1663
- kind: section.kind,
1664
- rule: rule.id,
1665
- originalText,
1666
- transformedText,
1667
- ...rule.wireOverrides ? { wireOverrides: rule.wireOverrides } : {}
1668
- });
1669
- return { ...section, text: transformedText };
1670
- });
1671
- if (rewrites.length === 0) {
1672
- return { rewrittenIR: ir, rewrites: [] };
1674
+ const now = Date.now();
1675
+ const stale = snap.expiresAt <= now;
1676
+ if (stale && !snap.refreshing) {
1677
+ snap.refreshing = true;
1678
+ void asyncRefresh3(rt, appId);
1673
1679
  }
1674
- const rewrittenIR = { ...ir, sections: newSections };
1675
- return { rewrittenIR, rewrites };
1676
- }
1677
-
1678
- // src/models-brain.ts
1679
- function isModelRow(x) {
1680
- if (!x || typeof x !== "object") return false;
1681
- const r = x;
1682
- return typeof r.model_id === "string" && typeof r.provider === "string";
1683
- }
1684
- function isAliasRow(x) {
1685
- if (!x || typeof x !== "object") return false;
1686
- const r = x;
1687
- return typeof r.alias_id === "string" && typeof r.canonical_id === "string";
1680
+ if (opts.archetype) {
1681
+ return snap.data.filter((f) => f.archetype === opts.archetype);
1682
+ }
1683
+ return snap.data;
1688
1684
  }
1689
- function rowToProfile(row) {
1685
+ async function asyncRefresh3(rt, appId) {
1686
+ const promise = doRefresh3(rt, appId);
1687
+ pendingRefreshes3.set(appId, promise);
1690
1688
  try {
1691
- if (row.cliffs !== void 0 && row.cliffs !== null && !Array.isArray(row.cliffs)) {
1692
- return null;
1689
+ await promise;
1690
+ } finally {
1691
+ if (pendingRefreshes3.get(appId) === promise) {
1692
+ pendingRefreshes3.delete(appId);
1693
1693
  }
1694
- if (row.recovery !== void 0 && row.recovery !== null && !Array.isArray(row.recovery)) {
1695
- return null;
1694
+ }
1695
+ }
1696
+ async function doRefresh3(rt, appId) {
1697
+ const url = `${rt.endpoint}?app_id=${encodeURIComponent(appId)}`;
1698
+ let snap = snapshots3.get(appId);
1699
+ if (!snap) {
1700
+ snap = { data: [], expiresAt: 0, refreshing: false };
1701
+ snapshots3.set(appId, snap);
1702
+ }
1703
+ try {
1704
+ const res = await rt.fetchImpl(url, { method: "GET" });
1705
+ if (!res.ok) {
1706
+ throw new Error(`stale-model findings ${res.status}: ${res.statusText}`);
1696
1707
  }
1697
- if (row.lowering !== void 0 && row.lowering !== null && (typeof row.lowering !== "object" || Array.isArray(row.lowering))) {
1698
- return null;
1708
+ const body = await res.json();
1709
+ if (runtime3 !== rt) return;
1710
+ const rows = Array.isArray(body) ? mapRowsToFindings3(body) : [];
1711
+ snap.data = rows;
1712
+ snap.expiresAt = Date.now() + rt.ttlMs;
1713
+ snap.refreshing = false;
1714
+ } catch (err) {
1715
+ if (runtime3 !== rt) return;
1716
+ snap.refreshing = false;
1717
+ snap.expiresAt = Date.now() + rt.ttlMs;
1718
+ if (!warnedOnce3) {
1719
+ warnedOnce3 = true;
1720
+ (rt.onError ?? defaultOnError3)(err);
1699
1721
  }
1700
- return {
1701
- id: row.model_id,
1702
- provider: row.provider,
1703
- status: row.status ?? "current",
1704
- maxContextTokens: row.max_context_tokens ?? 0,
1705
- maxOutputTokens: row.max_output_tokens ?? 0,
1706
- maxTools: row.max_tools ?? 0,
1707
- parallelToolCalls: row.parallel_tool_calls ?? false,
1708
- structuredOutput: row.structured_output ?? "none",
1709
- systemPromptMode: row.system_prompt_mode ?? "inline",
1710
- streaming: row.streaming ?? true,
1711
- cliffs: row.cliffs ?? [],
1712
- costInputPer1m: row.cost_input_per_1m ?? 0,
1713
- costOutputPer1m: row.cost_output_per_1m ?? 0,
1714
- lowering: row.lowering ?? { system: { mode: "inline" }, cache: { strategy: "unsupported" } },
1715
- recovery: row.recovery ?? [],
1716
- strengths: row.strengths ?? [],
1717
- weaknesses: row.weaknesses ?? [],
1718
- notes: row.notes ?? void 0,
1719
- verifiedAgainstDocs: row.verified_against_docs ?? void 0,
1720
- archetypePerf: row.archetype_perf ?? void 0,
1721
- // alpha.41 — family-resolution fields. `family` may be null pre-
1722
- // migration 024; runtime falls back to deriveFamilyFromModelId at
1723
- // resolution time (see family-resolution.ts, G1).
1724
- family: row.family ?? void 0,
1725
- versionAdded: row.version_added ?? void 0,
1726
- active: row.active ?? void 0
1727
- };
1728
- } catch {
1729
- return null;
1730
1722
  }
1731
1723
  }
1732
- function profileToRow(profile, opts = {}) {
1733
- const row = {
1734
- model_id: profile.id,
1735
- provider: profile.provider,
1736
- status: profile.status,
1737
- max_context_tokens: profile.maxContextTokens,
1738
- max_output_tokens: profile.maxOutputTokens,
1739
- max_tools: profile.maxTools,
1740
- parallel_tool_calls: profile.parallelToolCalls,
1741
- structured_output: profile.structuredOutput,
1742
- system_prompt_mode: profile.systemPromptMode,
1743
- streaming: profile.streaming,
1744
- cliffs: profile.cliffs,
1745
- cost_input_per_1m: profile.costInputPer1m,
1746
- cost_output_per_1m: profile.costOutputPer1m,
1747
- lowering: profile.lowering,
1748
- recovery: profile.recovery,
1749
- strengths: profile.strengths,
1750
- weaknesses: profile.weaknesses,
1751
- notes: profile.notes ?? null,
1752
- archetype_perf: profile.archetypePerf ?? null,
1753
- active: opts.active ?? profile.active ?? true,
1754
- // alpha.41 — round-trip family + version_added when present on profile.
1755
- // version_added is operator-controlled via opts; profile-side value is
1756
- // used only when opts didn't override.
1757
- family: profile.family ?? null
1758
- };
1759
- if (opts.verifiedAgainstDocs !== void 0) {
1760
- row.verified_against_docs = opts.verifiedAgainstDocs;
1761
- } else if (profile.verifiedAgainstDocs !== void 0) {
1762
- const v = profile.verifiedAgainstDocs;
1763
- row.verified_against_docs = /^\d{4}-\d{2}-\d{2}/.test(v) ? v : null;
1764
- }
1765
- if (opts.versionAdded !== void 0) row.version_added = opts.versionAdded;
1766
- else if (profile.versionAdded !== void 0) row.version_added = profile.versionAdded;
1767
- if (opts.versionRemoved !== void 0) row.version_removed = opts.versionRemoved;
1768
- return row;
1724
+ function defaultOnError3(err) {
1725
+ console.warn(
1726
+ "[kgauto] stale-model findings fetch failed (using empty fallback):",
1727
+ err
1728
+ );
1729
+ }
1730
+ var CONSUMER_ON_STALE_MODEL_RULE_CODE = "consumer-on-stale-model";
1731
+ function advisorRuleConsumerOnStaleModel(ir) {
1732
+ if (!isStaleModelFindingsBrainActive()) return [];
1733
+ if (!ir.appId) return [];
1734
+ const findings = getStaleModelFindings({
1735
+ appId: ir.appId,
1736
+ archetype: ir.intent.archetype
1737
+ });
1738
+ if (findings.length === 0) return [];
1739
+ const ranked = [...findings].sort((a, b) => {
1740
+ if (a.staleStatus !== b.staleStatus) {
1741
+ return a.staleStatus === "deprecated" ? -1 : 1;
1742
+ }
1743
+ return a.staleModel.localeCompare(b.staleModel);
1744
+ });
1745
+ const top = ranked[0];
1746
+ const extraCount = findings.length - 1;
1747
+ const extraNote = extraCount > 0 ? ` (+ ${extraCount} more stale model${extraCount === 1 ? "" : "s"} for this archetype)` : "";
1748
+ return [
1749
+ {
1750
+ level: "warn",
1751
+ code: CONSUMER_ON_STALE_MODEL_RULE_CODE,
1752
+ message: `${top.message}${extraNote}`,
1753
+ suggestion: top.suggestion ?? `Migrate ${top.staleModel} \u2192 ${top.recommendedModel} for archetype "${top.archetype}". The newer model is the current latest in the "${top.family}" family; the stale one is ${top.staleStatus}.`,
1754
+ recommendationType: "model-swap",
1755
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1756
+ }
1757
+ ];
1769
1758
  }
1770
- function mapRowsToModels(rows) {
1771
- const out = /* @__PURE__ */ new Map();
1772
- for (const row of rows) {
1773
- if (!isModelRow(row)) continue;
1774
- const profile = rowToProfile(row);
1775
- if (profile) out.set(profile.id, profile);
1759
+
1760
+ // src/archetype-fits.ts
1761
+ var ARCHETYPE_FAMILY_FITS = Object.freeze([
1762
+ {
1763
+ archetype: "plan",
1764
+ betterFitFamily: "deepseek-reasoner",
1765
+ reason: "Plan archetype is reasoning-shaped (multi-step chains, hypothesis-and-check, sub-goal decomposition) \u2014 exactly where reasoner-family models excel. Sonnet/Opus produce plans but at higher cost; reasoners produce equivalent-or-better plans at 7-17x lower cost at current promo pricing (deepseek-v4-pro $0.435/$0.87 per 1M promo through 2026-05-31 vs sonnet $3/$15).",
1766
+ costGuidance: "substantially cheaper at current pricing (deepseek-v4-pro promo: ~7-17x cheaper than sonnet)"
1767
+ },
1768
+ {
1769
+ archetype: "critique",
1770
+ betterFitFamily: "deepseek-reasoner",
1771
+ reason: "Critique archetype rewards epistemic humility and explicit reasoning \u2014 reasoner-family default behavior. Sonnet/Opus over-confident on critique tasks; reasoners surface uncertainty productively.",
1772
+ costGuidance: "comparable or cheaper at current pricing"
1776
1773
  }
1777
- return out;
1774
+ ]);
1775
+ function findBetterFit(archetype, currentFamily) {
1776
+ for (const fit of ARCHETYPE_FAMILY_FITS) {
1777
+ if (fit.archetype !== archetype) continue;
1778
+ if (fit.betterFitFamily === currentFamily) return null;
1779
+ return fit;
1780
+ }
1781
+ return null;
1778
1782
  }
1779
- function mapRowsToAliases(rows) {
1780
- const out = {};
1781
- for (const row of rows) {
1782
- if (!isAliasRow(row)) continue;
1783
- out[row.alias_id] = row.canonical_id;
1783
+
1784
+ // src/advisor-rules/cross-family-fit.ts
1785
+ function familyHasCurrentActiveModel(family) {
1786
+ for (const profile of allProfiles()) {
1787
+ const profileFamily = profile.family ?? deriveFamilyFromModelId(profile.id);
1788
+ if (profileFamily !== family) continue;
1789
+ if (profile.status !== "current") continue;
1790
+ if (profile.active === false) continue;
1791
+ return true;
1784
1792
  }
1785
- return out;
1793
+ return false;
1786
1794
  }
1787
- function bundledModels() {
1788
- return new Map(allProfilesRaw().map((p) => [p.id, p]));
1795
+ function listCandidatesInFamily(family) {
1796
+ const candidates = [];
1797
+ for (const profile of allProfiles()) {
1798
+ const profileFamily = profile.family ?? deriveFamilyFromModelId(profile.id);
1799
+ if (profileFamily !== family) continue;
1800
+ if (profile.status !== "current") continue;
1801
+ if (profile.active === false) continue;
1802
+ candidates.push(profile.id);
1803
+ if (candidates.length >= 3) break;
1804
+ }
1805
+ return candidates;
1789
1806
  }
1790
- function bundledAliases() {
1791
- return { ...ALIASES };
1807
+ function advisorRuleCrossFamilyFit(ctx) {
1808
+ if (!ctx.resolvedPrimary) return [];
1809
+ const currentFamily = deriveFamilyFromModelId(ctx.resolvedPrimary);
1810
+ if (!currentFamily) return [];
1811
+ const fit = findBetterFit(ctx.archetype, currentFamily);
1812
+ if (!fit) return [];
1813
+ if (!familyHasCurrentActiveModel(fit.betterFitFamily)) return [];
1814
+ const candidates = listCandidatesInFamily(fit.betterFitFamily);
1815
+ if (candidates.length === 0) return [];
1816
+ const candidateStr = candidates.join(", ");
1817
+ const message = `Your ${currentFamily} call on ${ctx.archetype} could shift to ${fit.betterFitFamily} \u2014 typically better quality + ${fit.costGuidance}. Suggested candidates: ${candidateStr}.`;
1818
+ return [
1819
+ {
1820
+ level: "info",
1821
+ code: "cross-family-fit-candidate",
1822
+ ownership: "consumer-actionable",
1823
+ message,
1824
+ suggestion: `Swap the model literal in \`ir.models\` to one of: ${candidateStr}. Or call \`getRecommendedPrimary({ family: '${fit.betterFitFamily}', archetype: '${ctx.archetype}', fallback: { id: '${candidates[0]}', reason: 'cross-family-fit-recommendation' } })\` to let kgauto resolve to the current+active family member.`,
1825
+ recommendationType: "model-swap",
1826
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1827
+ }
1828
+ ];
1792
1829
  }
1793
- var loadModelsFromBrain = createBrainQueryCache({
1794
- table: "kgauto_models",
1795
- mapRows: mapRowsToModels,
1796
- bundledFallback: bundledModels
1797
- });
1798
- var loadAliasesFromBrain = createBrainQueryCache({
1799
- table: "kgauto_aliases",
1800
- mapRows: mapRowsToAliases,
1801
- bundledFallback: bundledAliases
1802
- });
1803
- _setProfileBrainHook({
1804
- getProfile: (canonical) => loadModelsFromBrain().get(canonical),
1805
- resolveAlias: (id) => loadAliasesFromBrain()[id]
1806
- });
1807
1830
 
1808
- // src/family-resolution.ts
1809
- var FamilyResolutionError = class extends Error {
1810
- family;
1811
- cause;
1812
- constructor(family, cause) {
1813
- super(
1814
- `Family "${family}" did not resolve to any current+active model. ${cause}. Pass a literal model id in ir.models, or call getRecommendedPrimary({ family, fallback }) at IR-construction time.`
1815
- );
1816
- this.name = "FamilyResolutionError";
1817
- this.family = family;
1818
- this.cause = cause;
1831
+ // src/advisor.ts
1832
+ var QUALITY_FLOOR_FOR_RECOMMENDATION = 6;
1833
+ var TIER_DOWN_COST_RATIO = 0.5;
1834
+ var COST_MISMATCHED_CHOSEN_SCORE_CEILING = 7;
1835
+ var PRODUCER_OWNED_RULE_CODES = Object.freeze(
1836
+ /* @__PURE__ */ new Set(["model-stale-evidence", "promote-ready"])
1837
+ );
1838
+ function deriveOwnership(code, selfDeclared) {
1839
+ if (selfDeclared) return selfDeclared;
1840
+ return PRODUCER_OWNED_RULE_CODES.has(code) ? "producer-owned" : "consumer-actionable";
1841
+ }
1842
+ function runAdvisor(ir, result, profile, policy, phase2) {
1843
+ const out = [];
1844
+ out.push(...detectCachingOff(ir, profile));
1845
+ out.push(...detectSingleChunkSystem(ir, profile));
1846
+ out.push(...detectToolBloat(ir, result));
1847
+ out.push(...detectHistoryUncached(ir, profile));
1848
+ out.push(...detectSingleModelArray(ir, policy));
1849
+ if (policy?.posture !== "locked") {
1850
+ out.push(...detectCostMismatchedArchetype(ir, profile, phase2));
1851
+ out.push(...detectModelStaleEvidence(ir, profile));
1852
+ out.push(...detectTierDown(ir, profile, phase2));
1819
1853
  }
1820
- };
1821
- function deriveFamilyFromModelId(modelId) {
1822
- if (typeof modelId !== "string" || modelId.length === 0) return null;
1823
- if (modelId.startsWith("claude-opus-")) return "claude-opus";
1824
- if (modelId.startsWith("claude-sonnet-")) return "claude-sonnet";
1825
- if (modelId.startsWith("claude-haiku-")) return "claude-haiku";
1826
- if (modelId.startsWith("gemini-") && modelId.includes("flash-lite")) {
1827
- return "gemini-flash-lite";
1854
+ if (!translatorClearedToolCallCliff(phase2)) {
1855
+ out.push(...detectArchetypePerfFloorBreach(ir, profile));
1828
1856
  }
1829
- if (modelId.startsWith("gemini-") && modelId.includes("flash")) {
1830
- return "gemini-flash";
1857
+ if (policy?.posture !== "locked") {
1858
+ out.push(...detectStaleExclusionCandidate(ir));
1831
1859
  }
1832
- if (modelId.startsWith("gemini-") && modelId.includes("pro")) {
1833
- return "gemini-pro";
1860
+ if (policy?.posture !== "locked" && ir.appId) {
1861
+ out.push(
1862
+ ...advisorRulePromoteReady({
1863
+ appId: ir.appId,
1864
+ archetype: ir.intent.archetype,
1865
+ resolvedPrimary: profile.id
1866
+ })
1867
+ );
1868
+ out.push(...advisorRuleConsumerOnStaleModel(ir));
1834
1869
  }
1835
- if (modelId.startsWith("deepseek-") && modelId.includes("pro")) {
1836
- return "deepseek-reasoner";
1870
+ if (policy?.posture !== "locked") {
1871
+ out.push(
1872
+ ...advisorRuleCrossFamilyFit({
1873
+ archetype: ir.intent.archetype,
1874
+ resolvedPrimary: profile.id
1875
+ })
1876
+ );
1837
1877
  }
1838
- if (modelId.startsWith("deepseek-")) return "deepseek-chat";
1839
- if (modelId.startsWith("gpt-")) return "openai-gpt";
1840
- return null;
1878
+ return out;
1841
1879
  }
1842
- function familyOf(profile) {
1843
- return profile.family ?? deriveFamilyFromModelId(profile.id);
1880
+ function translatorClearedToolCallCliff(phase2) {
1881
+ const rewrites = phase2?.sectionRewritesApplied;
1882
+ if (!rewrites || rewrites.length === 0) return false;
1883
+ for (const rw of rewrites) {
1884
+ if (rw.kind === "tool_call_contract") return true;
1885
+ }
1886
+ return false;
1887
+ }
1888
+ function detectCachingOff(ir, profile) {
1889
+ if (profile.provider !== "anthropic") return [];
1890
+ const totalChars = ir.sections.reduce((s, sec) => s + sec.text.length, 0);
1891
+ if (totalChars < 2e3) return [];
1892
+ const anyCacheable = ir.sections.some((s) => s.cacheable === true);
1893
+ if (anyCacheable) return [];
1894
+ return [
1895
+ {
1896
+ level: "warn",
1897
+ code: "caching-off-on-claude",
1898
+ message: `System prompt is ${totalChars} chars on Anthropic but no PromptSection has cacheable=true. Anthropic prompt caching cuts cached-prefix input cost by ~90% on subsequent calls; without it, every turn re-pays full price for the static system context.`,
1899
+ suggestion: "Mark stable system sections (role, persona, tool policy) with `cacheable: true`. The lowering pass concatenates cacheable sections into a single cache-controlled block before the dynamic ones.",
1900
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1901
+ }
1902
+ ];
1903
+ }
1904
+ function detectSingleChunkSystem(ir, profile) {
1905
+ if (profile.provider !== "anthropic") return [];
1906
+ if (ir.sections.length !== 1) return [];
1907
+ const only = ir.sections[0];
1908
+ if (!only || only.text.length <= 1e3) return [];
1909
+ return [
1910
+ {
1911
+ level: "info",
1912
+ code: "single-chunk-system",
1913
+ message: `System prompt is a single ${only.text.length}-char chunk. Splitting into NamedChunks (static role/persona vs dynamic context) gives the lowering pass a finer cache-marker boundary \u2014 only the static portion needs to be byte-stable for the cache to hit.`,
1914
+ suggestion: "Refactor the system builder to return an array of `PromptSection` shaped { id, text, cacheable?: boolean }. Static chunks (role, persona, tool policy) get `cacheable: true`; dynamic ones (current context, today's date) don't.",
1915
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1916
+ }
1917
+ ];
1918
+ }
1919
+ function detectToolBloat(ir, result) {
1920
+ const SHORT_OUTPUT = /* @__PURE__ */ new Set([
1921
+ "classify",
1922
+ "extract",
1923
+ "summarize",
1924
+ "transform",
1925
+ "critique"
1926
+ ]);
1927
+ if (!ir.tools || ir.tools.length === 0) return [];
1928
+ const toolsKept = result.diagnostics.toolsKept;
1929
+ if (toolsKept <= 10) return [];
1930
+ if (!SHORT_OUTPUT.has(ir.intent.archetype)) return [];
1931
+ return [
1932
+ {
1933
+ level: "warn",
1934
+ code: "tool-bloat",
1935
+ message: `${toolsKept} tools kept after the relevance pass for archetype="${ir.intent.archetype}" (consumer declared ${ir.tools.length}). This archetype is short-output and rarely needs more than 3 tools; each tool definition eats ~350 tokens of context budget.`,
1936
+ suggestion: "Tighten `relevanceByIntent: { [archetype]: 0..1 }` per ToolDefinition. Tools below `toolRelevanceThreshold` (default 0.2) get dropped. Without `relevanceByIntent`, every tool defaults to neutral (0.5) and stays.",
1937
+ docsUrl: "https://github.com/stue/kgauto/blob/main/v2/README.md#tools"
1938
+ }
1939
+ ];
1940
+ }
1941
+ function detectHistoryUncached(ir, profile) {
1942
+ if (profile.provider !== "anthropic") return [];
1943
+ if (!ir.history || ir.history.length < 2) return [];
1944
+ if (ir.historyCachePolicy && ir.historyCachePolicy.strategy !== "none") {
1945
+ return [];
1946
+ }
1947
+ return [
1948
+ {
1949
+ level: "warn",
1950
+ code: "history-uncached-on-claude",
1951
+ message: `${ir.history.length} history messages on Anthropic with no historyCachePolicy. Every turn re-pays for the full conversation context; with caching, subsequent turns hit the cache at ~10% the input cost.`,
1952
+ suggestion: "Set `historyCachePolicy: { strategy: 'all-but-latest' }` on this IR. The lowering pass marks the message immediately preceding currentTurn with cache_control; subsequent turns whose history prefix matches byte-for-byte hit the cache.",
1953
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1954
+ }
1955
+ ];
1956
+ }
1957
+ function detectSingleModelArray(ir, policy) {
1958
+ if (ir.models.length !== 1) return [];
1959
+ if (policy?.posture === "locked") return [];
1960
+ const only = ir.models[0];
1961
+ return [
1962
+ {
1963
+ level: "warn",
1964
+ code: "single-model-array",
1965
+ message: `\`ir.models\` has length 1 (only "${only}") and posture is not 'locked'. A single-model chain has no safety net \u2014 the first 429 / 5xx / cliff hits the user as a failure. Master plan \xA71.2 closes the reliability gap with a 2-step minimum.`,
1966
+ suggestion: "Use `getDefaultFallbackChain({ archetype: ir.intent.archetype, primary: '" + only + "', posture: 'preferred' })` for a user-anchored chain, or `getDefaultFallbackChain({ archetype, posture: 'open' })` for library-picked. If single-model is intentional (compliance/brand promise), set `policy.posture = 'locked'` to silence this rule.",
1967
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#single-model-array"
1968
+ }
1969
+ ];
1970
+ }
1971
+ function detectCostMismatchedArchetype(ir, profile, phase2) {
1972
+ if (!phase2 || phase2.fallbackChain.length === 0) return [];
1973
+ if (!phase2.profileResolver) return [];
1974
+ const archetype = ir.intent.archetype;
1975
+ const chosenScore = getArchetypePerfScore(profile.id, archetype);
1976
+ const chosenHasRoomToGrow = chosenScore.grounding === "judgment" || chosenScore.score < COST_MISMATCHED_CHOSEN_SCORE_CEILING;
1977
+ if (!chosenHasRoomToGrow) return [];
1978
+ let bestAlt = null;
1979
+ for (const altId of phase2.fallbackChain) {
1980
+ const altProfile = phase2.profileResolver(altId);
1981
+ if (!altProfile) continue;
1982
+ if (altProfile.id === profile.id) continue;
1983
+ const altScore = getArchetypePerfScore(altProfile.id, archetype);
1984
+ if (altScore.score < QUALITY_FLOOR_FOR_RECOMMENDATION) continue;
1985
+ if (altScore.score < chosenScore.score) continue;
1986
+ if (altProfile.costInputPer1m >= profile.costInputPer1m) continue;
1987
+ if (!bestAlt || altScore.score > bestAlt.score.score || altScore.score === bestAlt.score.score && altProfile.costInputPer1m < bestAlt.profile.costInputPer1m) {
1988
+ bestAlt = { id: altId, profile: altProfile, score: altScore };
1989
+ }
1990
+ }
1991
+ if (!bestAlt) return [];
1992
+ const tierDownWouldFire = bestAlt.score.grounding === "measured" && bestAlt.profile.costInputPer1m <= profile.costInputPer1m * TIER_DOWN_COST_RATIO;
1993
+ if (tierDownWouldFire) return [];
1994
+ const chosenGrounding = chosenScore.grounding === "judgment" ? `archetypePerf.${archetype}=judgment` : `archetypePerf.${archetype}=${chosenScore.score}`;
1995
+ const altGrounding = bestAlt.score.grounding === "measured" ? `archetypePerf.${archetype}=${bestAlt.score.score}, measured, n=${bestAlt.score.n}` : `archetypePerf.${archetype}=${bestAlt.score.score}, judgment`;
1996
+ return [
1997
+ {
1998
+ level: "warn",
1999
+ code: "cost-mismatched-archetype",
2000
+ message: `Cost-mismatched-archetype: target=${profile.id} (${chosenGrounding}) selected for ${archetype}. Alternative ${bestAlt.id} (${altGrounding}) is cheaper ($${bestAlt.profile.costInputPer1m}/$${bestAlt.profile.costOutputPer1m} vs $${profile.costInputPer1m}/$${profile.costOutputPer1m} per 1M) at equal-or-better quality.`,
2001
+ suggestion: `Consider declaring \`${bestAlt.id}\` as the primary model for this archetype, or relax to posture='open' to let kgauto select among the chain. If the chosen model is required for compliance/brand reasons, set \`policy.posture = 'locked'\` to silence this rule.`,
2002
+ recommendationType: profile.provider === bestAlt.profile.provider ? "tier-down" : "model-swap",
2003
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2004
+ }
2005
+ ];
1844
2006
  }
1845
- function archetypePerfFor(profile, archetype) {
1846
- if (!archetype) return 0;
1847
- const score = profile.archetypePerf?.[archetype];
1848
- return typeof score === "number" ? score : 5;
2007
+ function detectModelStaleEvidence(ir, profile) {
2008
+ if (!isBrainQueryActiveFor("kgauto_archetype_perf")) return [];
2009
+ const archetype = ir.intent.archetype;
2010
+ const chosen = getArchetypePerfScore(profile.id, archetype);
2011
+ if (chosen.grounding !== "judgment") return [];
2012
+ return [
2013
+ {
2014
+ level: "info",
2015
+ code: "model-stale-evidence",
2016
+ message: `Model-stale-evidence: target=${profile.id} archetype=${archetype} is judgment-grounded (n=${chosen.n}) despite brain-query mode being active. Measurement substrate is wired but the brain hasn't accumulated >=10 outcomes for this (model, archetype) tuple yet \u2014 routing decisions remain pre-measured for this slot.`,
2017
+ suggestion: "Verify that `record()` is being called on every call() outcome with the appropriate `actualModel` and `mutationsApplied` fields. Once the brain accumulates n>=10 rows on this tuple, the score promotes from judgment to measured automatically (5-min SWR cache). No code change required from your side \u2014 this is the substrate signaling the gap.",
2018
+ recommendationType: "prompt-fix",
2019
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2020
+ }
2021
+ ];
1849
2022
  }
1850
- function selectCandidates(registry, family, archetype, appId) {
1851
- const out = [];
1852
- const excluded = /* @__PURE__ */ new Set();
1853
- if (appId && archetype) {
1854
- const findings = getStaleExclusionFindings({ appId, archetype });
1855
- for (const f of findings) {
1856
- if (f.verdict === "stay-excluded") excluded.add(f.excludedModel);
2023
+ function detectTierDown(ir, profile, phase2) {
2024
+ if (!phase2 || phase2.fallbackChain.length === 0) return [];
2025
+ if (!phase2.profileResolver) return [];
2026
+ const archetype = ir.intent.archetype;
2027
+ const chosenScore = getArchetypePerfScore(profile.id, archetype);
2028
+ const chosenCost = profile.costInputPer1m;
2029
+ let bestAlt = null;
2030
+ for (const altId of phase2.fallbackChain) {
2031
+ const altProfile = phase2.profileResolver(altId);
2032
+ if (!altProfile) continue;
2033
+ if (altProfile.id === profile.id) continue;
2034
+ const altScore = getArchetypePerfScore(altProfile.id, archetype);
2035
+ if (altScore.grounding !== "measured") continue;
2036
+ if (altScore.score < QUALITY_FLOOR_FOR_RECOMMENDATION) continue;
2037
+ if (altScore.score < chosenScore.score) continue;
2038
+ if (altProfile.costInputPer1m > chosenCost * TIER_DOWN_COST_RATIO) continue;
2039
+ if (!bestAlt || altProfile.costInputPer1m < bestAlt.profile.costInputPer1m || altProfile.costInputPer1m === bestAlt.profile.costInputPer1m && altScore.score > bestAlt.score.score) {
2040
+ bestAlt = { id: altId, profile: altProfile, score: altScore };
1857
2041
  }
1858
2042
  }
1859
- for (const profile of registry.values()) {
1860
- if (familyOf(profile) !== family) continue;
1861
- if (profile.status !== "current") continue;
1862
- if (profile.active === false) continue;
1863
- if (archetype) {
1864
- if (archetypePerfFor(profile, archetype) < ARCHETYPE_FLOOR_DEFAULT) continue;
2043
+ if (!bestAlt) return [];
2044
+ const chosenDesc = chosenScore.grounding === "measured" ? `archetypePerf.${archetype}=${chosenScore.score} (measured, n=${chosenScore.n})` : `archetypePerf.${archetype}=${chosenScore.score} (${chosenScore.grounding})`;
2045
+ return [
2046
+ {
2047
+ level: "warn",
2048
+ code: "tier-down",
2049
+ message: `Tier-down: target=${profile.id} (${chosenDesc}) selected for ${archetype}. Brain shows ${bestAlt.id} delivers equal-or-better quality (archetypePerf.${archetype}=${bestAlt.score.score}, measured, n=${bestAlt.score.n}) at $${bestAlt.profile.costInputPer1m}/$${bestAlt.profile.costOutputPer1m} per 1M vs $${profile.costInputPer1m}/$${profile.costOutputPer1m} \u2014 a measured tier-down opportunity.`,
2050
+ suggestion: `Move \`${bestAlt.id}\` to primary for this archetype. The brain has n=${bestAlt.score.n} measured outcomes backing the recommendation; this is data, not opinion. If posture='locked' is required (compliance/brand promise), set it explicitly to silence this rule.`,
2051
+ recommendationType: "tier-down",
2052
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
1865
2053
  }
1866
- if (excluded.has(profile.id)) continue;
1867
- out.push(profile);
2054
+ ];
2055
+ }
2056
+ function detectArchetypePerfFloorBreach(ir, profile) {
2057
+ const compat = getModelCompatibility(profile.id, {
2058
+ archetype: ir.intent.archetype,
2059
+ toolOrchestration: ir.constraints?.toolOrchestration
2060
+ });
2061
+ if (compat.status === "compatible") return [];
2062
+ if (compat.status === "requires-adapter") {
2063
+ return [
2064
+ {
2065
+ level: "warn",
2066
+ code: "archetype-perf-floor-breach",
2067
+ message: `${profile.id} sits below the archetype floor for ${ir.intent.archetype} (score ${compat.archetypePerf}/10, floor ${6}). A known adapter would lift it: ${compat.adapter.parameter}=${compat.adapter.value}. ${compat.adapter.consequence}`,
2068
+ suggestion: `Pass \`ir.constraints.${compat.adapter.parameter} = '${compat.adapter.value}'\` for this call, OR pick a model whose archetypePerf for ${ir.intent.archetype} already clears the floor (call \`getModelCompatibility(modelId, { archetype: '${ir.intent.archetype}' })\` to check). Estimated post-adapter score: ${compat.archetypePerfWithAdapter}/10.`,
2069
+ recommendationType: "prompt-fix",
2070
+ suggestedAdaptation: compat.adapter,
2071
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2072
+ }
2073
+ ];
1868
2074
  }
1869
- return out;
2075
+ return [
2076
+ {
2077
+ level: "critical",
2078
+ code: "archetype-perf-floor-breach",
2079
+ message: `${profile.id} sits below the archetype floor for ${ir.intent.archetype} (score ${compat.archetypePerf}/10, floor ${6}) and no known adapter would lift it. ${compat.reason}`,
2080
+ suggestion: `Swap to a model whose archetypePerf for ${ir.intent.archetype} clears the floor. Use \`getModelCompatibility(candidateId, { archetype: '${ir.intent.archetype}' })\` to vet candidates, or \`getDefaultFallbackChain({ archetype: '${ir.intent.archetype}', posture: 'open' })\` for a library-picked chain that respects the floor by construction.`,
2081
+ recommendationType: "model-swap",
2082
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2083
+ }
2084
+ ];
1870
2085
  }
1871
- function sortCandidates(candidates, archetype) {
1872
- const sorted = [...candidates];
1873
- sorted.sort((a, b) => {
1874
- const perfA = archetypePerfFor(a, archetype);
1875
- const perfB = archetypePerfFor(b, archetype);
1876
- if (perfA !== perfB) return perfB - perfA;
1877
- const vA = a.versionAdded ?? "";
1878
- const vB = b.versionAdded ?? "";
1879
- if (vA !== vB) return vA < vB ? 1 : -1;
1880
- return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
2086
+ function detectStaleExclusionCandidate(ir) {
2087
+ if (!isExclusionFindingsBrainActive()) return [];
2088
+ if (!ir.appId) return [];
2089
+ const findings = getStaleExclusionFindings({
2090
+ appId: ir.appId,
2091
+ archetype: ir.intent.archetype
1881
2092
  });
1882
- return sorted;
2093
+ if (findings.length === 0) return [];
2094
+ const ranked = [...findings].sort((a, b) => {
2095
+ const sa = a.estimatedSavingsUsd30d ?? -Infinity;
2096
+ const sb = b.estimatedSavingsUsd30d ?? -Infinity;
2097
+ if (sa !== sb) return sb - sa;
2098
+ return confidenceRank(b.confidence) - confidenceRank(a.confidence);
2099
+ });
2100
+ const top = ranked[0];
2101
+ const extraCount = findings.length - 1;
2102
+ const extraNote = extraCount > 0 ? ` (+ ${extraCount} more excluded model${extraCount === 1 ? "" : "s"} for this archetype)` : "";
2103
+ return [
2104
+ {
2105
+ level: "info",
2106
+ code: "stale-exclusion-candidate",
2107
+ message: `${top.message}${extraNote}`,
2108
+ suggestion: top.suggestion,
2109
+ recommendationType: "tier-down",
2110
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories"
2111
+ }
2112
+ ];
1883
2113
  }
1884
- function getRecommendedPrimary(opts) {
1885
- if (opts.posture === "locked") return opts.fallback;
1886
- const registry = loadModelsFromBrain();
1887
- const candidates = selectCandidates(
1888
- registry,
1889
- opts.family,
1890
- opts.archetype,
1891
- opts.appId
1892
- );
1893
- if (candidates.length === 0) return opts.fallback;
1894
- const sorted = sortCandidates(candidates, opts.archetype);
1895
- return sorted[0]?.id ?? opts.fallback;
2114
+ function confidenceRank(c) {
2115
+ if (c === "high") return 3;
2116
+ if (c === "medium") return 2;
2117
+ return 1;
1896
2118
  }
1897
- function resolveFamilyEntry(family, ctx) {
1898
- if (typeof family !== "string" || family.length === 0) {
1899
- throw new FamilyResolutionError(
1900
- String(family),
1901
- "Family tag must be a non-empty string"
1902
- );
2119
+
2120
+ // src/translator.ts
2121
+ var TRANSLATOR_FLOOR = ARCHETYPE_FLOOR_DEFAULT;
2122
+ var RULE_SEQUENTIAL_TOOL_CLIFF = "sequential-tool-cliff-below-floor";
2123
+ var RULE_NARRATION_DRIFT_ANTHROPIC = "narration-drift-anthropic";
2124
+ var RULE_NARRATION_THINKING_LEAK_DEEPSEEK = "narration-thinking-leak-deepseek";
2125
+ var SEQUENTIAL_TOOL_PREAMBLE = "IMPORTANT: Use one tool call per response. Wait for the tool result before deciding the next tool. Do NOT batch tool calls in parallel.";
2126
+ var NARRATION_DRIFT_ANTHROPIC_PREAMBLE = "Output ONLY the requested content. Do not narrate your thought process. Each line \u2264 12 words.";
2127
+ var NARRATION_THINKING_LEAK_DEEPSEEK_PREAMBLE = "Reasoning is internal. Output ONLY the requested content; do not emit <thinking> blocks or internal monologue as user-facing text.";
2128
+ function matchRule(kind, profile, archetype) {
2129
+ if (kind === "tool_call_contract") {
2130
+ if (!profile.archetypePerf) return null;
2131
+ const archetypeScore = profile.archetypePerf[archetype];
2132
+ if (typeof archetypeScore !== "number" || archetypeScore >= TRANSLATOR_FLOOR) {
2133
+ return null;
2134
+ }
2135
+ return {
2136
+ id: RULE_SEQUENTIAL_TOOL_CLIFF,
2137
+ preamble: SEQUENTIAL_TOOL_PREAMBLE,
2138
+ wireOverrides: { parallelToolCalls: false }
2139
+ };
1903
2140
  }
1904
- const registry = loadModelsFromBrain();
1905
- const candidates = selectCandidates(
1906
- registry,
1907
- family,
1908
- ctx.archetype,
1909
- ctx.appId
1910
- );
1911
- if (candidates.length === 0) {
1912
- let anyInFamily = false;
1913
- for (const profile of registry.values()) {
1914
- if (familyOf(profile) === family) {
1915
- anyInFamily = true;
1916
- break;
1917
- }
2141
+ if (kind === "narration_contract") {
2142
+ if (profile.provider === "anthropic") {
2143
+ return {
2144
+ id: RULE_NARRATION_DRIFT_ANTHROPIC,
2145
+ preamble: NARRATION_DRIFT_ANTHROPIC_PREAMBLE
2146
+ };
1918
2147
  }
1919
- const cause = anyInFamily ? `Family has registered models but none are current+active${ctx.archetype ? ` at archetype-perf >= ${ARCHETYPE_FLOOR_DEFAULT} for "${ctx.archetype}"` : ""}${ctx.appId ? ` and not excluded for app "${ctx.appId}"` : ""}` : `No models in brain registry carry family="${family}". Check the taxonomy table in family-resolution.ts or migration 024`;
1920
- throw new FamilyResolutionError(family, cause);
2148
+ if (profile.provider === "deepseek") {
2149
+ return {
2150
+ id: RULE_NARRATION_THINKING_LEAK_DEEPSEEK,
2151
+ preamble: NARRATION_THINKING_LEAK_DEEPSEEK_PREAMBLE
2152
+ };
2153
+ }
2154
+ return null;
1921
2155
  }
1922
- const sorted = sortCandidates(candidates, ctx.archetype);
1923
- const winner = sorted[0];
1924
- if (!winner) {
1925
- throw new FamilyResolutionError(
1926
- family,
1927
- "Candidates non-empty but sort returned undefined \u2014 internal invariant violation"
1928
- );
2156
+ return null;
2157
+ }
2158
+ function applySectionRewrites(args) {
2159
+ const { ir, profile, archetype } = args;
2160
+ if (!Array.isArray(ir.sections) || ir.sections.length === 0) {
2161
+ return { rewrittenIR: ir, rewrites: [] };
1929
2162
  }
1930
- return winner.id;
2163
+ const rewrites = [];
2164
+ const newSections = ir.sections.map((section) => {
2165
+ if (!section.kind || section.kind === "arbitrary") return section;
2166
+ const rule = matchRule(section.kind, profile, archetype);
2167
+ if (!rule) return section;
2168
+ const originalText = section.text;
2169
+ const transformedText = `${rule.preamble}
2170
+
2171
+ ${originalText}`;
2172
+ rewrites.push({
2173
+ sectionId: section.id,
2174
+ kind: section.kind,
2175
+ rule: rule.id,
2176
+ originalText,
2177
+ transformedText,
2178
+ ...rule.wireOverrides ? { wireOverrides: rule.wireOverrides } : {}
2179
+ });
2180
+ return { ...section, text: transformedText };
2181
+ });
2182
+ if (rewrites.length === 0) {
2183
+ return { rewrittenIR: ir, rewrites: [] };
2184
+ }
2185
+ const rewrittenIR = { ...ir, sections: newSections };
2186
+ return { rewrittenIR, rewrites };
1931
2187
  }
1932
2188
 
1933
2189
  // src/compile.ts
@@ -1974,6 +2230,9 @@ function compile(ir, opts = {}) {
1974
2230
  const cliffs = passApplyCliffs(workingIR, profile, inputTokens);
1975
2231
  workingIR = cliffs.value.ir;
1976
2232
  accumulatedMutations.push(...cliffs.mutations);
2233
+ const conventions = passApplyConventions(workingIR, profile);
2234
+ workingIR = conventions.value.ir;
2235
+ accumulatedMutations.push(...conventions.mutations);
1977
2236
  const translated = applySectionRewrites({
1978
2237
  ir: workingIR,
1979
2238
  profile,
@@ -2025,7 +2284,15 @@ function compile(ir, opts = {}) {
2025
2284
  toolOrchestration: ir.constraints?.toolOrchestration,
2026
2285
  // alpha.33 — see top-of-block comment.
2027
2286
  historyCacheMarkIndex,
2028
- systemCacheMarkIndex
2287
+ systemCacheMarkIndex,
2288
+ // alpha.43 — cliff-style warnings emitted by passApplyConventions.
2289
+ // Merge convention-pass cliffWarnings with any cliff-guard quality
2290
+ // warnings the cliff pass surfaced (the same shape — informational
2291
+ // text the consumer can route on without changing behavior).
2292
+ cliffWarnings: [
2293
+ ...cliffs.value.loweringHints.qualityWarning ?? [],
2294
+ ...conventions.value.cliffWarnings
2295
+ ]
2029
2296
  };
2030
2297
  if (ir.intent.archetype === "hunt" && ir.constraints?.toolOrchestration === "sequential") {
2031
2298
  accumulatedMutations.push({
@@ -2042,7 +2309,7 @@ function compile(ir, opts = {}) {
2042
2309
  return void 0;
2043
2310
  }
2044
2311
  } : tryGetProfile;
2045
- const advisories = runAdvisor(
2312
+ const rawAdvisories = runAdvisor(
2046
2313
  ir,
2047
2314
  {
2048
2315
  target: profile.id,
@@ -2063,6 +2330,11 @@ function compile(ir, opts = {}) {
2063
2330
  sectionRewritesApplied
2064
2331
  }
2065
2332
  );
2333
+ const advisories = rawAdvisories.map((a) => ({
2334
+ ...a,
2335
+ kgautoRequestId: handle,
2336
+ ownership: deriveOwnership(a.code, a.ownership)
2337
+ }));
2066
2338
  return {
2067
2339
  handle,
2068
2340
  target: profile.id,
@@ -3904,6 +4176,7 @@ export {
3904
4176
  ABSOLUTE_FLOOR,
3905
4177
  ALIASES,
3906
4178
  ALL_ARCHETYPES,
4179
+ ARCHETYPE_FAMILY_FITS,
3907
4180
  ARCHETYPE_FLOOR_DEFAULT,
3908
4181
  CallError,
3909
4182
  DEFAULT_FINDINGS_ENDPOINT,
@@ -3911,10 +4184,12 @@ export {
3911
4184
  FamilyResolutionError,
3912
4185
  INTENT_ARCHETYPES,
3913
4186
  MEASURED_GROUNDING_MIN_N,
4187
+ PRODUCER_OWNED_RULE_CODES,
3914
4188
  PROVIDER_ENV_KEYS,
3915
4189
  RULE_SEQUENTIAL_TOOL_CLIFF,
3916
4190
  TRANSLATOR_FLOOR,
3917
4191
  allProfiles,
4192
+ applyArchetypeConvention,
3918
4193
  applySectionRewrites,
3919
4194
  attachCacheControlToStreamTextInput,
3920
4195
  bucketContext,
@@ -3927,7 +4202,9 @@ export {
3927
4202
  configureBrain,
3928
4203
  countTokens,
3929
4204
  deriveFamilyFromModelId,
4205
+ deriveOwnership,
3930
4206
  execute,
4207
+ findBetterFit,
3931
4208
  getActionableAdvisories,
3932
4209
  getAllStarterChains,
3933
4210
  getAllStarterChainsWithGrounding,
@@ -3962,9 +4239,11 @@ export {
3962
4239
  markPromoteReadyHandled,
3963
4240
  profileToRow,
3964
4241
  profilesByProvider,
4242
+ readBrainReadEnv,
3965
4243
  record,
3966
4244
  recordOutcome,
3967
4245
  resetTokenizer,
4246
+ resolveConventionsForProfile,
3968
4247
  resolvePricingAt,
3969
4248
  resolveProviderKey,
3970
4249
  runAdvisor,