@warmdrift/kgauto-compiler 2.0.0-alpha.30 → 2.0.0-alpha.32

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.
@@ -148,6 +148,99 @@ interface TraceDetail extends TraceSummary {
148
148
  health: TraceHealth;
149
149
  }
150
150
 
151
+ /**
152
+ * proxy(req) — Glass-Box replay query handler.
153
+ *
154
+ * GET /api/glassbox/proxy?traceId=<id> → single compile_outcomes row for trace
155
+ * GET /api/glassbox/proxy?limit=<N> → N most-recent compile_outcomes rows
156
+ * GET /api/glassbox/proxy → defaults to limit=20
157
+ *
158
+ * Brain reads go through PostgREST on Supabase. We always filter by
159
+ * `app_id=eq.<appId>` even though the scoped JWT's RLS policy already
160
+ * enforces tenant isolation — the explicit filter trims the network payload
161
+ * and makes the query intent obvious to anyone reading logs.
162
+ *
163
+ * Scrub: the optional `scrub` hook runs on each row before responding so
164
+ * sensitive payloads (rendered prompts, tool calls) can be redacted at the
165
+ * proxy boundary. Default is identity (no-op).
166
+ */
167
+
168
+ /**
169
+ * Typed boundary transformer: compile_outcomes row (snake_case, Postgres
170
+ * native) → TraceSummary (camelCase, TypeScript native + extension wire
171
+ * contract). Single canonical mapping site — every DB-shape ↔ wire-shape
172
+ * crossing routes through here so renderer-expected fields can't silently
173
+ * go missing. See `feedback_typed_boundary_transformers.md` for the rule.
174
+ */
175
+ declare function rowToSummary(row: Record<string, unknown>): TraceSummary;
176
+ /**
177
+ * Typed boundary transformer (L-118): compile_outcomes row → TraceDetail.
178
+ * Single canonical mapping site for the detail-view wire contract. Pre-018
179
+ * brain rows return NULL for the new columns; this transformer applies
180
+ * type-safe defaults (0 for token columns, undefined for optional strings)
181
+ * so the renderer never crashes on missing fields.
182
+ *
183
+ * alpha.28 extends the field set per the renderer design contract — every
184
+ * extended field has a default. The `counterfactuals` + `projectedDailyCost`
185
+ * fields are populated by the proxy AFTER this transformer runs (they need
186
+ * profile lookup / brain query / async I/O).
187
+ */
188
+ declare function rowToDetail(row: Record<string, unknown>): TraceDetail;
189
+
190
+ /**
191
+ * computeCounterfactuals — for a served trace, compute up to 2 cheaper
192
+ * alternatives that ALSO meet the archetype quality floor.
193
+ *
194
+ * Pure function. Called at detail-view time by the proxy's `rowToDetail`
195
+ * path. The cost-equivalent answer to "what could this have run on without
196
+ * tanking quality?" lets the Glass-Box render `💰 Cheaper alternatives` so a
197
+ * founder sees the actual cost of the routing choice rather than just the
198
+ * served cost.
199
+ *
200
+ * Mechanism (per design contract Phase 0 spec):
201
+ * 1. Walk the archetype's full default fallback chain.
202
+ * 2. For each model in the chain (excluding the served model):
203
+ * - Skip if archetypePerf[archetype] < ARCHETYPE_FLOOR_DEFAULT (6).
204
+ * Matches the cliff-advisor work — never recommend a swap that
205
+ * violates the quality floor. (Composition with Builder C is
206
+ * intentional.)
207
+ * - Skip if profile missing.
208
+ * - Compute estimated cost at the OBSERVED token counts:
209
+ * non_cached_in = tokensIn - cacheReadInputTokens
210
+ * cost = (non_cached_in / 1e6) * profile.costInputPer1m
211
+ * + (cacheable_in / 1e6) * profile.costInputPer1m
212
+ * * profile.lowering.cache.discount
213
+ * + (tokensOut / 1e6) * profile.costOutputPer1m
214
+ * - Only include if estimatedCostUsd ≤ 0.9 × servedCostUsd
215
+ * (≥10% cheaper — below that's noise of token-count estimation).
216
+ * 3. Sort cheapest first.
217
+ * 4. Cap to top 2.
218
+ *
219
+ * Edge cases:
220
+ * - tokensIn === 0 → return [] (can't meaningfully estimate).
221
+ * - servedCostUsd === 0 → return [] (no baseline to beat).
222
+ * - Zero matching alternatives → return [] (renderer hides the card).
223
+ * - Served model isn't in the chain (e.g. forceModel'd off-chain) → use
224
+ * the chain for the archetype anyway.
225
+ */
226
+
227
+ /** Only surface swaps that save at least 10% vs. served. */
228
+ declare const COUNTERFACTUAL_MIN_SAVINGS_RATIO = 0.1;
229
+ /** Cap on alternatives surfaced per trace. */
230
+ declare const COUNTERFACTUAL_MAX_RESULTS = 2;
231
+ interface ComputeCounterfactualsArgs {
232
+ servedModel: string;
233
+ servedCostUsd: number;
234
+ archetype: string;
235
+ tokensIn: number;
236
+ tokensOut: number;
237
+ /** Defaults to 0 when caller doesn't know. */
238
+ cacheReadInputTokens?: number;
239
+ /** Reserved for future per-mode chain selection. */
240
+ toolOrchestration?: 'parallel' | 'sequential' | 'either';
241
+ }
242
+ declare function computeCounterfactuals(args: ComputeCounterfactualsArgs): TraceCounterfactual[];
243
+
151
244
  /**
152
245
  * Public entry point for `@warmdrift/kgauto-compiler/glassbox-routes`.
153
246
  *
@@ -239,4 +332,4 @@ interface GlassboxRoutes {
239
332
  }
240
333
  declare function createGlassboxRoutes(config: GlassboxRoutesConfig): GlassboxRoutes;
241
334
 
242
- export { type GlassboxRoutes, type GlassboxRoutesConfig, type TraceDetail, type TraceSummary, createGlassboxRoutes };
335
+ export { type AdvisoryRecord, COUNTERFACTUAL_MAX_RESULTS, COUNTERFACTUAL_MIN_SAVINGS_RATIO, type ComputeCounterfactualsArgs, type GlassboxRoutes, type GlassboxRoutesConfig, type TraceCounterfactual, type TraceDetail, type TraceHealth, type TraceSectionRewrite, type TraceSummary, computeCounterfactuals, createGlassboxRoutes, rowToDetail, rowToSummary };
@@ -148,6 +148,99 @@ interface TraceDetail extends TraceSummary {
148
148
  health: TraceHealth;
149
149
  }
150
150
 
151
+ /**
152
+ * proxy(req) — Glass-Box replay query handler.
153
+ *
154
+ * GET /api/glassbox/proxy?traceId=<id> → single compile_outcomes row for trace
155
+ * GET /api/glassbox/proxy?limit=<N> → N most-recent compile_outcomes rows
156
+ * GET /api/glassbox/proxy → defaults to limit=20
157
+ *
158
+ * Brain reads go through PostgREST on Supabase. We always filter by
159
+ * `app_id=eq.<appId>` even though the scoped JWT's RLS policy already
160
+ * enforces tenant isolation — the explicit filter trims the network payload
161
+ * and makes the query intent obvious to anyone reading logs.
162
+ *
163
+ * Scrub: the optional `scrub` hook runs on each row before responding so
164
+ * sensitive payloads (rendered prompts, tool calls) can be redacted at the
165
+ * proxy boundary. Default is identity (no-op).
166
+ */
167
+
168
+ /**
169
+ * Typed boundary transformer: compile_outcomes row (snake_case, Postgres
170
+ * native) → TraceSummary (camelCase, TypeScript native + extension wire
171
+ * contract). Single canonical mapping site — every DB-shape ↔ wire-shape
172
+ * crossing routes through here so renderer-expected fields can't silently
173
+ * go missing. See `feedback_typed_boundary_transformers.md` for the rule.
174
+ */
175
+ declare function rowToSummary(row: Record<string, unknown>): TraceSummary;
176
+ /**
177
+ * Typed boundary transformer (L-118): compile_outcomes row → TraceDetail.
178
+ * Single canonical mapping site for the detail-view wire contract. Pre-018
179
+ * brain rows return NULL for the new columns; this transformer applies
180
+ * type-safe defaults (0 for token columns, undefined for optional strings)
181
+ * so the renderer never crashes on missing fields.
182
+ *
183
+ * alpha.28 extends the field set per the renderer design contract — every
184
+ * extended field has a default. The `counterfactuals` + `projectedDailyCost`
185
+ * fields are populated by the proxy AFTER this transformer runs (they need
186
+ * profile lookup / brain query / async I/O).
187
+ */
188
+ declare function rowToDetail(row: Record<string, unknown>): TraceDetail;
189
+
190
+ /**
191
+ * computeCounterfactuals — for a served trace, compute up to 2 cheaper
192
+ * alternatives that ALSO meet the archetype quality floor.
193
+ *
194
+ * Pure function. Called at detail-view time by the proxy's `rowToDetail`
195
+ * path. The cost-equivalent answer to "what could this have run on without
196
+ * tanking quality?" lets the Glass-Box render `💰 Cheaper alternatives` so a
197
+ * founder sees the actual cost of the routing choice rather than just the
198
+ * served cost.
199
+ *
200
+ * Mechanism (per design contract Phase 0 spec):
201
+ * 1. Walk the archetype's full default fallback chain.
202
+ * 2. For each model in the chain (excluding the served model):
203
+ * - Skip if archetypePerf[archetype] < ARCHETYPE_FLOOR_DEFAULT (6).
204
+ * Matches the cliff-advisor work — never recommend a swap that
205
+ * violates the quality floor. (Composition with Builder C is
206
+ * intentional.)
207
+ * - Skip if profile missing.
208
+ * - Compute estimated cost at the OBSERVED token counts:
209
+ * non_cached_in = tokensIn - cacheReadInputTokens
210
+ * cost = (non_cached_in / 1e6) * profile.costInputPer1m
211
+ * + (cacheable_in / 1e6) * profile.costInputPer1m
212
+ * * profile.lowering.cache.discount
213
+ * + (tokensOut / 1e6) * profile.costOutputPer1m
214
+ * - Only include if estimatedCostUsd ≤ 0.9 × servedCostUsd
215
+ * (≥10% cheaper — below that's noise of token-count estimation).
216
+ * 3. Sort cheapest first.
217
+ * 4. Cap to top 2.
218
+ *
219
+ * Edge cases:
220
+ * - tokensIn === 0 → return [] (can't meaningfully estimate).
221
+ * - servedCostUsd === 0 → return [] (no baseline to beat).
222
+ * - Zero matching alternatives → return [] (renderer hides the card).
223
+ * - Served model isn't in the chain (e.g. forceModel'd off-chain) → use
224
+ * the chain for the archetype anyway.
225
+ */
226
+
227
+ /** Only surface swaps that save at least 10% vs. served. */
228
+ declare const COUNTERFACTUAL_MIN_SAVINGS_RATIO = 0.1;
229
+ /** Cap on alternatives surfaced per trace. */
230
+ declare const COUNTERFACTUAL_MAX_RESULTS = 2;
231
+ interface ComputeCounterfactualsArgs {
232
+ servedModel: string;
233
+ servedCostUsd: number;
234
+ archetype: string;
235
+ tokensIn: number;
236
+ tokensOut: number;
237
+ /** Defaults to 0 when caller doesn't know. */
238
+ cacheReadInputTokens?: number;
239
+ /** Reserved for future per-mode chain selection. */
240
+ toolOrchestration?: 'parallel' | 'sequential' | 'either';
241
+ }
242
+ declare function computeCounterfactuals(args: ComputeCounterfactualsArgs): TraceCounterfactual[];
243
+
151
244
  /**
152
245
  * Public entry point for `@warmdrift/kgauto-compiler/glassbox-routes`.
153
246
  *
@@ -239,4 +332,4 @@ interface GlassboxRoutes {
239
332
  }
240
333
  declare function createGlassboxRoutes(config: GlassboxRoutesConfig): GlassboxRoutes;
241
334
 
242
- export { type GlassboxRoutes, type GlassboxRoutesConfig, type TraceDetail, type TraceSummary, createGlassboxRoutes };
335
+ export { type AdvisoryRecord, COUNTERFACTUAL_MAX_RESULTS, COUNTERFACTUAL_MIN_SAVINGS_RATIO, type ComputeCounterfactualsArgs, type GlassboxRoutes, type GlassboxRoutesConfig, type TraceCounterfactual, type TraceDetail, type TraceHealth, type TraceSectionRewrite, type TraceSummary, computeCounterfactuals, createGlassboxRoutes, rowToDetail, rowToSummary };
@@ -20,7 +20,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/glassbox-routes/index.ts
21
21
  var glassbox_routes_exports = {};
22
22
  __export(glassbox_routes_exports, {
23
- createGlassboxRoutes: () => createGlassboxRoutes
23
+ COUNTERFACTUAL_MAX_RESULTS: () => COUNTERFACTUAL_MAX_RESULTS,
24
+ COUNTERFACTUAL_MIN_SAVINGS_RATIO: () => COUNTERFACTUAL_MIN_SAVINGS_RATIO,
25
+ computeCounterfactuals: () => computeCounterfactuals,
26
+ createGlassboxRoutes: () => createGlassboxRoutes,
27
+ rowToDetail: () => rowToDetail,
28
+ rowToSummary: () => rowToSummary
24
29
  });
25
30
  module.exports = __toCommonJS(glassbox_routes_exports);
26
31
 
@@ -1815,6 +1820,12 @@ function summarizeSectionRewrite(kind, rule) {
1815
1820
  if (kind === "tool_call_contract" && rule === "sequential-tool-cliff-below-floor") {
1816
1821
  return "Sequential tool pattern applied (model cliff cleared at compile time).";
1817
1822
  }
1823
+ if (kind === "narration_contract" && rule === "narration-drift-anthropic") {
1824
+ return "Narration tightened for Anthropic dialect (terse-log shape preserved).";
1825
+ }
1826
+ if (kind === "narration_contract" && rule === "narration-thinking-leak-deepseek") {
1827
+ return "Thinking-block suppression applied (DeepSeek V4 internal reasoning kept off-wire).";
1828
+ }
1818
1829
  return `Translator applied rule "${rule}" to ${kind} section.`;
1819
1830
  }
1820
1831
  function rowToSectionRewrite(raw) {
@@ -2448,5 +2459,10 @@ function createGlassboxRoutes(config) {
2448
2459
  }
2449
2460
  // Annotate the CommonJS export names for ESM import in node:
2450
2461
  0 && (module.exports = {
2451
- createGlassboxRoutes
2462
+ COUNTERFACTUAL_MAX_RESULTS,
2463
+ COUNTERFACTUAL_MIN_SAVINGS_RATIO,
2464
+ computeCounterfactuals,
2465
+ createGlassboxRoutes,
2466
+ rowToDetail,
2467
+ rowToSummary
2452
2468
  });
@@ -295,6 +295,12 @@ function summarizeSectionRewrite(kind, rule) {
295
295
  if (kind === "tool_call_contract" && rule === "sequential-tool-cliff-below-floor") {
296
296
  return "Sequential tool pattern applied (model cliff cleared at compile time).";
297
297
  }
298
+ if (kind === "narration_contract" && rule === "narration-drift-anthropic") {
299
+ return "Narration tightened for Anthropic dialect (terse-log shape preserved).";
300
+ }
301
+ if (kind === "narration_contract" && rule === "narration-thinking-leak-deepseek") {
302
+ return "Thinking-block suppression applied (DeepSeek V4 internal reasoning kept off-wire).";
303
+ }
298
304
  return `Translator applied rule "${rule}" to ${kind} section.`;
299
305
  }
300
306
  function rowToSectionRewrite(raw) {
@@ -648,5 +654,10 @@ function createGlassboxRoutes(config) {
648
654
  return { proxy, stream };
649
655
  }
650
656
  export {
651
- createGlassboxRoutes
657
+ COUNTERFACTUAL_MAX_RESULTS,
658
+ COUNTERFACTUAL_MIN_SAVINGS_RATIO,
659
+ computeCounterfactuals,
660
+ createGlassboxRoutes,
661
+ rowToDetail,
662
+ rowToSummary
652
663
  };
package/dist/index.d.mts CHANGED
@@ -374,26 +374,42 @@ interface RunAdvisorPhase2Context {
374
374
  declare function runAdvisor(ir: PromptIR, result: AdvisorContext, profile: ModelProfile, policy?: CompilePolicy, phase2?: RunAdvisorPhase2Context): BestPracticeAdvisory[];
375
375
 
376
376
  /**
377
- * Translator primitive — alpha.29.
377
+ * Translator primitive — alpha.31.
378
378
  *
379
379
  * Pure function. Walks `IR.sections`, matches each section's `kind` against
380
- * the model + archetype + profile, and applies a model-aware rewrite when
381
- * a rule fires. Returns the rewritten IR + the list of rewrites for
382
- * `CompileResult.sectionRewritesApplied` and brain persistence.
380
+ * a per-rule dispatch table keyed on (kind, profile, archetype), and applies
381
+ * a model-aware rewrite when a rule fires. Returns the rewritten IR + the
382
+ * list of rewrites for `CompileResult.sectionRewritesApplied` and brain
383
+ * persistence.
383
384
  *
384
385
  * This is the s37 translator-framing eureka in code: kgauto graduates from
385
386
  * "gate" (alpha.28's cliff advisor: "consumer must accept adapter") to
386
387
  * "translator" (alpha.29: "consumer declared the section kind, kgauto
387
388
  * applies the adapter at compile time without consumer-side branching").
388
389
  *
389
- * alpha.29 ships ONE rule:
390
+ * Rules shipped:
390
391
  *
391
- * tool_call_contract + profile.archetypePerf[archetype] < ARCHETYPE_FLOOR_DEFAULT
392
+ * alpha.29 — tool_call_contract + archetypePerf[archetype] < TRANSLATOR_FLOOR
392
393
  * → prepend sequential-tool-pattern guidance
393
394
  * → emit wireOverrides: { parallelToolCalls: false }
395
+ * → rule_id: 'sequential-tool-cliff-below-floor'
394
396
  *
395
- * alpha.30+ will extend the rule table to `narration_contract`, `role_intro`,
396
- * etc. Each new rule lands here as an explicit branch.
397
+ * alpha.31 narration_contract + profile.provider === 'anthropic'
398
+ * prepend terse-log narration guidance
399
+ * → no wireOverrides
400
+ * → rule_id: 'narration-drift-anthropic'
401
+ *
402
+ * alpha.31 — narration_contract + profile.provider === 'deepseek'
403
+ * → prepend <thinking>-suppression guidance
404
+ * → no wireOverrides
405
+ * → rule_id: 'narration-thinking-leak-deepseek'
406
+ *
407
+ * Per-rule walk (alpha.31 refactor): the alpha.30 short-circuit
408
+ * `if (!cliffFires) return passthrough` was correct when only the
409
+ * cliff-gated tool_call_contract rule existed; alpha.31's narration rules
410
+ * fire on every call regardless of cliff (narration drift is steady-state,
411
+ * not a cliff condition). Each section now consults the dispatch table
412
+ * independently — first-match wins per (section.kind, profile, archetype).
397
413
  *
398
414
  * **Interaction with the cliff advisor (alpha.28):** when this translator
399
415
  * fires for a `tool_call_contract` section, the advisor's
@@ -402,8 +418,9 @@ declare function runAdvisor(ir: PromptIR, result: AdvisorContext, profile: Model
402
418
  * suppression check lives in `advisor.ts` and consults the
403
419
  * `CompileResult.sectionRewritesApplied` list.
404
420
  *
405
- * Design contract:
421
+ * Design contracts:
406
422
  * command-center/advisory/kgauto/2026-05-21_alpha-29-translator-and-advisories-api.md
423
+ * command-center/advisory/kgauto/2026-05-22_alpha-31-narration-contract.md
407
424
  */
408
425
 
409
426
  /**
@@ -415,8 +432,9 @@ declare function runAdvisor(ir: PromptIR, result: AdvisorContext, profile: Model
415
432
  declare const TRANSLATOR_FLOOR = 6;
416
433
  /**
417
434
  * Stable identifier of the alpha.29 sequential-tool rule. Surfaces on
418
- * `SectionRewrite.rule` and in brain aggregates. Future rules extend this
419
- * list; the brain learns per-rule effectiveness over time.
435
+ * `SectionRewrite.rule` and in brain aggregates. The brain treats this
436
+ * identifier together with the preamble string as the rule's wire
437
+ * fingerprint — both stay byte-stable across releases.
420
438
  */
421
439
  declare const RULE_SEQUENTIAL_TOOL_CLIFF = "sequential-tool-cliff-below-floor";
422
440
  interface ApplySectionRewritesArgs {
@@ -442,16 +460,14 @@ interface ApplySectionRewritesResult {
442
460
  * Pure function. Apply model-aware section rewrites to the IR at compile time.
443
461
  *
444
462
  * Discipline:
445
- * - Never mutates the input IR; returns a new IR with new sections array.
463
+ * - Never mutates the input IR; returns a new IR with new sections array
464
+ * when at least one rewrite fired; otherwise returns the input IR by
465
+ * reference (referential identity preserved on no-op).
446
466
  * - Sections without a `kind` (or `kind === 'arbitrary'`) pass through
447
467
  * unchanged.
448
468
  * - Empty `sections` array → returns `{ rewrittenIR: ir, rewrites: [] }`.
449
- * - Missing `profile.archetypePerf` no rewrite (defensive treat the
450
- * model as un-classified rather than below-floor).
451
- * - Sections of the same `kind` are processed in array order; the rule
452
- * fires once per matching section (today every tool_call_contract
453
- * section gets the same prepend — multiple sections of the same kind
454
- * ARE supported but is an unusual consumer shape).
469
+ * - Sections of the same `kind` are processed in array order; first-match
470
+ * wins per section. (Today every rule is a single-match rule.)
455
471
  *
456
472
  * @example
457
473
  * ```ts
package/dist/index.d.ts CHANGED
@@ -374,26 +374,42 @@ interface RunAdvisorPhase2Context {
374
374
  declare function runAdvisor(ir: PromptIR, result: AdvisorContext, profile: ModelProfile, policy?: CompilePolicy, phase2?: RunAdvisorPhase2Context): BestPracticeAdvisory[];
375
375
 
376
376
  /**
377
- * Translator primitive — alpha.29.
377
+ * Translator primitive — alpha.31.
378
378
  *
379
379
  * Pure function. Walks `IR.sections`, matches each section's `kind` against
380
- * the model + archetype + profile, and applies a model-aware rewrite when
381
- * a rule fires. Returns the rewritten IR + the list of rewrites for
382
- * `CompileResult.sectionRewritesApplied` and brain persistence.
380
+ * a per-rule dispatch table keyed on (kind, profile, archetype), and applies
381
+ * a model-aware rewrite when a rule fires. Returns the rewritten IR + the
382
+ * list of rewrites for `CompileResult.sectionRewritesApplied` and brain
383
+ * persistence.
383
384
  *
384
385
  * This is the s37 translator-framing eureka in code: kgauto graduates from
385
386
  * "gate" (alpha.28's cliff advisor: "consumer must accept adapter") to
386
387
  * "translator" (alpha.29: "consumer declared the section kind, kgauto
387
388
  * applies the adapter at compile time without consumer-side branching").
388
389
  *
389
- * alpha.29 ships ONE rule:
390
+ * Rules shipped:
390
391
  *
391
- * tool_call_contract + profile.archetypePerf[archetype] < ARCHETYPE_FLOOR_DEFAULT
392
+ * alpha.29 — tool_call_contract + archetypePerf[archetype] < TRANSLATOR_FLOOR
392
393
  * → prepend sequential-tool-pattern guidance
393
394
  * → emit wireOverrides: { parallelToolCalls: false }
395
+ * → rule_id: 'sequential-tool-cliff-below-floor'
394
396
  *
395
- * alpha.30+ will extend the rule table to `narration_contract`, `role_intro`,
396
- * etc. Each new rule lands here as an explicit branch.
397
+ * alpha.31 narration_contract + profile.provider === 'anthropic'
398
+ * prepend terse-log narration guidance
399
+ * → no wireOverrides
400
+ * → rule_id: 'narration-drift-anthropic'
401
+ *
402
+ * alpha.31 — narration_contract + profile.provider === 'deepseek'
403
+ * → prepend <thinking>-suppression guidance
404
+ * → no wireOverrides
405
+ * → rule_id: 'narration-thinking-leak-deepseek'
406
+ *
407
+ * Per-rule walk (alpha.31 refactor): the alpha.30 short-circuit
408
+ * `if (!cliffFires) return passthrough` was correct when only the
409
+ * cliff-gated tool_call_contract rule existed; alpha.31's narration rules
410
+ * fire on every call regardless of cliff (narration drift is steady-state,
411
+ * not a cliff condition). Each section now consults the dispatch table
412
+ * independently — first-match wins per (section.kind, profile, archetype).
397
413
  *
398
414
  * **Interaction with the cliff advisor (alpha.28):** when this translator
399
415
  * fires for a `tool_call_contract` section, the advisor's
@@ -402,8 +418,9 @@ declare function runAdvisor(ir: PromptIR, result: AdvisorContext, profile: Model
402
418
  * suppression check lives in `advisor.ts` and consults the
403
419
  * `CompileResult.sectionRewritesApplied` list.
404
420
  *
405
- * Design contract:
421
+ * Design contracts:
406
422
  * command-center/advisory/kgauto/2026-05-21_alpha-29-translator-and-advisories-api.md
423
+ * command-center/advisory/kgauto/2026-05-22_alpha-31-narration-contract.md
407
424
  */
408
425
 
409
426
  /**
@@ -415,8 +432,9 @@ declare function runAdvisor(ir: PromptIR, result: AdvisorContext, profile: Model
415
432
  declare const TRANSLATOR_FLOOR = 6;
416
433
  /**
417
434
  * Stable identifier of the alpha.29 sequential-tool rule. Surfaces on
418
- * `SectionRewrite.rule` and in brain aggregates. Future rules extend this
419
- * list; the brain learns per-rule effectiveness over time.
435
+ * `SectionRewrite.rule` and in brain aggregates. The brain treats this
436
+ * identifier together with the preamble string as the rule's wire
437
+ * fingerprint — both stay byte-stable across releases.
420
438
  */
421
439
  declare const RULE_SEQUENTIAL_TOOL_CLIFF = "sequential-tool-cliff-below-floor";
422
440
  interface ApplySectionRewritesArgs {
@@ -442,16 +460,14 @@ interface ApplySectionRewritesResult {
442
460
  * Pure function. Apply model-aware section rewrites to the IR at compile time.
443
461
  *
444
462
  * Discipline:
445
- * - Never mutates the input IR; returns a new IR with new sections array.
463
+ * - Never mutates the input IR; returns a new IR with new sections array
464
+ * when at least one rewrite fired; otherwise returns the input IR by
465
+ * reference (referential identity preserved on no-op).
446
466
  * - Sections without a `kind` (or `kind === 'arbitrary'`) pass through
447
467
  * unchanged.
448
468
  * - Empty `sections` array → returns `{ rewrittenIR: ir, rewrites: [] }`.
449
- * - Missing `profile.archetypePerf` no rewrite (defensive treat the
450
- * model as un-classified rather than below-floor).
451
- * - Sections of the same `kind` are processed in array order; the rule
452
- * fires once per matching section (today every tool_call_contract
453
- * section gets the same prepend — multiple sections of the same kind
454
- * ARE supported but is an unusual consumer shape).
469
+ * - Sections of the same `kind` are processed in array order; first-match
470
+ * wins per section. (Today every rule is a single-match rule.)
455
471
  *
456
472
  * @example
457
473
  * ```ts
package/dist/index.js CHANGED
@@ -2630,34 +2630,62 @@ function detectArchetypePerfFloorBreach(ir, profile) {
2630
2630
  // src/translator.ts
2631
2631
  var TRANSLATOR_FLOOR = ARCHETYPE_FLOOR_DEFAULT;
2632
2632
  var RULE_SEQUENTIAL_TOOL_CLIFF = "sequential-tool-cliff-below-floor";
2633
+ var RULE_NARRATION_DRIFT_ANTHROPIC = "narration-drift-anthropic";
2634
+ var RULE_NARRATION_THINKING_LEAK_DEEPSEEK = "narration-thinking-leak-deepseek";
2633
2635
  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.";
2636
+ var NARRATION_DRIFT_ANTHROPIC_PREAMBLE = "Output ONLY the requested content. Do not narrate your thought process. Each line \u2264 12 words.";
2637
+ 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.";
2638
+ function matchRule(kind, profile, archetype) {
2639
+ if (kind === "tool_call_contract") {
2640
+ if (!profile.archetypePerf) return null;
2641
+ const archetypeScore = profile.archetypePerf[archetype];
2642
+ if (typeof archetypeScore !== "number" || archetypeScore >= TRANSLATOR_FLOOR) {
2643
+ return null;
2644
+ }
2645
+ return {
2646
+ id: RULE_SEQUENTIAL_TOOL_CLIFF,
2647
+ preamble: SEQUENTIAL_TOOL_PREAMBLE,
2648
+ wireOverrides: { parallelToolCalls: false }
2649
+ };
2650
+ }
2651
+ if (kind === "narration_contract") {
2652
+ if (profile.provider === "anthropic") {
2653
+ return {
2654
+ id: RULE_NARRATION_DRIFT_ANTHROPIC,
2655
+ preamble: NARRATION_DRIFT_ANTHROPIC_PREAMBLE
2656
+ };
2657
+ }
2658
+ if (profile.provider === "deepseek") {
2659
+ return {
2660
+ id: RULE_NARRATION_THINKING_LEAK_DEEPSEEK,
2661
+ preamble: NARRATION_THINKING_LEAK_DEEPSEEK_PREAMBLE
2662
+ };
2663
+ }
2664
+ return null;
2665
+ }
2666
+ return null;
2667
+ }
2634
2668
  function applySectionRewrites(args) {
2635
2669
  const { ir, profile, archetype } = args;
2636
2670
  if (!Array.isArray(ir.sections) || ir.sections.length === 0) {
2637
2671
  return { rewrittenIR: ir, rewrites: [] };
2638
2672
  }
2639
- if (!profile.archetypePerf) {
2640
- return { rewrittenIR: ir, rewrites: [] };
2641
- }
2642
- const archetypeScore = profile.archetypePerf[archetype];
2643
- const cliffFires = typeof archetypeScore === "number" && archetypeScore < TRANSLATOR_FLOOR;
2644
- if (!cliffFires) {
2645
- return { rewrittenIR: ir, rewrites: [] };
2646
- }
2647
2673
  const rewrites = [];
2648
2674
  const newSections = ir.sections.map((section) => {
2649
- if (section.kind !== "tool_call_contract") return section;
2675
+ if (!section.kind || section.kind === "arbitrary") return section;
2676
+ const rule = matchRule(section.kind, profile, archetype);
2677
+ if (!rule) return section;
2650
2678
  const originalText = section.text;
2651
- const transformedText = `${SEQUENTIAL_TOOL_PREAMBLE}
2679
+ const transformedText = `${rule.preamble}
2652
2680
 
2653
2681
  ${originalText}`;
2654
2682
  rewrites.push({
2655
2683
  sectionId: section.id,
2656
- kind: "tool_call_contract",
2657
- rule: RULE_SEQUENTIAL_TOOL_CLIFF,
2684
+ kind: section.kind,
2685
+ rule: rule.id,
2658
2686
  originalText,
2659
2687
  transformedText,
2660
- wireOverrides: { parallelToolCalls: false }
2688
+ ...rule.wireOverrides ? { wireOverrides: rule.wireOverrides } : {}
2661
2689
  });
2662
2690
  return { ...section, text: transformedText };
2663
2691
  });
package/dist/index.mjs CHANGED
@@ -1056,34 +1056,62 @@ function detectArchetypePerfFloorBreach(ir, profile) {
1056
1056
  // src/translator.ts
1057
1057
  var TRANSLATOR_FLOOR = ARCHETYPE_FLOOR_DEFAULT;
1058
1058
  var RULE_SEQUENTIAL_TOOL_CLIFF = "sequential-tool-cliff-below-floor";
1059
+ var RULE_NARRATION_DRIFT_ANTHROPIC = "narration-drift-anthropic";
1060
+ var RULE_NARRATION_THINKING_LEAK_DEEPSEEK = "narration-thinking-leak-deepseek";
1059
1061
  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.";
1062
+ var NARRATION_DRIFT_ANTHROPIC_PREAMBLE = "Output ONLY the requested content. Do not narrate your thought process. Each line \u2264 12 words.";
1063
+ 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.";
1064
+ function matchRule(kind, profile, archetype) {
1065
+ if (kind === "tool_call_contract") {
1066
+ if (!profile.archetypePerf) return null;
1067
+ const archetypeScore = profile.archetypePerf[archetype];
1068
+ if (typeof archetypeScore !== "number" || archetypeScore >= TRANSLATOR_FLOOR) {
1069
+ return null;
1070
+ }
1071
+ return {
1072
+ id: RULE_SEQUENTIAL_TOOL_CLIFF,
1073
+ preamble: SEQUENTIAL_TOOL_PREAMBLE,
1074
+ wireOverrides: { parallelToolCalls: false }
1075
+ };
1076
+ }
1077
+ if (kind === "narration_contract") {
1078
+ if (profile.provider === "anthropic") {
1079
+ return {
1080
+ id: RULE_NARRATION_DRIFT_ANTHROPIC,
1081
+ preamble: NARRATION_DRIFT_ANTHROPIC_PREAMBLE
1082
+ };
1083
+ }
1084
+ if (profile.provider === "deepseek") {
1085
+ return {
1086
+ id: RULE_NARRATION_THINKING_LEAK_DEEPSEEK,
1087
+ preamble: NARRATION_THINKING_LEAK_DEEPSEEK_PREAMBLE
1088
+ };
1089
+ }
1090
+ return null;
1091
+ }
1092
+ return null;
1093
+ }
1060
1094
  function applySectionRewrites(args) {
1061
1095
  const { ir, profile, archetype } = args;
1062
1096
  if (!Array.isArray(ir.sections) || ir.sections.length === 0) {
1063
1097
  return { rewrittenIR: ir, rewrites: [] };
1064
1098
  }
1065
- if (!profile.archetypePerf) {
1066
- return { rewrittenIR: ir, rewrites: [] };
1067
- }
1068
- const archetypeScore = profile.archetypePerf[archetype];
1069
- const cliffFires = typeof archetypeScore === "number" && archetypeScore < TRANSLATOR_FLOOR;
1070
- if (!cliffFires) {
1071
- return { rewrittenIR: ir, rewrites: [] };
1072
- }
1073
1099
  const rewrites = [];
1074
1100
  const newSections = ir.sections.map((section) => {
1075
- if (section.kind !== "tool_call_contract") return section;
1101
+ if (!section.kind || section.kind === "arbitrary") return section;
1102
+ const rule = matchRule(section.kind, profile, archetype);
1103
+ if (!rule) return section;
1076
1104
  const originalText = section.text;
1077
- const transformedText = `${SEQUENTIAL_TOOL_PREAMBLE}
1105
+ const transformedText = `${rule.preamble}
1078
1106
 
1079
1107
  ${originalText}`;
1080
1108
  rewrites.push({
1081
1109
  sectionId: section.id,
1082
- kind: "tool_call_contract",
1083
- rule: RULE_SEQUENTIAL_TOOL_CLIFF,
1110
+ kind: section.kind,
1111
+ rule: rule.id,
1084
1112
  originalText,
1085
1113
  transformedText,
1086
- wireOverrides: { parallelToolCalls: false }
1114
+ ...rule.wireOverrides ? { wireOverrides: rule.wireOverrides } : {}
1087
1115
  });
1088
1116
  return { ...section, text: transformedText };
1089
1117
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warmdrift/kgauto-compiler",
3
- "version": "2.0.0-alpha.30",
3
+ "version": "2.0.0-alpha.32",
4
4
  "description": "Prompt compiler + central learning brain for multi-model AI apps. Swap models without rewriting prompts.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",