Files
hanzo.ai/components/kai/scenario.ts
zeekayandHanzo Dev 2d8ddd097d /kai: theme inks for the figures, a phone-sized headline, plain board title
The distribution's track and runners-up, the switches and the surface read in
theme inks, so they hold on a light ground. The hero's sentence takes a size
that tracks the width on phones, keeping "don't generate text." on one line at
375. The harness board says what it is: Kai and Jev on Decision Index v2.
evidence_sufficient is evidence_enough, so the graph's label clears its value.


Co-authored-by: Hanzo Dev <[email protected]>
2026-10-09 04:27:02 -07:00

121 lines
5.9 KiB
TypeScript

/**
* The scripted scenario the /kai hero plays: one vehicle, one Decision Program,
* and a deterministic answer for every combination of evidence.
*
* NOTHING HERE IS MODEL OUTPUT, and the figure says so on its face. Every
* probability is written below by hand, so the same toggles always draw the same
* graph, and a reader is shown what the mechanism does rather than a number that
* looks measured. The page states the model is in training; a figure that
* computed its answers would be a claim the page cannot back.
*
* The arithmetic is the least that still behaves like the mechanism it stands
* for: a decision's confidence is what it has with every source, less the share
* each missing source carried. Confident decisions freeze on the first pass;
* uncertain and dependent ones are refined on the second; a decision whose
* evidence cannot carry it defers and names what it needs.
*/
/** An evidence stream. `toggle` is whether the reader can switch it off. */
export type Source = 'camera' | 'thermal' | 'radar' | 'can' | 'audio' | 'sysml' | 'sim' | 'docs'
/** The shape a stream's trace draws in its scope window. */
export type Wave = 'frames' | 'slow' | 'ping' | 'bits' | 'noise' | 'struct' | 'saw' | 'sparse'
export const SOURCES: readonly { id: Source; label: string; name: string; wave: Wave; toggle: boolean }[] = [
{ id: 'camera', label: 'camera', name: 'camera', wave: 'frames', toggle: true },
{ id: 'thermal', label: 'thermal', name: 'thermal', wave: 'slow', toggle: false },
{ id: 'radar', label: 'radar', name: 'radar', wave: 'ping', toggle: false },
{ id: 'can', label: 'can', name: 'CAN', wave: 'bits', toggle: true },
{ id: 'audio', label: 'audio', name: 'audio', wave: 'noise', toggle: true },
{ id: 'sysml', label: 'sysml', name: 'SysML', wave: 'struct', toggle: true },
{ id: 'sim', label: 'sim', name: 'simulation', wave: 'saw', toggle: true },
{ id: 'docs', label: 'docs', name: 'docs', wave: 'sparse', toggle: false },
]
/**
* A typed variable in the program. `full` is its confidence with every source
* present; `weights` is how much of that each source carries. `lag` is how far
* below `full` the first pass sits for a decision that depends on another one —
* it cannot be confident before its parent is.
*/
export type Decision = {
id: string
value: string
full: number
weights: Partial<Record<Source, number>>
lag?: number
after?: string
/** A predicate's other answer. Below `DEFER` it is what the evidence supports, not a deferral. */
otherwise?: string
}
/** The program, in the order the figure lists it: each dependent under its parent. */
export const DECISIONS: readonly Decision[] = [
{ id: 'fault', value: 'bearing', full: 0.91, weights: { can: 0.2, audio: 0.16, camera: 0.05, sysml: 0.03 } },
{ id: 'readiness', value: 'no-go', full: 0.96, after: 'fault', weights: { can: 0.12, camera: 0.06, sysml: 0.05, sim: 0.06 } },
{ id: 'action', value: 'inspect-bearing', full: 0.83, lag: 0.3, after: 'fault', weights: { can: 0.12, audio: 0.12 } },
{ id: 'next_test', value: 'spectrum', full: 0.88, lag: 0.3, after: 'fault', weights: { audio: 0.14, can: 0.1 } },
{ id: 'severity', value: 'critical', full: 0.86, weights: { can: 0.15, audio: 0.1, sim: 0.08 } },
{ id: 'human_review', value: 'yes', full: 0.99, after: 'severity', weights: {} },
{ id: 'subsystem', value: 'drivetrain', full: 0.94, weights: { sysml: 0.25, can: 0.1 } },
{ id: 'evidence_enough', value: 'true', otherwise: 'false', full: 0.81, weights: { can: 0.1, audio: 0.08, camera: 0.06, sysml: 0.05, sim: 0.05 } },
{ id: 'thermal_margin', value: 'low', full: 0.87, weights: { sim: 0.2, can: 0.06 } },
{ id: 'mission_range', value: 'met', full: 0.92, weights: { sim: 0.25, sysml: 0.15 } },
{ id: 'visible_damage', value: 'none', full: 0.89, weights: { camera: 0.4 } },
{ id: 'operator_report', value: 'grinding', full: 0.84, weights: { audio: 0.45 } },
]
/** A first-pass confidence at or above this freezes; below it, the node is refined. */
export const FREEZE = 0.8
/** A final confidence below this is not a decision. The node defers. */
export const DEFER = 0.5
export type On = Record<Source, boolean>
export const ALL: On = Object.fromEntries(SOURCES.map((s) => [s.id, true])) as On
const round = (p: number) => Math.round(Math.min(0.99, Math.max(0.05, p)) * 100) / 100
/** Confidence once the program has settled, given which sources are present. */
export function final(d: Decision, on: On): number {
let p = d.full
for (const [s, w] of Object.entries(d.weights) as [Source, number][]) if (!on[s]) p -= w
return round(p)
}
/** Confidence on the first pass: a dependent sits below its settled value. */
export function draft(d: Decision, on: On): number {
return round(final(d, on) - (d.lag ?? 0))
}
/** The source whose absence costs this decision the most — what a DEFER asks for. */
export function needs(d: Decision, on: On): Source | null {
let best: Source | null = null
let most = 0
for (const [s, w] of Object.entries(d.weights) as [Source, number][]) {
if (!on[s] && w > most) {
most = w
best = s
}
}
return best
}
/** The decisions a source reaches, and every decision that depends on one of them. */
export function reach(source: Source): string[] {
const hit = new Set(DECISIONS.filter((d) => (d.weights[source] ?? 0) > 0).map((d) => d.id))
for (const d of DECISIONS) if (d.after && hit.has(d.after)) hit.add(d.id)
return DECISIONS.filter((d) => hit.has(d.id)).map((d) => d.id)
}
/** What the node says once settled: its value and confidence, or the evidence it defers for. */
export function answer(d: Decision, on: On): { value: string; p: number; defer: Source | null } {
const p = final(d, on)
if (p >= DEFER) return { value: d.value, p, defer: null }
if (d.otherwise) return { value: d.otherwise, p: round(1 - p), defer: null }
return { value: 'defer', p, defer: needs(d, on) }
}
/** `.91`, the way the copy writes a probability. */
export const prob = (p: number) => p.toFixed(2).replace(/^0/, '')