@warmdrift/kgauto-compiler 2.0.0-alpha.83 → 2.0.0-alpha.85

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.js CHANGED
@@ -27,6 +27,8 @@ __export(index_exports, {
27
27
  ALT_STRATEGY_IDS: () => ALT_STRATEGY_IDS,
28
28
  ARCHETYPE_FAMILY_FITS: () => ARCHETYPE_FAMILY_FITS,
29
29
  ARCHETYPE_FLOOR_DEFAULT: () => ARCHETYPE_FLOOR_DEFAULT,
30
+ BLOCKED_MODEL_FAMILY_SIBLING_SERVED_CODE: () => BLOCKED_MODEL_FAMILY_SIBLING_SERVED_CODE,
31
+ BLOCKED_MODEL_NOT_IN_ROSTER_CODE: () => BLOCKED_MODEL_NOT_IN_ROSTER_CODE,
30
32
  BRAIN_READ_ENV_NAMES: () => BRAIN_READ_ENV_NAMES,
31
33
  BURST_SPAN_MS: () => BURST_SPAN_MS,
32
34
  COACH_CFG: () => COACH_CFG,
@@ -168,6 +170,7 @@ __export(index_exports, {
168
170
  resetTokenizer: () => resetTokenizer,
169
171
  resolveConventionsForProfile: () => resolveConventionsForProfile,
170
172
  resolveGoldenCaptureRate: () => resolveGoldenCaptureRate,
173
+ resolveModelAlias: () => resolveModelAlias,
171
174
  resolvePricingAt: () => resolvePricingAt,
172
175
  resolveProviderKey: () => resolveProviderKey,
173
176
  rowToAdvisory: () => rowToAdvisory,
@@ -2024,6 +2027,9 @@ function _setProfileBrainHook(hook) {
2024
2027
  function canonicalId(id) {
2025
2028
  return brainHook.resolveAlias?.(id) ?? ALIASES[id] ?? id;
2026
2029
  }
2030
+ function resolveModelAlias(id) {
2031
+ return canonicalId(id);
2032
+ }
2027
2033
  var PROFILE_INDEX = new Map(
2028
2034
  PROFILES_RAW.map((p) => [p.id, p])
2029
2035
  );
@@ -2523,6 +2529,17 @@ function resolveFamilyEntry(family, ctx) {
2523
2529
  return winner.id;
2524
2530
  }
2525
2531
 
2532
+ // src/policy-match.ts
2533
+ function canonicalPolicySet(ids) {
2534
+ const set = /* @__PURE__ */ new Set();
2535
+ for (const id of ids ?? []) set.add(resolveModelAlias(id));
2536
+ return set;
2537
+ }
2538
+ function policySetHas(set, modelId) {
2539
+ if (set.size === 0) return false;
2540
+ return set.has(resolveModelAlias(modelId));
2541
+ }
2542
+
2526
2543
  // src/tokenizer.ts
2527
2544
  var tokenizerImpl = defaultCharBasedCounter;
2528
2545
  function defaultCharBasedCounter(text) {
@@ -2918,8 +2935,8 @@ function effectiveConventions(profile) {
2918
2935
  function passScoreTargets(ir, opts) {
2919
2936
  const constraints = ir.constraints ?? {};
2920
2937
  const policy = opts.policy ?? {};
2921
- const blockedSet = new Set(policy.blockedModels ?? []);
2922
- const preferredSet = new Set(policy.preferredModels ?? []);
2938
+ const blockedSet = canonicalPolicySet(policy.blockedModels);
2939
+ const preferredSet = canonicalPolicySet(policy.preferredModels);
2923
2940
  const scores = [];
2924
2941
  const policyMutations = [];
2925
2942
  const rawPromotion = opts.promotion;
@@ -2942,7 +2959,7 @@ function passScoreTargets(ir, opts) {
2942
2959
  continue;
2943
2960
  }
2944
2961
  const reasons = [];
2945
- if (blockedSet.has(modelId)) {
2962
+ if (policySetHas(blockedSet, modelId)) {
2946
2963
  reasons.push(`blocked_by_policy (consumer gated this model \u2014 see CompilePolicy.blockedModels)`);
2947
2964
  }
2948
2965
  if (opts.estimatedInputTokens > profile.maxContextTokens * 0.9) {
@@ -2972,7 +2989,7 @@ function passScoreTargets(ir, opts) {
2972
2989
  const qualityScore = Math.max(0, baseQuality - qualityPenalty);
2973
2990
  const callerOrderBoost = (modelIds.length - modelIds.indexOf(modelId)) * 0.1;
2974
2991
  const costPenalty = estimatedCostUsd * 5;
2975
- const preferredBoost = preferredSet.has(modelId) ? 0.5 : 0;
2992
+ const preferredBoost = policySetHas(preferredSet, modelId) ? 0.5 : 0;
2976
2993
  let latencyPenalty = 0;
2977
2994
  const maxLatencyMs = constraints.maxLatencyMs;
2978
2995
  if (typeof maxLatencyMs === "number" && maxLatencyMs > 0) {
@@ -3029,7 +3046,7 @@ function passScoreTargets(ir, opts) {
3029
3046
  description: `Model ${modelId} excluded \u2014 estimated cost $${estimatedCostUsd.toFixed(4)} exceeds policy ceiling $${policy.maxCostPerCallUsd.toFixed(4)}`
3030
3047
  });
3031
3048
  }
3032
- if (preferredSet.has(modelId) && reasons.length === 0) {
3049
+ if (policySetHas(preferredSet, modelId) && reasons.length === 0) {
3033
3050
  policyMutations.push({
3034
3051
  id: `policy-preferred-${modelId}`,
3035
3052
  source: "compile_policy",
@@ -4071,8 +4088,8 @@ function getDefaultFallbackChain(opts) {
4071
4088
  chain = [...starter];
4072
4089
  }
4073
4090
  if (policy?.blockedModels && policy.blockedModels.length > 0) {
4074
- const blocked = new Set(policy.blockedModels);
4075
- chain = chain.filter((id) => !blocked.has(id));
4091
+ const blocked = canonicalPolicySet(policy.blockedModels);
4092
+ chain = chain.filter((id) => !policySetHas(blocked, id));
4076
4093
  }
4077
4094
  const seen = /* @__PURE__ */ new Set();
4078
4095
  const deduped = [];
@@ -4175,8 +4192,8 @@ function getDefaultFallbackChainWithGrounding(opts) {
4175
4192
  chain = [...starter];
4176
4193
  }
4177
4194
  if (policy?.blockedModels && policy.blockedModels.length > 0) {
4178
- const blocked = new Set(policy.blockedModels);
4179
- chain = chain.filter((e) => !blocked.has(e.id));
4195
+ const blocked = canonicalPolicySet(policy.blockedModels);
4196
+ chain = chain.filter((e) => !policySetHas(blocked, e.id));
4180
4197
  }
4181
4198
  const seen = /* @__PURE__ */ new Set();
4182
4199
  const deduped = [];
@@ -5053,6 +5070,67 @@ function advisorRuleCrossFamilyFit(ctx) {
5053
5070
  ];
5054
5071
  }
5055
5072
 
5073
+ // src/advisor-rules/blocked-model-drift.ts
5074
+ var BLOCKED_MODEL_NOT_IN_ROSTER_CODE = "blocked-model-not-in-roster";
5075
+ var BLOCKED_MODEL_FAMILY_SIBLING_SERVED_CODE = "blocked-model-family-sibling-served";
5076
+ var DOCS_URL = "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories";
5077
+ function familyOf2(modelId, profile) {
5078
+ return profile?.family ?? deriveFamilyFromModelId(modelId);
5079
+ }
5080
+ function isOnTheWayOut(status) {
5081
+ return status === "legacy" || status === "deprecated";
5082
+ }
5083
+ function advisorRuleBlockedModelDrift(ctx) {
5084
+ const blocked = ctx.policy?.blockedModels;
5085
+ if (!blocked || blocked.length === 0) return [];
5086
+ const resolve = ctx.resolveProfile ?? tryGetProfile;
5087
+ const out = [];
5088
+ const entries = [...new Set(blocked)].sort();
5089
+ const selectedProfile = resolve(ctx.selectedModelId);
5090
+ const selectedFamily = familyOf2(ctx.selectedModelId, selectedProfile);
5091
+ const orphans = entries.filter((e) => resolve(e) === void 0);
5092
+ if (orphans.length > 0) {
5093
+ const list = orphans.map((o) => `\`${o}\``).join(", ");
5094
+ const plural = orphans.length === 1 ? "entry" : "entries";
5095
+ const verb = orphans.length === 1 ? "matches" : "match";
5096
+ out.push({
5097
+ level: "warn",
5098
+ code: BLOCKED_MODEL_NOT_IN_ROSTER_CODE,
5099
+ message: `CompilePolicy.blockedModels ${plural} ${list} ${verb} no model in the current roster, so ${orphans.length === 1 ? "it is" : "they are"} inert \u2014 \`blockedModels\` is matched by exact model id, and nothing kgauto can select carries ${orphans.length === 1 ? "that id" : "those ids"}. The block will never fire.`,
5100
+ suggestion: `Check for a typo, or for an id that was retired from the roster since the block was written. This is how a block goes quiet without an error: tt-intel (2026-07-29) carried \`KGAUTO_BLOCKED_MODELS="claude-sonnet-4-6"\` across a roster retarget and the block stopped covering the traffic they believed it covered. Resolve the intended model id against the live roster \u2014 \`getRecommendedPrimary({ family, fallback })\` returns the id the family currently resolves to \u2014 and block that id, or drop the entry if it is no longer needed.`,
5101
+ docsUrl: DOCS_URL
5102
+ });
5103
+ }
5104
+ if (selectedFamily !== null) {
5105
+ const siblings = entries.filter((e) => {
5106
+ if (resolveModelAlias(e) === resolveModelAlias(ctx.selectedModelId)) return false;
5107
+ const p = resolve(e);
5108
+ if (familyOf2(e, p) !== selectedFamily) return false;
5109
+ if (p && selectedProfile && p.provider !== selectedProfile.provider) {
5110
+ return false;
5111
+ }
5112
+ return true;
5113
+ });
5114
+ if (siblings.length > 0) {
5115
+ const list = siblings.map((s) => `\`${s}\``).join(", ");
5116
+ const plural = siblings.length === 1 ? "" : "s";
5117
+ const retargetShaped = siblings.some((s) => {
5118
+ const p = resolve(s);
5119
+ return isOnTheWayOut(p?.status) && selectedProfile?.status === "current";
5120
+ });
5121
+ const retargetNote = retargetShaped ? ` The blocked id${plural} ${siblings.length === 1 ? "is" : "are"} legacy/deprecated while \`${ctx.selectedModelId}\` is current \u2014 that is the signature of a roster lifecycle move rather than a deliberate per-generation block, so this is more likely to be drift than intent.` : "";
5122
+ out.push({
5123
+ level: "warn",
5124
+ code: BLOCKED_MODEL_FAMILY_SIBLING_SERVED_CODE,
5125
+ message: `\`${ctx.selectedModelId}\` was selected for this call. It is in the same family (\`${selectedFamily}\`) as blocked entr${siblings.length === 1 ? "y" : "ies"} ${list}, but its exact id differs \u2014 and \`CompilePolicy.blockedModels\` matches by exact id, so the block does not cover it.${retargetNote}`,
5126
+ suggestion: `Two readings, and kgauto cannot tell them apart: (1) intentional \u2014 you meant to gate that specific id and \`${ctx.selectedModelId}\` is fine, in which case nothing needs doing and you can filter this code; (2) drift \u2014 you meant to gate the family, and a roster change moved traffic to a sibling your block never named. This happened to tt-intel: when the \`claude-sonnet\` family primary retargeted from \`claude-sonnet-4-6\` to \`claude-sonnet-5\`, their literal-id gate stopped matching the family's routed traffic (their fix was local family resolution). If you meant the family, add \`${ctx.selectedModelId}\` to \`blockedModels\` \u2014 kgauto deliberately does NOT widen exact-id blocks into family globs, because that would silently change what every existing block covers.`,
5127
+ docsUrl: DOCS_URL
5128
+ });
5129
+ }
5130
+ }
5131
+ return out;
5132
+ }
5133
+
5056
5134
  // src/advisor.ts
5057
5135
  var QUALITY_FLOOR_FOR_RECOMMENDATION = 6;
5058
5136
  var TIER_DOWN_COST_RATIO = 0.5;
@@ -5071,6 +5149,12 @@ function runAdvisor(ir, result, profile, policy, phase2) {
5071
5149
  out.push(...detectToolBloat(ir, result));
5072
5150
  out.push(...detectHistoryUncached(ir, profile));
5073
5151
  out.push(...detectSingleModelArray(ir, policy));
5152
+ out.push(
5153
+ ...advisorRuleBlockedModelDrift({
5154
+ policy,
5155
+ selectedModelId: profile.id
5156
+ })
5157
+ );
5074
5158
  if (policy?.posture !== "locked") {
5075
5159
  out.push(...detectCostMismatchedArchetype(ir, profile, phase2));
5076
5160
  out.push(...detectModelStaleEvidence(ir, profile));
@@ -5182,18 +5266,27 @@ function detectHistoryUncached(ir, profile) {
5182
5266
  function detectSingleModelArray(ir, policy) {
5183
5267
  if (ir.models.length !== 1) return [];
5184
5268
  if (policy?.posture === "locked") return [];
5185
- const only = ir.models[0];
5269
+ const entry = ir.models[0];
5270
+ const only = typeof entry === "string" ? entry : `family:${entry.family}`;
5271
+ const blocked = canonicalPolicySet(policy?.blockedModels);
5272
+ let alternatives = [];
5273
+ try {
5274
+ alternatives = getDefaultFallbackChain({
5275
+ archetype: ir.intent.archetype,
5276
+ primary: only,
5277
+ posture: "preferred",
5278
+ policy
5279
+ }).filter((id) => resolveModelAlias(id) !== resolveModelAlias(only)).filter((id) => !policySetHas(blocked, id)).filter((id) => getModelCompatibility(id, { archetype: ir.intent.archetype }).status !== "reject");
5280
+ } catch {
5281
+ }
5282
+ const hasAlternative = alternatives.length > 0;
5283
+ const remedy = "Widen the chain AND pin your primary: `compile({ ...ir, models: getDefaultFallbackChain({ archetype: ir.intent.archetype, primary: '" + only + "', posture: 'preferred' }) }, { policy: { preferredModels: ['" + only + "'] } })`. The chain is a CANDIDATE SET \u2014 compile() scores it and does not honour its order, so without the `preferredModels` pin the extra entries can retarget your primary (and with it your cost profile). With the pin, `" + only + "` stays primary and the added entries serve only as the safety net. Note `posture` has no effect once `primary` is passed. If you gate models by spend, re-verify `policy.blockedModels` against the widened set before shipping. If single-model is intentional (compliance/brand promise), set `policy.posture = 'locked'` to silence this rule.";
5186
5284
  return [
5187
5285
  {
5188
- level: "warn",
5286
+ level: hasAlternative ? "critical" : "warn",
5189
5287
  code: "single-model-array",
5190
- 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.`,
5191
- // alpha.80: `posture: 'preferred'` keeps the consumer's current model at
5192
- // position 0 and is therefore cost-neutral; `'open'` re-picks the
5193
- // primary and is NOT. This rule is about reliability, not cost, so both
5194
- // stay on offer — but the cost consequence of the second is now stated,
5195
- // since a reliability fix should not silently become a repricing.
5196
- suggestion: "Use `getDefaultFallbackChain({ archetype: ir.intent.archetype, primary: '" + only + "', posture: 'preferred' })` for a user-anchored chain \u2014 this keeps `" + only + "` as your primary and only adds fallbacks, so your cost profile is unchanged. `getDefaultFallbackChain({ archetype, posture: 'open' })` instead lets the library pick the PRIMARY, ordered by archetype performance rather than cost \u2014 check what it returns before adopting it. If single-model is intentional (compliance/brand promise), set `policy.posture = 'locked'` to silence this rule.",
5288
+ message: hasAlternative ? `\`ir.models\` has length 1 (only "${only}") for archetype "${ir.intent.archetype}" 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 \u2014 and a compatible, non-blocked alternative exists in the roster today (${alternatives[0]}), so this is critical: the missing safety net is adoptable now.` : `\`ir.models\` has length 1 (only "${only}") for archetype "${ir.intent.archetype}" 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. No compatible non-blocked alternative is visible in the roster for this archetype, so this stays a warning.`,
5289
+ suggestion: remedy,
5197
5290
  docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#single-model-array"
5198
5291
  }
5199
5292
  ];
@@ -5747,6 +5840,15 @@ function compile(ir, opts = {}) {
5747
5840
  sectionRewritesApplied
5748
5841
  }
5749
5842
  );
5843
+ if (ir["policy"] !== void 0 && opts.policy === void 0) {
5844
+ rawAdvisories.push({
5845
+ level: "critical",
5846
+ code: "policy-in-ir-ignored",
5847
+ message: "The IR passed to compile() carries a `policy` field. `policy` belongs in the SECOND argument \u2014 `compile(ir, { policy })` \u2014 and inside the IR it is an unknown field that is completely ignored. If that policy names blockedModels, no block is being enforced on this call.",
5848
+ suggestion: "Move it: `compile(ir, { policy: { ... } })`. If you also configure policy correctly elsewhere on this path, remove the IR copy so the next reader is not misled.",
5849
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#public-api"
5850
+ });
5851
+ }
5750
5852
  const advisories = rawAdvisories.map((a) => ({
5751
5853
  ...a,
5752
5854
  kgautoRequestId: handle,
@@ -5865,7 +5967,7 @@ function validateFinalFit(ir, profile, tokens) {
5865
5967
  }
5866
5968
 
5867
5969
  // src/version.ts
5868
- var LIBRARY_VERSION = "2.0.0-alpha.83";
5970
+ var LIBRARY_VERSION = "2.0.0-alpha.85";
5869
5971
 
5870
5972
  // src/pricing-brain.ts
5871
5973
  function isPricingRow(x) {
@@ -7820,11 +7922,11 @@ async function call(ir, opts = {}) {
7820
7922
  }
7821
7923
  let policyBlockedFiltered;
7822
7924
  if (opts.policy?.blockedModels && opts.policy.blockedModels.length > 0) {
7823
- const blocked = new Set(opts.policy.blockedModels);
7925
+ const blocked = canonicalPolicySet(opts.policy.blockedModels);
7824
7926
  const filtered = [];
7825
7927
  const dropped = [];
7826
7928
  for (const t of targetsToTry) {
7827
- if (blocked.has(t)) {
7929
+ if (policySetHas(blocked, t)) {
7828
7930
  dropped.push(t);
7829
7931
  } else {
7830
7932
  filtered.push(t);
@@ -7864,10 +7966,18 @@ async function call(ir, opts = {}) {
7864
7966
  const failedProviders = /* @__PURE__ */ new Map();
7865
7967
  const sameModelRetryEnabled = opts.sameModelRetry ?? isSameModelRetryEnabledFromEnv();
7866
7968
  let retriedSameModel = false;
7969
+ const pushAttempt = (attempt) => {
7970
+ attempts.push(attempt);
7971
+ if (attempt.status === "success") return;
7972
+ try {
7973
+ opts.onFailedAttempt?.(attempt);
7974
+ } catch {
7975
+ }
7976
+ };
7867
7977
  for (let i = 0; i < targetsToTry.length; i++) {
7868
7978
  const targetModel = targetsToTry[i];
7869
7979
  if (opts.abortSignal?.aborted) {
7870
- attempts.push({
7980
+ pushAttempt({
7871
7981
  model: targetModel,
7872
7982
  status: "terminal",
7873
7983
  errorCode: "aborted",
@@ -7878,7 +7988,7 @@ async function call(ir, opts = {}) {
7878
7988
  const targetProfile = tryGetProfile(targetModel);
7879
7989
  const providerFailReason = targetProfile ? failedProviders.get(targetProfile.provider) : void 0;
7880
7990
  if (targetProfile && providerFailReason && !opts.noFallback) {
7881
- attempts.push({
7991
+ pushAttempt({
7882
7992
  model: targetModel,
7883
7993
  status: "terminal",
7884
7994
  errorCode: `${providerFailReason}_inferred`,
@@ -7897,7 +8007,7 @@ async function call(ir, opts = {}) {
7897
8007
  opts
7898
8008
  );
7899
8009
  } catch (err) {
7900
- attempts.push({
8010
+ pushAttempt({
7901
8011
  model: targetModel,
7902
8012
  status: "terminal",
7903
8013
  errorCode: "compile_error",
@@ -7935,7 +8045,7 @@ async function call(ir, opts = {}) {
7935
8045
  }
7936
8046
  if (!validated.ok && isStructuredContractViolation(validated.errorCode) && sameModelRetryEnabled && !retriedSameModel && !retrySuppressionNote) {
7937
8047
  retriedSameModel = true;
7938
- attempts.push({
8048
+ pushAttempt({
7939
8049
  model: targetModel,
7940
8050
  status: validated.errorType,
7941
8051
  errorCode: validated.errorCode,
@@ -7950,7 +8060,7 @@ async function call(ir, opts = {}) {
7950
8060
  servedByRetry = true;
7951
8061
  }
7952
8062
  if (validated.ok) {
7953
- attempts.push({
8063
+ pushAttempt({
7954
8064
  model: targetModel,
7955
8065
  status: "success",
7956
8066
  ...servedByRetry ? { sameModelRetry: true } : {}
@@ -8068,7 +8178,7 @@ async function call(ir, opts = {}) {
8068
8178
  advisories: activeCompile.advisories
8069
8179
  };
8070
8180
  }
8071
- attempts.push({
8181
+ pushAttempt({
8072
8182
  model: targetModel,
8073
8183
  status: validated.errorType,
8074
8184
  errorCode: validated.errorCode,
@@ -10588,6 +10698,8 @@ function compile2(ir, opts) {
10588
10698
  ALT_STRATEGY_IDS,
10589
10699
  ARCHETYPE_FAMILY_FITS,
10590
10700
  ARCHETYPE_FLOOR_DEFAULT,
10701
+ BLOCKED_MODEL_FAMILY_SIBLING_SERVED_CODE,
10702
+ BLOCKED_MODEL_NOT_IN_ROSTER_CODE,
10591
10703
  BRAIN_READ_ENV_NAMES,
10592
10704
  BURST_SPAN_MS,
10593
10705
  COACH_CFG,
@@ -10729,6 +10841,7 @@ function compile2(ir, opts) {
10729
10841
  resetTokenizer,
10730
10842
  resolveConventionsForProfile,
10731
10843
  resolveGoldenCaptureRate,
10844
+ resolveModelAlias,
10732
10845
  resolvePricingAt,
10733
10846
  resolveProviderKey,
10734
10847
  rowToAdvisory,
package/dist/index.mjs CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  LIBRARY_VERSION,
20
20
  createKeyHealthRoute,
21
21
  keyFingerprint
22
- } from "./chunk-QEFIAAE7.mjs";
22
+ } from "./chunk-WXN7FNXP.mjs";
23
23
  import {
24
24
  ABSOLUTE_FLOOR,
25
25
  ARCHETYPE_FLOOR_DEFAULT,
@@ -27,6 +27,7 @@ import {
27
27
  COST_RANKING_REFERENCE_SHAPE,
28
28
  MEASURED_GROUNDING_MIN_N,
29
29
  PROVIDER_ENV_KEYS,
30
+ canonicalPolicySet,
30
31
  chainProviderSpread,
31
32
  configureBrainQuery,
32
33
  createBrainQueryCache,
@@ -52,9 +53,10 @@ import {
52
53
  loadArchetypePerfFromBrain,
53
54
  loadArchetypePerfNFromBrain,
54
55
  loadChainsFromBrain,
56
+ policySetHas,
55
57
  readBrainReadEnv,
56
58
  resolveProviderKey
57
- } from "./chunk-XPD4I3Q5.mjs";
59
+ } from "./chunk-LO2JXTGG.mjs";
58
60
  import {
59
61
  ALIASES,
60
62
  LATENCY_TIER_MS,
@@ -64,8 +66,9 @@ import {
64
66
  getProfile,
65
67
  latencyTierOf,
66
68
  profilesByProvider,
69
+ resolveModelAlias,
67
70
  tryGetProfile
68
- } from "./chunk-VVRDFE6T.mjs";
71
+ } from "./chunk-FD3NFXDC.mjs";
69
72
  import {
70
73
  emitAdvisoryFired,
71
74
  emitCompileDone,
@@ -767,8 +770,8 @@ function effectiveConventions(profile) {
767
770
  function passScoreTargets(ir, opts) {
768
771
  const constraints = ir.constraints ?? {};
769
772
  const policy = opts.policy ?? {};
770
- const blockedSet = new Set(policy.blockedModels ?? []);
771
- const preferredSet = new Set(policy.preferredModels ?? []);
773
+ const blockedSet = canonicalPolicySet(policy.blockedModels);
774
+ const preferredSet = canonicalPolicySet(policy.preferredModels);
772
775
  const scores = [];
773
776
  const policyMutations = [];
774
777
  const rawPromotion = opts.promotion;
@@ -791,7 +794,7 @@ function passScoreTargets(ir, opts) {
791
794
  continue;
792
795
  }
793
796
  const reasons = [];
794
- if (blockedSet.has(modelId)) {
797
+ if (policySetHas(blockedSet, modelId)) {
795
798
  reasons.push(`blocked_by_policy (consumer gated this model \u2014 see CompilePolicy.blockedModels)`);
796
799
  }
797
800
  if (opts.estimatedInputTokens > profile.maxContextTokens * 0.9) {
@@ -821,7 +824,7 @@ function passScoreTargets(ir, opts) {
821
824
  const qualityScore = Math.max(0, baseQuality - qualityPenalty);
822
825
  const callerOrderBoost = (modelIds.length - modelIds.indexOf(modelId)) * 0.1;
823
826
  const costPenalty = estimatedCostUsd * 5;
824
- const preferredBoost = preferredSet.has(modelId) ? 0.5 : 0;
827
+ const preferredBoost = policySetHas(preferredSet, modelId) ? 0.5 : 0;
825
828
  let latencyPenalty = 0;
826
829
  const maxLatencyMs = constraints.maxLatencyMs;
827
830
  if (typeof maxLatencyMs === "number" && maxLatencyMs > 0) {
@@ -878,7 +881,7 @@ function passScoreTargets(ir, opts) {
878
881
  description: `Model ${modelId} excluded \u2014 estimated cost $${estimatedCostUsd.toFixed(4)} exceeds policy ceiling $${policy.maxCostPerCallUsd.toFixed(4)}`
879
882
  });
880
883
  }
881
- if (preferredSet.has(modelId) && reasons.length === 0) {
884
+ if (policySetHas(preferredSet, modelId) && reasons.length === 0) {
882
885
  policyMutations.push({
883
886
  id: `policy-preferred-${modelId}`,
884
887
  source: "compile_policy",
@@ -2361,6 +2364,67 @@ function advisorRuleCrossFamilyFit(ctx) {
2361
2364
  ];
2362
2365
  }
2363
2366
 
2367
+ // src/advisor-rules/blocked-model-drift.ts
2368
+ var BLOCKED_MODEL_NOT_IN_ROSTER_CODE = "blocked-model-not-in-roster";
2369
+ var BLOCKED_MODEL_FAMILY_SIBLING_SERVED_CODE = "blocked-model-family-sibling-served";
2370
+ var DOCS_URL = "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#best-practice-advisories";
2371
+ function familyOf2(modelId, profile) {
2372
+ return profile?.family ?? deriveFamilyFromModelId(modelId);
2373
+ }
2374
+ function isOnTheWayOut(status) {
2375
+ return status === "legacy" || status === "deprecated";
2376
+ }
2377
+ function advisorRuleBlockedModelDrift(ctx) {
2378
+ const blocked = ctx.policy?.blockedModels;
2379
+ if (!blocked || blocked.length === 0) return [];
2380
+ const resolve = ctx.resolveProfile ?? tryGetProfile;
2381
+ const out = [];
2382
+ const entries = [...new Set(blocked)].sort();
2383
+ const selectedProfile = resolve(ctx.selectedModelId);
2384
+ const selectedFamily = familyOf2(ctx.selectedModelId, selectedProfile);
2385
+ const orphans = entries.filter((e) => resolve(e) === void 0);
2386
+ if (orphans.length > 0) {
2387
+ const list = orphans.map((o) => `\`${o}\``).join(", ");
2388
+ const plural = orphans.length === 1 ? "entry" : "entries";
2389
+ const verb = orphans.length === 1 ? "matches" : "match";
2390
+ out.push({
2391
+ level: "warn",
2392
+ code: BLOCKED_MODEL_NOT_IN_ROSTER_CODE,
2393
+ message: `CompilePolicy.blockedModels ${plural} ${list} ${verb} no model in the current roster, so ${orphans.length === 1 ? "it is" : "they are"} inert \u2014 \`blockedModels\` is matched by exact model id, and nothing kgauto can select carries ${orphans.length === 1 ? "that id" : "those ids"}. The block will never fire.`,
2394
+ suggestion: `Check for a typo, or for an id that was retired from the roster since the block was written. This is how a block goes quiet without an error: tt-intel (2026-07-29) carried \`KGAUTO_BLOCKED_MODELS="claude-sonnet-4-6"\` across a roster retarget and the block stopped covering the traffic they believed it covered. Resolve the intended model id against the live roster \u2014 \`getRecommendedPrimary({ family, fallback })\` returns the id the family currently resolves to \u2014 and block that id, or drop the entry if it is no longer needed.`,
2395
+ docsUrl: DOCS_URL
2396
+ });
2397
+ }
2398
+ if (selectedFamily !== null) {
2399
+ const siblings = entries.filter((e) => {
2400
+ if (resolveModelAlias(e) === resolveModelAlias(ctx.selectedModelId)) return false;
2401
+ const p = resolve(e);
2402
+ if (familyOf2(e, p) !== selectedFamily) return false;
2403
+ if (p && selectedProfile && p.provider !== selectedProfile.provider) {
2404
+ return false;
2405
+ }
2406
+ return true;
2407
+ });
2408
+ if (siblings.length > 0) {
2409
+ const list = siblings.map((s) => `\`${s}\``).join(", ");
2410
+ const plural = siblings.length === 1 ? "" : "s";
2411
+ const retargetShaped = siblings.some((s) => {
2412
+ const p = resolve(s);
2413
+ return isOnTheWayOut(p?.status) && selectedProfile?.status === "current";
2414
+ });
2415
+ const retargetNote = retargetShaped ? ` The blocked id${plural} ${siblings.length === 1 ? "is" : "are"} legacy/deprecated while \`${ctx.selectedModelId}\` is current \u2014 that is the signature of a roster lifecycle move rather than a deliberate per-generation block, so this is more likely to be drift than intent.` : "";
2416
+ out.push({
2417
+ level: "warn",
2418
+ code: BLOCKED_MODEL_FAMILY_SIBLING_SERVED_CODE,
2419
+ message: `\`${ctx.selectedModelId}\` was selected for this call. It is in the same family (\`${selectedFamily}\`) as blocked entr${siblings.length === 1 ? "y" : "ies"} ${list}, but its exact id differs \u2014 and \`CompilePolicy.blockedModels\` matches by exact id, so the block does not cover it.${retargetNote}`,
2420
+ suggestion: `Two readings, and kgauto cannot tell them apart: (1) intentional \u2014 you meant to gate that specific id and \`${ctx.selectedModelId}\` is fine, in which case nothing needs doing and you can filter this code; (2) drift \u2014 you meant to gate the family, and a roster change moved traffic to a sibling your block never named. This happened to tt-intel: when the \`claude-sonnet\` family primary retargeted from \`claude-sonnet-4-6\` to \`claude-sonnet-5\`, their literal-id gate stopped matching the family's routed traffic (their fix was local family resolution). If you meant the family, add \`${ctx.selectedModelId}\` to \`blockedModels\` \u2014 kgauto deliberately does NOT widen exact-id blocks into family globs, because that would silently change what every existing block covers.`,
2421
+ docsUrl: DOCS_URL
2422
+ });
2423
+ }
2424
+ }
2425
+ return out;
2426
+ }
2427
+
2364
2428
  // src/advisor.ts
2365
2429
  var QUALITY_FLOOR_FOR_RECOMMENDATION = 6;
2366
2430
  var TIER_DOWN_COST_RATIO = 0.5;
@@ -2379,6 +2443,12 @@ function runAdvisor(ir, result, profile, policy, phase2) {
2379
2443
  out.push(...detectToolBloat(ir, result));
2380
2444
  out.push(...detectHistoryUncached(ir, profile));
2381
2445
  out.push(...detectSingleModelArray(ir, policy));
2446
+ out.push(
2447
+ ...advisorRuleBlockedModelDrift({
2448
+ policy,
2449
+ selectedModelId: profile.id
2450
+ })
2451
+ );
2382
2452
  if (policy?.posture !== "locked") {
2383
2453
  out.push(...detectCostMismatchedArchetype(ir, profile, phase2));
2384
2454
  out.push(...detectModelStaleEvidence(ir, profile));
@@ -2490,18 +2560,27 @@ function detectHistoryUncached(ir, profile) {
2490
2560
  function detectSingleModelArray(ir, policy) {
2491
2561
  if (ir.models.length !== 1) return [];
2492
2562
  if (policy?.posture === "locked") return [];
2493
- const only = ir.models[0];
2563
+ const entry = ir.models[0];
2564
+ const only = typeof entry === "string" ? entry : `family:${entry.family}`;
2565
+ const blocked = canonicalPolicySet(policy?.blockedModels);
2566
+ let alternatives = [];
2567
+ try {
2568
+ alternatives = getDefaultFallbackChain({
2569
+ archetype: ir.intent.archetype,
2570
+ primary: only,
2571
+ posture: "preferred",
2572
+ policy
2573
+ }).filter((id) => resolveModelAlias(id) !== resolveModelAlias(only)).filter((id) => !policySetHas(blocked, id)).filter((id) => getModelCompatibility(id, { archetype: ir.intent.archetype }).status !== "reject");
2574
+ } catch {
2575
+ }
2576
+ const hasAlternative = alternatives.length > 0;
2577
+ const remedy = "Widen the chain AND pin your primary: `compile({ ...ir, models: getDefaultFallbackChain({ archetype: ir.intent.archetype, primary: '" + only + "', posture: 'preferred' }) }, { policy: { preferredModels: ['" + only + "'] } })`. The chain is a CANDIDATE SET \u2014 compile() scores it and does not honour its order, so without the `preferredModels` pin the extra entries can retarget your primary (and with it your cost profile). With the pin, `" + only + "` stays primary and the added entries serve only as the safety net. Note `posture` has no effect once `primary` is passed. If you gate models by spend, re-verify `policy.blockedModels` against the widened set before shipping. If single-model is intentional (compliance/brand promise), set `policy.posture = 'locked'` to silence this rule.";
2494
2578
  return [
2495
2579
  {
2496
- level: "warn",
2580
+ level: hasAlternative ? "critical" : "warn",
2497
2581
  code: "single-model-array",
2498
- 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.`,
2499
- // alpha.80: `posture: 'preferred'` keeps the consumer's current model at
2500
- // position 0 and is therefore cost-neutral; `'open'` re-picks the
2501
- // primary and is NOT. This rule is about reliability, not cost, so both
2502
- // stay on offer — but the cost consequence of the second is now stated,
2503
- // since a reliability fix should not silently become a repricing.
2504
- suggestion: "Use `getDefaultFallbackChain({ archetype: ir.intent.archetype, primary: '" + only + "', posture: 'preferred' })` for a user-anchored chain \u2014 this keeps `" + only + "` as your primary and only adds fallbacks, so your cost profile is unchanged. `getDefaultFallbackChain({ archetype, posture: 'open' })` instead lets the library pick the PRIMARY, ordered by archetype performance rather than cost \u2014 check what it returns before adopting it. If single-model is intentional (compliance/brand promise), set `policy.posture = 'locked'` to silence this rule.",
2582
+ message: hasAlternative ? `\`ir.models\` has length 1 (only "${only}") for archetype "${ir.intent.archetype}" 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 \u2014 and a compatible, non-blocked alternative exists in the roster today (${alternatives[0]}), so this is critical: the missing safety net is adoptable now.` : `\`ir.models\` has length 1 (only "${only}") for archetype "${ir.intent.archetype}" 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. No compatible non-blocked alternative is visible in the roster for this archetype, so this stays a warning.`,
2583
+ suggestion: remedy,
2505
2584
  docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#single-model-array"
2506
2585
  }
2507
2586
  ];
@@ -3055,6 +3134,15 @@ function compile(ir, opts = {}) {
3055
3134
  sectionRewritesApplied
3056
3135
  }
3057
3136
  );
3137
+ if (ir["policy"] !== void 0 && opts.policy === void 0) {
3138
+ rawAdvisories.push({
3139
+ level: "critical",
3140
+ code: "policy-in-ir-ignored",
3141
+ message: "The IR passed to compile() carries a `policy` field. `policy` belongs in the SECOND argument \u2014 `compile(ir, { policy })` \u2014 and inside the IR it is an unknown field that is completely ignored. If that policy names blockedModels, no block is being enforced on this call.",
3142
+ suggestion: "Move it: `compile(ir, { policy: { ... } })`. If you also configure policy correctly elsewhere on this path, remove the IR copy so the next reader is not misled.",
3143
+ docsUrl: "https://github.com/stue/command-center/blob/main/interfaces/kgauto.md#public-api"
3144
+ });
3145
+ }
3058
3146
  const advisories = rawAdvisories.map((a) => ({
3059
3147
  ...a,
3060
3148
  kgautoRequestId: handle,
@@ -4824,11 +4912,11 @@ async function call(ir, opts = {}) {
4824
4912
  }
4825
4913
  let policyBlockedFiltered;
4826
4914
  if (opts.policy?.blockedModels && opts.policy.blockedModels.length > 0) {
4827
- const blocked = new Set(opts.policy.blockedModels);
4915
+ const blocked = canonicalPolicySet(opts.policy.blockedModels);
4828
4916
  const filtered = [];
4829
4917
  const dropped = [];
4830
4918
  for (const t of targetsToTry) {
4831
- if (blocked.has(t)) {
4919
+ if (policySetHas(blocked, t)) {
4832
4920
  dropped.push(t);
4833
4921
  } else {
4834
4922
  filtered.push(t);
@@ -4868,10 +4956,18 @@ async function call(ir, opts = {}) {
4868
4956
  const failedProviders = /* @__PURE__ */ new Map();
4869
4957
  const sameModelRetryEnabled = opts.sameModelRetry ?? isSameModelRetryEnabledFromEnv();
4870
4958
  let retriedSameModel = false;
4959
+ const pushAttempt = (attempt) => {
4960
+ attempts.push(attempt);
4961
+ if (attempt.status === "success") return;
4962
+ try {
4963
+ opts.onFailedAttempt?.(attempt);
4964
+ } catch {
4965
+ }
4966
+ };
4871
4967
  for (let i = 0; i < targetsToTry.length; i++) {
4872
4968
  const targetModel = targetsToTry[i];
4873
4969
  if (opts.abortSignal?.aborted) {
4874
- attempts.push({
4970
+ pushAttempt({
4875
4971
  model: targetModel,
4876
4972
  status: "terminal",
4877
4973
  errorCode: "aborted",
@@ -4882,7 +4978,7 @@ async function call(ir, opts = {}) {
4882
4978
  const targetProfile = tryGetProfile(targetModel);
4883
4979
  const providerFailReason = targetProfile ? failedProviders.get(targetProfile.provider) : void 0;
4884
4980
  if (targetProfile && providerFailReason && !opts.noFallback) {
4885
- attempts.push({
4981
+ pushAttempt({
4886
4982
  model: targetModel,
4887
4983
  status: "terminal",
4888
4984
  errorCode: `${providerFailReason}_inferred`,
@@ -4901,7 +4997,7 @@ async function call(ir, opts = {}) {
4901
4997
  opts
4902
4998
  );
4903
4999
  } catch (err) {
4904
- attempts.push({
5000
+ pushAttempt({
4905
5001
  model: targetModel,
4906
5002
  status: "terminal",
4907
5003
  errorCode: "compile_error",
@@ -4939,7 +5035,7 @@ async function call(ir, opts = {}) {
4939
5035
  }
4940
5036
  if (!validated.ok && isStructuredContractViolation(validated.errorCode) && sameModelRetryEnabled && !retriedSameModel && !retrySuppressionNote) {
4941
5037
  retriedSameModel = true;
4942
- attempts.push({
5038
+ pushAttempt({
4943
5039
  model: targetModel,
4944
5040
  status: validated.errorType,
4945
5041
  errorCode: validated.errorCode,
@@ -4954,7 +5050,7 @@ async function call(ir, opts = {}) {
4954
5050
  servedByRetry = true;
4955
5051
  }
4956
5052
  if (validated.ok) {
4957
- attempts.push({
5053
+ pushAttempt({
4958
5054
  model: targetModel,
4959
5055
  status: "success",
4960
5056
  ...servedByRetry ? { sameModelRetry: true } : {}
@@ -5072,7 +5168,7 @@ async function call(ir, opts = {}) {
5072
5168
  advisories: activeCompile.advisories
5073
5169
  };
5074
5170
  }
5075
- attempts.push({
5171
+ pushAttempt({
5076
5172
  model: targetModel,
5077
5173
  status: validated.errorType,
5078
5174
  errorCode: validated.errorCode,
@@ -7216,6 +7312,8 @@ export {
7216
7312
  ALT_STRATEGY_IDS,
7217
7313
  ARCHETYPE_FAMILY_FITS,
7218
7314
  ARCHETYPE_FLOOR_DEFAULT,
7315
+ BLOCKED_MODEL_FAMILY_SIBLING_SERVED_CODE,
7316
+ BLOCKED_MODEL_NOT_IN_ROSTER_CODE,
7219
7317
  BRAIN_READ_ENV_NAMES,
7220
7318
  BURST_SPAN_MS,
7221
7319
  COACH_CFG,
@@ -7357,6 +7455,7 @@ export {
7357
7455
  resetTokenizer,
7358
7456
  resolveConventionsForProfile,
7359
7457
  resolveGoldenCaptureRate,
7458
+ resolveModelAlias,
7360
7459
  resolvePricingAt,
7361
7460
  resolveProviderKey,
7362
7461
  rowToAdvisory,