@warmdrift/kgauto-compiler 2.0.0-alpha.82 → 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.mjs CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  LIBRARY_VERSION,
20
20
  createKeyHealthRoute,
21
21
  keyFingerprint
22
- } from "./chunk-L246YOV7.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,
@@ -5652,6 +5748,13 @@ function combineOrderSwappedVerdicts(run1, run2) {
5652
5748
  if (run1 === run2) return run1;
5653
5749
  return "tied";
5654
5750
  }
5751
+ var ALT_STRATEGY_IDS = [
5752
+ "discipline-gates-v1-alt",
5753
+ "discipline-gates-v1-alt-blind"
5754
+ ];
5755
+ function isAltStrategy(id) {
5756
+ return id !== void 0 && ALT_STRATEGY_IDS.includes(id);
5757
+ }
5655
5758
  function replayRetryDelayMs(errorCode) {
5656
5759
  return errorCode === "rate_limit" ? 65e3 : 2e3;
5657
5760
  }
@@ -5705,7 +5808,34 @@ function altGatesBlockFor(args) {
5705
5808
  ...shapeAltering
5706
5809
  ]);
5707
5810
  }
5811
+ var ALT_BLIND_TOKEN_BUDGET_BREACH = {
5812
+ ceilingTokens: 264,
5813
+ measuredTokensTextWithTools: 285,
5814
+ note: "Blind arm exceeds the stated 264-token ceiling on text+tools (285) and costs ~2.4x the v1-alt arm. Not corrected: compressing it would require editing prose the arm depends on NOT having been edited by this seat. Read a blind-arm LOSS as confounded by token tax; a WIN is unaffected."
5815
+ };
5816
+ var DISCIPLINE_GATES_V1_ALT_BLIND_HEADER = "Before stating any conclusion, run this check:";
5817
+ var ALT_BLIND_BULLETS = {
5818
+ falsify: "- Before committing to an approach, state in one line what evidence would prove it wrong and how you would notice it. If nothing could falsify the approach, you hold a preference, not a plan \u2014 rework it until something could.",
5819
+ deviation: "- When you choose not to follow an applicable instruction, say so explicitly: name the instruction, why you are deviating, and what you are doing instead. Silent deviation is forbidden \u2014 an override is only legitimate when it is visible.",
5820
+ toolPredict: "- When using tools: before each call, state the result you expect. On mismatch, treat the gap as evidence your model of the system is wrong \u2014 revise the model before acting again; never silently retry.",
5821
+ verifyDelegated: "- Verify every delegated or sub-agent result before composing it into an answer: spot-check it against the source or an independent probe. An unverified sub-result is a claim you are repeating, not a fact you know.",
5822
+ markAssumed: '- In the final answer, keep verified claims and assumptions visibly distinct: mark anything unchecked as "assumed" or "unverified". Never let an assumption borrow the confidence of the verified claims beside it.'
5823
+ };
5824
+ function altBlindGatesBlockFor(args) {
5825
+ const parts = [
5826
+ "falsify",
5827
+ "deviation",
5828
+ ...args.hasTools ? ["toolPredict"] : [],
5829
+ "verifyDelegated",
5830
+ ...args.outputMode === "text" ? ["markAssumed"] : []
5831
+ ];
5832
+ return [
5833
+ DISCIPLINE_GATES_V1_ALT_BLIND_HEADER,
5834
+ ...parts.map((p) => ALT_BLIND_BULLETS[p])
5835
+ ].join("\n");
5836
+ }
5708
5837
  var STRATEGY_ALT_SECTION_ID = "__kgauto_strategy_eval_gates_alt__";
5838
+ var STRATEGY_ALT_BLIND_SECTION_ID = "__kgauto_strategy_eval_gates_alt_blind__";
5709
5839
  function withAltDisciplineContract(ir) {
5710
5840
  const outputMode = resolveOutputMode({
5711
5841
  declared: ir.constraints?.outputMode,
@@ -5724,6 +5854,28 @@ function withAltDisciplineContract(ir) {
5724
5854
  ]
5725
5855
  };
5726
5856
  }
5857
+ function withAltBlindDisciplineContract(ir) {
5858
+ const outputMode = resolveOutputMode({
5859
+ declared: ir.constraints?.outputMode,
5860
+ structuredOutput: ir.constraints?.structuredOutput,
5861
+ toolCount: ir.tools?.length ?? 0
5862
+ });
5863
+ const hasTools = (ir.tools?.length ?? 0) > 0;
5864
+ return {
5865
+ ...ir,
5866
+ sections: [
5867
+ ...ir.sections ?? [],
5868
+ {
5869
+ id: STRATEGY_ALT_BLIND_SECTION_ID,
5870
+ text: altBlindGatesBlockFor({ outputMode, hasTools })
5871
+ }
5872
+ ]
5873
+ };
5874
+ }
5875
+ var ALT_SECTION_ID_BY_STRATEGY = {
5876
+ "discipline-gates-v1-alt": STRATEGY_ALT_SECTION_ID,
5877
+ "discipline-gates-v1-alt-blind": STRATEGY_ALT_BLIND_SECTION_ID
5878
+ };
5727
5879
  async function runGoldenEval(opts) {
5728
5880
  const fetchFn = opts.fetchImpl ?? fetch;
5729
5881
  const progress = opts.onProgress ?? (() => {
@@ -5791,7 +5943,9 @@ async function runGoldenEval(opts) {
5791
5943
  }
5792
5944
  const armBIr = (ir) => {
5793
5945
  if (axis !== "strategy") return ir;
5794
- return strategyId === "discipline-gates-v1-alt" ? withAltDisciplineContract(ir) : withDisciplineContract(ir);
5946
+ if (strategyId === "discipline-gates-v1-alt") return withAltDisciplineContract(ir);
5947
+ if (strategyId === "discipline-gates-v1-alt-blind") return withAltBlindDisciplineContract(ir);
5948
+ return withDisciplineContract(ir);
5795
5949
  };
5796
5950
  const replay = async (ir, model, captureGates = false) => {
5797
5951
  let gates;
@@ -5803,13 +5957,14 @@ async function runGoldenEval(opts) {
5803
5957
  };
5804
5958
  const compiled = compile(evalIr);
5805
5959
  if (captureGates) {
5806
- if (strategyId === "discipline-gates-v1-alt") {
5960
+ if (isAltStrategy(strategyId)) {
5961
+ const altSectionId = ALT_SECTION_ID_BY_STRATEGY[strategyId];
5807
5962
  const altSection = (evalIr.sections ?? []).find(
5808
- (s) => s.id === STRATEGY_ALT_SECTION_ID
5963
+ (s) => s.id === altSectionId
5809
5964
  );
5810
5965
  gates = {
5811
5966
  fired: altSection !== void 0 && altSection.text.length > 0,
5812
- rule: "discipline-gates-v1-alt",
5967
+ rule: strategyId,
5813
5968
  gateTokens: altSection ? countTokens(`${altSection.text}
5814
5969
 
5815
5970
  `) : 0
@@ -6240,7 +6395,8 @@ function classifyStrategyOutcome(r) {
6240
6395
  if (r.losses > r.wins) return "loses";
6241
6396
  return "ties";
6242
6397
  }
6243
- var STRATEGY_AUTHORSHIP_LIMITATION = "Both gate wordings share one author (kgauto-Cairn); a shared blind spot is invisible to this experiment. Strong verdicts: both-lose (mechanism) and v1-loses/alt-wins (wording). An independently-authored alt would strengthen every other cell.";
6398
+ var STRATEGY_AUTHORSHIP_LIMITATION = "Only the v1 wording was measured on this run; no re-wording arm was triggered, so this result says nothing about whether the wording or the mechanism produced it.";
6399
+ var STRATEGY_AUTHORSHIP_INDEPENDENT = "Three arms: v1 and one re-wording by this seat, plus an independently authored arm (context-free subagent, no repo access, no knowledge of the experiment). A both-lose verdict therefore spans independent authorship and is not explained by one author's blind spot. Residual limitation: all three arms encode a similar set of underlying principles, so a wrong CHOICE of principles remains harder to see than a wrong wording of them.";
6244
6400
  async function runStrategyEvalWithAttribution(opts) {
6245
6401
  const primary = await runGoldenEval({
6246
6402
  ...opts,
@@ -6257,9 +6413,20 @@ async function runStrategyEvalWithAttribution(opts) {
6257
6413
  axis: "strategy",
6258
6414
  strategy: "discipline-gates-v1-alt"
6259
6415
  });
6260
- const altOutcome = classifyStrategyOutcome(alt);
6261
- const attribution = altOutcome === "wins" ? "wording-failure" : altOutcome === "loses" ? "mechanism-failure" : "wording-inconclusive";
6262
- return { attribution, primary, alt, limitation: STRATEGY_AUTHORSHIP_LIMITATION };
6416
+ const altBlind = await runGoldenEval({
6417
+ ...opts,
6418
+ axis: "strategy",
6419
+ strategy: "discipline-gates-v1-alt-blind"
6420
+ });
6421
+ const outcomes = [classifyStrategyOutcome(alt), classifyStrategyOutcome(altBlind)];
6422
+ const attribution = outcomes.includes("wins") ? "wording-failure" : outcomes.every((o) => o === "loses") ? "mechanism-failure" : "wording-inconclusive";
6423
+ return {
6424
+ attribution,
6425
+ primary,
6426
+ alt,
6427
+ altBlind,
6428
+ limitation: STRATEGY_AUTHORSHIP_INDEPENDENT
6429
+ };
6263
6430
  }
6264
6431
 
6265
6432
  // src/oracle.ts
@@ -6516,6 +6683,30 @@ function createDelegate(opts) {
6516
6683
  }
6517
6684
 
6518
6685
  // src/advisories-api.ts
6686
+ var BURST_SPAN_MS = 5 * 60 * 1e3;
6687
+ function classifyEvidenceWindow(row) {
6688
+ const { evidence_first_at: first, evidence_last_at: last, evidence_n: n } = row;
6689
+ if (typeof first !== "string" || typeof last !== "string") return null;
6690
+ if (typeof n !== "number" || !Number.isFinite(n) || n <= 0) return null;
6691
+ const t0 = Date.parse(first);
6692
+ const t1 = Date.parse(last);
6693
+ if (Number.isNaN(t0) || Number.isNaN(t1)) return null;
6694
+ const spanMs = Math.max(0, t1 - t0);
6695
+ return { firstAt: first, lastAt: last, n, spanMs, isBurst: spanMs < BURST_SPAN_MS };
6696
+ }
6697
+ function formatEvidenceSpan(spanMs) {
6698
+ if (spanMs < 1e3) return `${spanMs}ms`;
6699
+ const s = Math.round(spanMs / 1e3);
6700
+ if (s < 90) return `${s}s`;
6701
+ const m = Math.round(s / 60);
6702
+ if (m < 90) return `${m}m`;
6703
+ const h = Math.round(m / 60);
6704
+ if (h < 48) return `${h}h`;
6705
+ return `${Math.round(h / 24)}d`;
6706
+ }
6707
+ function burstCaveat(w) {
6708
+ return `Evidence is ${w.n} observation${w.n === 1 ? "" : "s"} spanning ${formatEvidenceSpan(w.spanMs)} (${w.firstAt} \u2192 ${w.lastAt}) \u2014 that is a single burst, not a standing rate. Check it against your own deploy log before acting: a cluster this tight is usually one incident, and may already be fixed.`;
6709
+ }
6519
6710
  var SEVERITY_SET = /* @__PURE__ */ new Set(["info", "warn", "critical"]);
6520
6711
  var STATUS_SET = /* @__PURE__ */ new Set(["open", "snoozed", "resolved"]);
6521
6712
  var RESOLUTION_SOURCE_SET = /* @__PURE__ */ new Set([
@@ -6555,10 +6746,18 @@ function rowToAdvisory(row) {
6555
6746
  if (docsLink) suggestedFix.docsLink = docsLink;
6556
6747
  if (suggestion) suggestedFix.before = suggestion;
6557
6748
  }
6749
+ const evidenceWindow = classifyEvidenceWindow(
6750
+ row
6751
+ );
6752
+ const declaredSeverity = asSeverity(row.severity);
6753
+ const downgraded = evidenceWindow?.isBurst === true && declaredSeverity === "critical";
6754
+ const effectiveSeverity = downgraded ? "warn" : declaredSeverity;
6558
6755
  const out = {
6559
6756
  id: typeof row.id === "string" ? row.id : "",
6560
6757
  rule: typeof row.rule === "string" ? row.rule : "",
6561
- severity: asSeverity(row.severity),
6758
+ severity: effectiveSeverity,
6759
+ evidenceWindow,
6760
+ ...downgraded ? { severityBeforeBurstDowngrade: declaredSeverity } : {},
6562
6761
  openedAt: typeof row.opened_at === "string" ? row.opened_at : "",
6563
6762
  lastObservedAt: typeof row.last_observed_at === "string" ? row.last_observed_at : "",
6564
6763
  observationCount: typeof row.observation_count === "number" ? row.observation_count : 0,
@@ -6566,7 +6765,13 @@ function rowToAdvisory(row) {
6566
6765
  ...archetype ? { archetype } : {},
6567
6766
  ...model ? { model } : {}
6568
6767
  },
6569
- message: typeof row.message === "string" ? row.message : "",
6768
+ // The caveat goes INLINE in the message, not only in `evidenceWindow`.
6769
+ // A structured field a consumer must think to read is the same defect
6770
+ // one layer along: the substrate knowing and not telling. The whole
6771
+ // point is that the check costs a glance rather than a query.
6772
+ message: (typeof row.message === "string" ? row.message : "") + (evidenceWindow?.isBurst ? `
6773
+
6774
+ ${burstCaveat(evidenceWindow)}` : ""),
6570
6775
  suggestedFix,
6571
6776
  autoApplicable: false,
6572
6777
  // reserved — alpha.30+
@@ -7103,9 +7308,14 @@ export {
7103
7308
  ABSOLUTE_FLOOR,
7104
7309
  ALIASES,
7105
7310
  ALL_ARCHETYPES,
7311
+ ALT_BLIND_TOKEN_BUDGET_BREACH,
7312
+ ALT_STRATEGY_IDS,
7106
7313
  ARCHETYPE_FAMILY_FITS,
7107
7314
  ARCHETYPE_FLOOR_DEFAULT,
7315
+ BLOCKED_MODEL_FAMILY_SIBLING_SERVED_CODE,
7316
+ BLOCKED_MODEL_NOT_IN_ROSTER_CODE,
7108
7317
  BRAIN_READ_ENV_NAMES,
7318
+ BURST_SPAN_MS,
7109
7319
  COACH_CFG,
7110
7320
  COST_RANKING_REFERENCE_SHAPE,
7111
7321
  CallError,
@@ -7116,6 +7326,7 @@ export {
7116
7326
  DEFAULT_PROMOTIONS_ENDPOINT,
7117
7327
  DELEGATE_TOOL_DEFINITION,
7118
7328
  DIALECT_VERSION,
7329
+ DISCIPLINE_GATES_V1_ALT_BLIND_HEADER,
7119
7330
  DISCIPLINE_GATES_V1_ALT_HEADER,
7120
7331
  FamilyResolutionError,
7121
7332
  INTENT_ARCHETYPES,
@@ -7132,6 +7343,7 @@ export {
7132
7343
  RULE_DISCIPLINE_GATES_V1,
7133
7344
  RULE_DISCIPLINE_GATES_V1_STRUCTURED,
7134
7345
  RULE_SEQUENTIAL_TOOL_CLIFF,
7346
+ STRATEGY_AUTHORSHIP_INDEPENDENT,
7135
7347
  STRATEGY_AUTHORSHIP_LIMITATION,
7136
7348
  TRANSLATOR_FLOOR,
7137
7349
  _testResetMeasuredFailure,
@@ -7139,6 +7351,7 @@ export {
7139
7351
  _testWaitForMeasuredFailureRefresh,
7140
7352
  _testWaitForPromotionsRefresh,
7141
7353
  allProfiles,
7354
+ altBlindGatesBlockFor,
7142
7355
  altGatesBlockFor,
7143
7356
  applyArchetypeConvention,
7144
7357
  applySectionRewrites,
@@ -7152,9 +7365,11 @@ export {
7152
7365
  buildLLMJudge,
7153
7366
  buildPairwiseJudgePrompt,
7154
7367
  buildShadowProbeRow,
7368
+ burstCaveat,
7155
7369
  call,
7156
7370
  captureGoldenIr,
7157
7371
  chainProviderSpread,
7372
+ classifyEvidenceWindow,
7158
7373
  classifyStrategyOutcome,
7159
7374
  clearBrain,
7160
7375
  combineOrderSwappedVerdicts,
@@ -7174,6 +7389,7 @@ export {
7174
7389
  execute,
7175
7390
  findBetterFit,
7176
7391
  flushBrainDeadLetter,
7392
+ formatEvidenceSpan,
7177
7393
  getActionableAdvisories,
7178
7394
  getAllStarterChains,
7179
7395
  getAllStarterChainsWithGrounding,
@@ -7195,6 +7411,7 @@ export {
7195
7411
  getStarterChainWithGrounding,
7196
7412
  hasMutation,
7197
7413
  hashShape,
7414
+ isAltStrategy,
7198
7415
  isArchetype,
7199
7416
  isAutoPromoteEnabledFromEnv,
7200
7417
  isBrainQueryActiveFor,
@@ -7238,8 +7455,10 @@ export {
7238
7455
  resetTokenizer,
7239
7456
  resolveConventionsForProfile,
7240
7457
  resolveGoldenCaptureRate,
7458
+ resolveModelAlias,
7241
7459
  resolvePricingAt,
7242
7460
  resolveProviderKey,
7461
+ rowToAdvisory,
7243
7462
  rubricFor,
7244
7463
  runAdvisor,
7245
7464
  runGoldenEval,
@@ -7248,6 +7467,7 @@ export {
7248
7467
  shouldCaptureGolden,
7249
7468
  tryGetProfile,
7250
7469
  wilsonLowerBound,
7470
+ withAltBlindDisciplineContract,
7251
7471
  withAltDisciplineContract,
7252
7472
  withDisciplineContract
7253
7473
  };
@@ -305,6 +305,17 @@ interface CompilePolicy {
305
305
  * Model IDs the consumer has gated. Compile() will never select these.
306
306
  * Use for: cost caps, account-level rate limits, "this model is broken
307
307
  * for our workload" decisions.
308
+ *
309
+ * Matching is by EXACT id after alias resolution (alpha.85): an alias
310
+ * and its canonical id are the same model, so blocking either blocks
311
+ * both (`deepseek-chat` ≡ `deepseek-v4-flash`). Matching does NOT widen
312
+ * to family siblings — blocking `deepseek-v4-flash` says nothing about
313
+ * `deepseek-v4-pro` — and a bare family key (`'deepseek'`,
314
+ * `'claude-sonnet'`) is INERT: it matches no model and no warning fires
315
+ * today. To gate a family, list its concrete ids. Beware roster
316
+ * retargets: when a family primary moves to a new id, a literal-id
317
+ * block does not follow it (tt-intel s113 built consumer-side family
318
+ * resolution for exactly this).
308
319
  */
309
320
  blockedModels?: string[];
310
321
  /**
@@ -1073,6 +1084,44 @@ interface ShadowProbeConfig {
1073
1084
  skipSlowTierInSync?: boolean;
1074
1085
  }
1075
1086
  interface CallOptions {
1087
+ /**
1088
+ * alpha.84 — fires once per FAILED attempt during the fallback walk, before
1089
+ * the walk continues. IC-Cairn's filing, 2026-07-29.
1090
+ *
1091
+ * ## The gap this closes
1092
+ *
1093
+ * `call()` records exactly ONE outcome row per call. An attempt that fails
1094
+ * and then walks to a successful fallback survives only as
1095
+ * `fellOverFrom`/`fallbackReason` metadata ON the success row — so a
1096
+ * `call()`-only consumer's corpus contains **no failure rows at all**, by
1097
+ * construction, no matter how carefully they wired `record()`.
1098
+ *
1099
+ * That is not hypothetical. kgauto's own `corpus-implausible-success`
1100
+ * detector fired on inspire-central and told them to "wire record() on the
1101
+ * FAILURE path" — which they had already done, in five places. Measured:
1102
+ * 89 rows, 0 failure rows, 2 fellover-bearing. **The detector demanded
1103
+ * output the API could not produce.** Streaming consumers have had
1104
+ * `onFailedAttempt` since alpha.48 via `streamWithFallover`; `call()`
1105
+ * consumers had nothing. This restores parity.
1106
+ *
1107
+ * ## Contract
1108
+ *
1109
+ * Fires for every attempt whose `status` is `'retryable'` or `'terminal'`,
1110
+ * including skipped ones (`*_inferred`, `aborted`) — a skip is a real
1111
+ * datum about the chain, not an absence. Never fires for `'success'`.
1112
+ *
1113
+ * Called synchronously and **its throw is swallowed**: a consumer's
1114
+ * telemetry bug must never take down the call it is observing. It is a
1115
+ * notification, not a hook that can veto or alter the walk.
1116
+ *
1117
+ * Typical use — emit a failure row per attempt so the corpus can contain
1118
+ * failures at all:
1119
+ *
1120
+ * call(ir, { onFailedAttempt: (a) => void recordOutcome({
1121
+ * ...base, success: false, errorType: a.errorCode, model: a.model,
1122
+ * }) })
1123
+ */
1124
+ onFailedAttempt?: (attempt: CallAttempt) => void;
1076
1125
  /** Forwarded to compile(). */
1077
1126
  policy?: CompilePolicy;
1078
1127
  /**