File size: 2,525 Bytes
518343a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// Recorded router — MoBA's "record what was attended, do not prescribe what
// must be attended" discipline. The router decides per-query which blocks to
// route to; the record of those decisions IS the artifact under audit.
//
// We wrap any block-selection function (the router) so that every call emits
// `sparse.router.trace.v1`. A router whose recent traces violate its declared
// envelope is auto-demoted by the orchestrator at receipt-write time.

import type { SparseAttentionEnvelope } from "./envelope.js";
import type { SparseReceiptCommon } from "./receipts.js";

export interface SparseRouterTraceReceipt extends SparseReceiptCommon {
  readonly receiptClass: "sparse.router.trace.v1";
  readonly routerRef: string;
  readonly regimeRef: string;
  readonly queryId: string;
  readonly blocksSelected: ReadonlyArray<number>;
  readonly scoreDistribution: ReadonlyArray<number>;
}

export type BlockSelector = (queryId: string) => {
  blocksSelected: ReadonlyArray<number>;
  scoreDistribution: ReadonlyArray<number>;
};

export interface RecordedRouterInput {
  readonly routerRef: string;
  readonly envelope: SparseAttentionEnvelope;
  readonly select: BlockSelector;
  readonly tenant: string;
}

export interface RecordedRouter {
  readonly route: (queryId: string, nonce: string, issuedAt?: string) => {
    blocksSelected: ReadonlyArray<number>;
    trace: SparseRouterTraceReceipt;
    envelopeBreach: boolean;
  };
}

/**
 * Wrap a `BlockSelector` in a receipt emitter. `envelopeBreach` is true when
 * the router selects more than envelope.maxBlocks — the orchestrator MUST
 * treat that as a demotion trigger (NOT silently truncate).
 */
export function recordedRouter(input: RecordedRouterInput): RecordedRouter {
  return {
    route: (queryId, nonce, issuedAt) => {
      const at = issuedAt ?? new Date().toISOString();
      const { blocksSelected, scoreDistribution } = input.select(queryId);
      const breach = blocksSelected.length > input.envelope.maxBlocks;
      const trace: SparseRouterTraceReceipt = {
        receiptClass: "sparse.router.trace.v1",
        freshnessNonce: nonce,
        issuedAt: at,
        tenant: input.tenant,
        parentRef: input.envelope.regimeId,
        routerRef: input.routerRef,
        regimeRef: input.envelope.regimeId,
        queryId,
        blocksSelected: [...blocksSelected].sort((a, b) => a - b),
        scoreDistribution: [...scoreDistribution],
      };
      return { blocksSelected, trace, envelopeBreach: breach };
    },
  };
}