Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bg6jx9vHNHzB91qyV64GQ7
289 lines
23 KiB
JavaScript
289 lines
23 KiB
JavaScript
import { writeFileSync } from 'node:fs'
|
||
|
||
const C = {
|
||
ink: '#1B2331', ink2: '#4A5568', ink3: '#7A8494', line: '#B9C2CE',
|
||
teal: '#2C6B70', tealSoft: '#E4F0F0', amber: '#B86A14', amberSoft: '#F7ECDD',
|
||
slate: '#EEF1F5', red: '#A23B3B', redSoft: '#F6E3E3', white: '#FFFFFF',
|
||
}
|
||
const FONT = 'font-family="Helvetica Neue,Helvetica,Arial,sans-serif"'
|
||
const esc = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
|
||
function text(x, y, s, { size = 13, fill = C.ink, anchor = 'middle', weight = 400, mono = false, italic = false } = {}) {
|
||
const fam = mono ? 'font-family="Menlo,SFMono-Regular,Consolas,monospace"' : ''
|
||
return `<text x="${x}" y="${y}" ${fam} font-size="${size}" fill="${fill}" text-anchor="${anchor}" font-weight="${weight}"${italic ? ' font-style="italic"' : ''}>${esc(s)}</text>`
|
||
}
|
||
// title + sub lines centered in box
|
||
function box(x, y, w, h, title, sub = [], o = {}) {
|
||
const { fill = C.white, stroke = C.line, sw = 1.2, dash = '', tcolor = C.ink, tsize = 14, ssize = 12, scolor = C.ink2, r = 5, tag = null, tagColor = C.amber, mono = false } = o
|
||
const lines = 1 + sub.length
|
||
const lh = 16
|
||
const total = tsize + 4 + sub.length * lh
|
||
let cy = y + h / 2 - total / 2 + tsize - 2
|
||
let s = `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="${r}" fill="${fill}" stroke="${stroke}" stroke-width="${sw}"${dash ? ` stroke-dasharray="${dash}"` : ''}/>`
|
||
s += text(x + w / 2, cy, title, { size: tsize, fill: tcolor, weight: 600, mono })
|
||
for (const l of sub) { cy += lh; s += text(x + w / 2, cy, l, { size: ssize, fill: scolor }) }
|
||
if (tag) {
|
||
const tw = tag.length * 7 + 14
|
||
s += `<rect x="${x + w - tw - 6}" y="${y - 9}" width="${tw}" height="18" rx="9" fill="${tagColor}"/>` + text(x + w - tw / 2 - 6, y + 4, tag, { size: 10, fill: C.white, weight: 700 })
|
||
}
|
||
return s
|
||
}
|
||
function group(x, y, w, h, label, o = {}) {
|
||
const { stroke = C.line, fill = 'none', color = C.ink3 } = o
|
||
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="8" fill="${fill}" stroke="${stroke}" stroke-width="1" stroke-dasharray="5 4"/>` +
|
||
text(x + 12, y + 18, label.toUpperCase(), { size: 10.5, fill: color, anchor: 'start', weight: 700 })
|
||
}
|
||
function arrow(x1, y1, x2, y2, label = '', o = {}) {
|
||
const { dashed = false, both = false, color = C.ink2, lx = 0, ly = -6, path = null, lsize = 11.5, lfill = C.ink2 } = o
|
||
const d = path || `M${x1},${y1} L${x2},${y2}`
|
||
let s = `<path d="${d}" fill="none" stroke="${color}" stroke-width="1.4"${dashed ? ' stroke-dasharray="5 4"' : ''} marker-end="url(#ah)"${both ? ' marker-start="url(#ahs)"' : ''}/>`
|
||
if (label) {
|
||
const mx = (x1 + x2) / 2 + lx, my = (y1 + y2) / 2 + ly
|
||
const tw = label.length * 6.4 + 8
|
||
s += `<rect x="${mx - tw / 2}" y="${my - 10}" width="${tw}" height="15" fill="${C.white}" opacity=".92"/>` + text(mx, my + 2, label, { size: lsize, fill: lfill })
|
||
}
|
||
return s
|
||
}
|
||
function svg(w, h, body) {
|
||
return `<!doctype html><body style="margin:0;background:#fff"><svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" ${FONT}>
|
||
<defs>
|
||
<marker id="ah" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto" markerUnits="userSpaceOnUse"><path d="M0,1 L9,5 L0,9 z" fill="${C.ink2}"/></marker>
|
||
<marker id="ahs" markerWidth="10" markerHeight="10" refX="1" refY="5" orient="auto" markerUnits="userSpaceOnUse"><path d="M10,1 L1,5 L10,9 z" fill="${C.ink2}"/></marker>
|
||
</defs>
|
||
<rect width="${w}" height="${h}" fill="#fff"/>
|
||
${body}
|
||
</svg></body>`
|
||
}
|
||
const legend = (x, y, items) => items.map(([fill, stroke, lbl], i) =>
|
||
`<rect x="${x + i * 150}" y="${y - 9}" width="14" height="12" rx="2" fill="${fill}" stroke="${stroke}"/>` + text(x + i * 150 + 20, y + 1, lbl, { size: 11, fill: C.ink2, anchor: 'start' })).join('')
|
||
|
||
const out = {}
|
||
|
||
// ---------- 1. system context ----------
|
||
{
|
||
let b = ''
|
||
b += group(30, 40, 250, 420, 'External systems')
|
||
const ext = [['Trading platform', ['bids · clearing · disclosures']], ['Dispatch / load mgmt', ['regulation requests']], ['Metering / settlement', ['meter data · statements']], ['Weather & market data', ['read-only feeds']]]
|
||
ext.forEach(([t, s], i) => { b += box(50, 70 + i * 95, 210, 70, t, s, { fill: C.slate }) })
|
||
|
||
b += group(330, 40, 600, 490, 'VPP AI Platform')
|
||
b += box(360, 70, 540, 90, 'Cognitive plane', ['five LLM agents · agent runtime · workflows', 'minutes to days'], { fill: C.amberSoft, stroke: C.amber, tag: 'LLM' })
|
||
b += box(360, 215, 540, 90, 'Safety chain', ['rule check → simulation → envelope / human approval → fresh check → permit', 'deterministic · the only path between planes'], { fill: C.tealSoft, stroke: C.teal, tag: 'NO LLM', tagColor: C.teal })
|
||
b += box(360, 360, 540, 80, 'Control plane', ['execution engine · edge terminals · seconds'], { fill: C.tealSoft, stroke: C.teal, tag: 'NO LLM', tagColor: C.teal })
|
||
b += box(360, 465, 540, 45, 'Data foundation', [], { fill: C.slate, tsize: 13 })
|
||
b += text(630, 500, 'time-series · relational · knowledge/vector · feature store · event log', { size: 11.5, fill: C.ink2 })
|
||
b += arrow(630, 160, 630, 215, 'Proposal (digest + lineage)', { lx: 110 })
|
||
b += arrow(630, 305, 630, 360, '(Proposal, ExecutionPermit)', { lx: 110 })
|
||
b += arrow(280, 115, 360, 115, '', { both: true })
|
||
b += arrow(360, 260, 280, 100, '', { path: 'M360,260 L300,260 L300,130 L280,130' })
|
||
b += text(292, 253, 'bid + permit', { size: 10.5, fill: C.ink2, anchor: 'end' })
|
||
b += arrow(900, 400, 980, 400, '')
|
||
b += arrow(980, 125, 900, 125, '', { both: true })
|
||
b += box(980, 90, 190, 70, 'Operators', ['Case Desk · approvals', 'scenario forks · AI cards'], { fill: C.white })
|
||
b += box(980, 360, 190, 80, 'Flexible resources', ['PV · storage · EV charging', 'HVAC · industrial · microgrids'], { fill: C.white })
|
||
b += arrow(630, 440, 630, 465)
|
||
b += arrow(360, 400, 360, 140, '', { path: 'M360,400 L345,400 L345,140 L360,140', dashed: true })
|
||
b += `<text transform="translate(340,330) rotate(-90)" font-size="10.5" fill="${C.ink2}" text-anchor="middle">execution feedback</text>`
|
||
b += text(1075, 470, '31 MW today → ≥ 80 MW target', { size: 11, fill: C.ink3 })
|
||
out['01-system-context'] = svg(1200, 550, b)
|
||
}
|
||
|
||
// ---------- 2. agent runtime ----------
|
||
{
|
||
let b = ''
|
||
b += group(30, 40, 250, 330, 'Task triggers')
|
||
b += box(50, 70, 210, 70, 'Scheduled', ['day-ahead 06:00 · bid window · D+1 review'], { fill: C.tealSoft, stroke: C.teal })
|
||
b += box(50, 165, 210, 70, 'Event', ['deviation · resource offline · clearing'], { fill: C.tealSoft, stroke: C.teal })
|
||
b += box(50, 260, 210, 70, 'Manual', ['operator question or task'], { fill: C.slate })
|
||
b += box(330, 260, 190, 70, 'Router agent', ['intent → template id + params', 'schema-constrained enum'], { fill: C.amberSoft, stroke: C.amber, tag: 'LLM' })
|
||
b += arrow(260, 295, 330, 295)
|
||
b += box(580, 120, 240, 130, 'Workflow engine', ['deterministic step chains', 'durable · suspend/resume', 'one template per business process'], { fill: C.tealSoft, stroke: C.teal, tag: 'NO LLM', tagColor: C.teal })
|
||
b += arrow(260, 105, 580, 160, 'no LLM in the path', { path: 'M260,105 L420,105 L420,160 L580,160', lx: 110, ly: -37 })
|
||
b += arrow(260, 200, 580, 200, 'rule-matched', { lx: -20 })
|
||
b += arrow(520, 295, 580, 240, '', { path: 'M520,295 L560,295 L560,240 L580,240' })
|
||
b += box(880, 70, 260, 80, 'Agent step', ['one of five agents, invoked at a step', 'tools whitelisted per agent'], { fill: C.amberSoft, stroke: C.amber, tag: 'LLM' })
|
||
b += arrow(820, 150, 880, 110, '', { path: 'M820,150 L850,150 L850,110 L880,110' })
|
||
b += box(880, 190, 260, 80, 'Tool registry', ['typed, versioned skill contracts', 'every call snapshotted into lineage'], { fill: C.tealSoft, stroke: C.teal })
|
||
b += arrow(1010, 150, 1010, 190)
|
||
b += box(880, 310, 260, 60, 'Python skill services', ['forecast · MILP · simulation'], { fill: C.slate })
|
||
b += arrow(1010, 270, 1010, 310, 'HTTP / JSON', { lx: 60 })
|
||
b += box(580, 300, 240, 70, 'Proposal → safety chain', ['numbers dereferenced from lineage'], { fill: C.white, stroke: C.ink2 })
|
||
b += arrow(700, 250, 700, 300)
|
||
b += box(330, 400, 810, 46, 'Event log (event-sourced): triggers, steps, tool calls, transitions, approvals, permits, receipts', [], { fill: C.slate, tsize: 12.5, dash: '' })
|
||
b += arrow(700, 370, 700, 400, '', { dashed: true })
|
||
b += legend(30, 445, [[C.amberSoft, C.amber, 'LLM involved'], [C.tealSoft, C.teal, 'Deterministic']])
|
||
b += text(30, 480, 'Workflows are the entry point, not agents. The LLM decides what to do (pick a template); the engine decides how, step by step.', { size: 12, fill: C.ink2, anchor: 'start', italic: true })
|
||
out['02-agent-runtime'] = svg(1200, 500, b)
|
||
}
|
||
|
||
// ---------- 3. context & memory ----------
|
||
{
|
||
let b = ''
|
||
b += group(30, 40, 1140, 275, 'Context assembly for one agent step · deterministic baseline + progressive disclosure')
|
||
const src = [['Position ledger', 'versioned read'], ['Time-series / snapshots', 'immutable, checksummed'], ['Semantic memory', 'lessons declared by template'], ['Knowledge base (RAG)', 'effective rules only']]
|
||
src.forEach(([t, s], i) => { b += box(50 + i * 180, 70, 165, 60, t, [s], { fill: C.slate, tsize: 12.5, ssize: 11 }) })
|
||
b += box(50, 170, 705, 55, 'fetchContext step (deterministic, reproducible)', ['compact baseline: summaries, identifiers, the few figures a step needs'], { fill: C.tealSoft, stroke: C.teal, tsize: 13 })
|
||
b += box(50, 240, 705, 55, 'Agentic retrieval tools · read-only · whitelisted · every call logged into lineage', ['rule retrieval · ledger read · snapshot fetch · forecast lookup — detail disclosed on demand, retrieved content tagged as data'], { fill: C.white, stroke: C.teal, tsize: 12.5, ssize: 11 })
|
||
b += arrow(755, 267, 790, 267, '', { both: true })
|
||
for (let i = 0; i < 4; i++) b += arrow(132 + i * 180, 130, 132 + i * 180, 170)
|
||
b += box(790, 70, 170, 225, 'Agent step', ['prose + tool calls', 'numbers only as', '{toolCallId, path}', 'references'], { fill: C.amberSoft, stroke: C.amber, tag: 'LLM' })
|
||
b += arrow(755, 197, 790, 197)
|
||
b += box(995, 70, 160, 225, 'Assembler', ['dereferences refs', 'from recorded lineage', '→ Proposal', 'LLM text cannot', 'carry a number'], { fill: C.tealSoft, stroke: C.teal })
|
||
b += arrow(960, 147, 995, 147)
|
||
|
||
b += group(30, 345, 1140, 165, 'Three memory layers')
|
||
b += box(50, 380, 355, 105, 'Working memory', ['current task context, intermediate results', 'runtime memory · task lifetime', 'step-to-step parameters preferred over memory'], { fill: C.white })
|
||
b += box(423, 380, 355, 105, 'Episodic memory', ['operating states, trading, control, responses', 'event log + time-series · permanent (audit)', 'queried through fetchContext, not recalled'], { fill: C.white })
|
||
b += box(796, 380, 355, 105, 'Semantic memory', ['structured lessons from D+1 review', 'knowledge + strategy store · versioned', 'written by ReviewFinding, injected explicitly'], { fill: C.white })
|
||
b += text(30, 540, 'Authoritative facts live in the domain database, never in agent memory (invariant I8). No automatic memory recall feeds production decisions.', { size: 12, fill: C.ink2, anchor: 'start', italic: true })
|
||
out['03-context-memory'] = svg(1200, 560, b)
|
||
}
|
||
|
||
// ---------- 4. safety chain state machine ----------
|
||
{
|
||
let b = ''
|
||
const Y = 120, H = 56, W = 118, G = 32
|
||
const stages = [
|
||
['DRAFT', 'agent output'], ['RULE_CHECK', 'policy pack'], ['SIMULATION', 'backtest / balance'], ['ENVELOPE_CHECK', 'within bounds?'],
|
||
['APPROVED', 'auto or human'], ['FRESH_CHECK', 're-verify state'], ['AUTHORIZED', 'permit issued'], ['RELEASED', 'gateway accepts'],
|
||
]
|
||
const xs = stages.map((_, i) => 30 + i * (W + G))
|
||
stages.forEach(([t, s], i) => {
|
||
const hi = i >= 1 && i <= 6
|
||
b += box(xs[i], Y, W, H, t, [s], { fill: hi ? C.tealSoft : C.white, stroke: hi ? C.teal : C.line, tsize: 12, ssize: 10.5, mono: true })
|
||
if (i < stages.length - 1) b += arrow(xs[i] + W, Y + H / 2, xs[i + 1], Y + H / 2)
|
||
})
|
||
b += group(xs[1] - 12, 52, xs[6] + W + 12 - xs[1] + 12, 148, 'Safety chain · deterministic · shared by every proposal type', { stroke: C.teal, color: C.teal })
|
||
// human branch
|
||
b += box(xs[3] + 20, 250, 200, 56, 'PENDING_HUMAN', ['suspended run · Case Desk inbox'], { fill: C.amberSoft, stroke: C.amber, tsize: 12, ssize: 10.5, mono: true })
|
||
b += arrow(xs[3] + W / 2, Y + H, xs[3] + 60, 250, 'outside envelope', { path: `M${xs[3] + W / 2},${Y + H} L${xs[3] + W / 2},250`, lx: -70, ly: 8 })
|
||
b += arrow(xs[2] + W / 2, Y + H, xs[3] + 20, 278, 'sim warning', { path: `M${xs[2] + W / 2},${Y + H} L${xs[2] + W / 2},278 L${xs[3] + 20},278`, lx: -45, ly: 40 })
|
||
b += arrow(xs[3] + 220, 278, xs[4] + W / 2, Y + H, 'human resume', { path: `M${xs[3] + 220},278 L${xs[4] + W / 2},278 L${xs[4] + W / 2},${Y + H}`, lx: 40, ly: 40 })
|
||
b += text(xs[4] + W / 2 + 16, 300, 'approver ≠ originator (I1)', { size: 10.5, fill: C.ink3, anchor: 'start' })
|
||
b += arrow(xs[3] + W / 2 + 30, Y, xs[4] + W / 2 - 30, Y, 'within envelope → AUTO_APPROVED', { path: `M${xs[3] + W - 10},${Y} L${xs[3] + W - 10},${Y - 24} L${xs[4] + 10},${Y - 24} L${xs[4] + 10},${Y}`, ly: -26, lx: 0 })
|
||
// rejected
|
||
b += box(xs[1], 350, W, 44, 'REJECTED', [], { fill: C.redSoft, stroke: C.red, tsize: 12, mono: true })
|
||
b += arrow(xs[1] + W / 2, Y + H, xs[1] + W / 2, 350, 'rule failure', { lx: 44 })
|
||
b += arrow(xs[3] + 60, 306, xs[1] + W, 372, 'human rejects', { path: `M${xs[3] + 60},306 L${xs[3] + 60},372 L${xs[1] + W},372`, lx: -30, ly: 45 })
|
||
// stale
|
||
b += box(xs[5], 350, W, 44, 'STALE', [], { fill: C.redSoft, stroke: C.red, tsize: 12, mono: true })
|
||
b += arrow(xs[5] + W / 2, Y + H, xs[5] + W / 2, 350, 'evidence changed', { lx: 60 })
|
||
b += arrow(xs[5] + W / 2, 350, xs[1] + W - 15, Y + H, 're-enter chain', { path: `M${xs[5] + W / 2 - 30},350 L${xs[5] + W / 2 - 30},330 L${xs[1] + W - 15},330 L${xs[1] + W - 15},${Y + H + 2}`, dashed: true, lx: -172, ly: 59 })
|
||
// after release
|
||
b += text(xs[7] + W / 2, 210, 'EXECUTING → COMPLETED', { size: 11, fill: C.ink2, mono: true })
|
||
b += text(xs[7] + W / 2, 226, 'or ROLLED_BACK', { size: 11, fill: C.ink2, mono: true })
|
||
b += text(xs[7] + W / 2, 244, '→ ExecutionReport → review', { size: 10.5, fill: C.ink3 })
|
||
// notes
|
||
b += text(30, 430, 'Approval binds to the proposal digest and a validity window (I3). Gateways accept only (Proposal, ExecutionPermit): short-lived, revocable, digest-matched (I4).', { size: 12, fill: C.ink2, anchor: 'start' })
|
||
b += text(30, 452, 'Simulation warnings always escalate to a human. No code path lets the AI reach APPROVED. Envelopes start empty and widen only on shadow-run evidence.', { size: 12, fill: C.ink2, anchor: 'start' })
|
||
out['04-safety-chain'] = svg(1290, 475, b)
|
||
}
|
||
|
||
// ---------- 5. tech stack ----------
|
||
{
|
||
let b = ''
|
||
const row = (y, h, label, items, o = {}) => {
|
||
b += text(30, y + h / 2 + 4, label, { size: 11, fill: C.ink3, anchor: 'start', weight: 700 })
|
||
const x0 = 190, W = 980, n = items.length, g = 12, w = (W - g * (n - 1)) / n
|
||
items.forEach(([t, s], i) => { b += box(x0 + i * (w + g), y, w, h, t, s, { fill: o.fill || C.white, stroke: o.stroke || C.line, tsize: 13, ssize: 11, tag: o.tag && i === 0 ? o.tag : null, tagColor: o.tagColor }) })
|
||
}
|
||
row(40, 64, 'OPERATOR UI', [['Case Desk', ['approval inbox · case view · lineage expansion']], ['AI insight cards', ['read-only projections in existing business pages']]])
|
||
row(120, 80, 'RUNTIME', [['TypeScript + Mastra', ['workflows (durable suspend/resume)', 'agents as controlled steps · tool registry']], ['Trigger service', ['cron · event bus consumers', 'router agent for manual requests']], ['LLM port', ['capability interface + model router', 'LLM-down mode for degraded runs']]], { fill: C.amberSoft, stroke: C.amber })
|
||
row(226, 80, 'SERVICES', [['Policy engine', ['versioned, tested policy packs']], ['Envelope + authority', ['envelope match · fresh check · permits']], ['Ledger + lineage', ['position cascade · snapshots · assembler']], ['Gateways + Case Desk', ['file export · simulation · cases']]], { fill: C.tealSoft, stroke: C.teal, tag: 'NO LLM', tagColor: C.teal })
|
||
row(326, 64, 'CONTRACTS', [['Domain schemas (zod)', ['single source of truth']], ['JSON Schema', ['exported, committed, versioned']], ['Golden fixtures', ['validated by both CIs']]])
|
||
row(406, 72, 'SKILLS', [['Python services (FastAPI)', ['load / PV / price forecasts with quantiles', 'bid MILP · dispatch MILP · potential · report']], ['Generated pydantic models', ['never hand-edited']]], { fill: C.slate })
|
||
row(494, 72, 'DATA', [['PostgreSQL + pgvector', ['relational · knowledge · outbox events']], ['TimescaleDB / IoTDB', ['telemetry curves']], ['Object storage', ['snapshots · archives']], ['Kafka (phase 2)', ['outbox relay, same interface']]])
|
||
row(582, 72, 'LLM BACKENDS', [['Commercial API', ['development']], ['Local open model', ['pre-production']], ['GuangMing Power LLM', ['production, on-prem adapter']]], { fill: C.amberSoft, stroke: C.amber })
|
||
b += text(30, 688, 'Backend swaps are gated by the L1 evaluation set and an L4 shadow comparison, not judgment. About a third of the platform is framework; the differentiating parts are the deterministic services.', { size: 12, fill: C.ink2, anchor: 'start', italic: true })
|
||
out['05-tech-stack'] = svg(1200, 706, b)
|
||
}
|
||
|
||
// ---------- 6. interfaces & contracts ----------
|
||
{
|
||
let b = ''
|
||
b += group(30, 40, 560, 260, 'Contract pipeline (one source of truth)')
|
||
b += box(55, 75, 220, 60, 'zod schemas', ['packages/domain · hand-written'], { fill: C.tealSoft, stroke: C.teal, mono: true, tsize: 13 })
|
||
b += box(55, 165, 220, 60, 'JSON Schema', ['contracts/ · committed · versioned'], { fill: C.slate, mono: true, tsize: 13 })
|
||
b += arrow(165, 135, 165, 165, 'build export', { lx: 60 })
|
||
b += box(340, 165, 225, 60, 'pydantic models', ['generated · never hand-edited'], { fill: C.slate, mono: true, tsize: 13 })
|
||
b += arrow(275, 195, 340, 195, 'codegen', { ly: -8 })
|
||
b += box(340, 75, 225, 60, 'Golden fixtures', ['valid + invalid samples per object'], { fill: C.white })
|
||
b += arrow(275, 105, 340, 105, '', {})
|
||
b += arrow(452, 135, 452, 165, '', { dashed: true })
|
||
b += box(55, 245, 510, 40, 'TS CI and Python CI both validate every fixture; any disagreement fails the build', [], { fill: C.white, stroke: C.ink2, tsize: 12 })
|
||
b += arrow(165, 225, 165, 245, '', { dashed: true })
|
||
b += arrow(452, 225, 452, 245, '', { dashed: true })
|
||
|
||
b += group(620, 40, 550, 260, 'Ports = trust boundaries (deterministic, not exposed to the LLM)')
|
||
const ports = [
|
||
['ProposalSubmissionPort', 'the only way into the safety chain'],
|
||
['PolicyEnginePort · SimulationPort', 'rule check and simulation'],
|
||
['EnvelopePort · AuthorityPort', 'envelope match · fresh check · permits · revoke'],
|
||
['GatewayPort', 'accepts (Proposal, ExecutionPermit) only'],
|
||
['LedgerPort · EvidencePort', 'versioned ledger · event-sourced evidence'],
|
||
['CaseDeskPort', 'open · read · apply command · watch'],
|
||
]
|
||
ports.forEach(([p, d], i) => {
|
||
const y = 78 + i * 36
|
||
b += text(640, y + 12, p, { size: 12, fill: C.teal, anchor: 'start', mono: true, weight: 600 })
|
||
b += text(1150, y + 12, d, { size: 11.5, fill: C.ink2, anchor: 'end' })
|
||
if (i < ports.length - 1) b += `<line x1="640" y1="${y + 24}" x2="1150" y2="${y + 24}" stroke="${C.slate}"/>`
|
||
})
|
||
|
||
b += group(30, 330, 1140, 150, 'Cross-process interfaces and data representation rules')
|
||
const ifs = [['TS runtime → Python skills', 'HTTP/JSON, schema-validated both ends'], ['Case Desk / front end → runtime', 'HTTP/JSON approvals and case API'], ['External systems', 'anti-corruption adapters, raw + translated logged'], ['Edge control link', 'grid protocol stack, outside the JSON domain']]
|
||
ifs.forEach(([t, s], i) => { b += box(50 + i * 280, 360, 265, 56, t, [s], { fill: C.white, tsize: 12.5, ssize: 11 }) })
|
||
b += text(50, 448, 'Money, energy and prices as fixed-point decimal strings · units in field names (power_mw, price_yuan_per_mwh) · ISO 8601 UTC · intervals as {date, interval_index}', { size: 11.5, fill: C.ink2, anchor: 'start' })
|
||
b += text(50, 466, 'all IDs strings · enums UPPER_SNAKE · breaking schema changes bump the major version and ship a migration note', { size: 11.5, fill: C.ink2, anchor: 'start' })
|
||
out['06-contracts'] = svg(1200, 500, b)
|
||
}
|
||
|
||
// ---------- 7. eval-driven development ----------
|
||
{
|
||
let b = ''
|
||
b += group(30, 40, 470, 345, 'Four evaluation layers')
|
||
const layers = [
|
||
['L4', 'End-to-end decision quality', 'shadow run · historical backtest · counterfactual attribution'],
|
||
['L3', 'Safety chain correctness', 'policy pack tests · invariant tests I1–I8 · red-team cases'],
|
||
['L2', 'Professional-model skills', 'MAPE · interval coverage · solver quality vs. bounds'],
|
||
['L1', 'LLM tasks', 'routing accuracy · schema compliance · numeric consistency'],
|
||
]
|
||
layers.forEach(([l, t, s], i) => {
|
||
const y = 70 + i * 72
|
||
b += `<rect x="50" y="${y}" width="430" height="60" rx="5" fill="${i === 0 ? C.tealSoft : C.white}" stroke="${i === 0 ? C.teal : C.line}"/>`
|
||
b += text(72, y + 36, l, { size: 16, fill: C.teal, weight: 700, mono: true })
|
||
b += text(105, y + 26, t, { size: 13, fill: C.ink, anchor: 'start', weight: 600 })
|
||
b += text(105, y + 45, s, { size: 11, fill: C.ink2, anchor: 'start' })
|
||
})
|
||
b += text(50, 370, 'A problem at L4 is localised to the layer that caused it.', { size: 11.5, fill: C.ink3, anchor: 'start', italic: true })
|
||
|
||
b += group(530, 40, 640, 345, 'Change gates: what must pass before a change ships')
|
||
const gates = [
|
||
['Prompt / agent instructions', 'L1 task set + L4 fixed-day backtest, no regression'],
|
||
['LLM backend switch', 'full L1 set + L4 shadow comparison vs. baseline'],
|
||
['Skill model upgrade', 'all L2 metrics for that skill + downstream L4 backtest'],
|
||
['Policy pack upgrade', 'L3 pack tests green + impact analysis on pending proposals'],
|
||
['Contract schema change', 'golden fixtures on both sides + compatibility check'],
|
||
['Envelope widening', 'L4 shadow / online evidence only — no data, no widening'],
|
||
]
|
||
gates.forEach(([c, g], i) => {
|
||
const y = 78 + i * 46
|
||
b += text(550, y + 14, c, { size: 12.5, fill: C.ink, anchor: 'start', weight: 600 })
|
||
b += text(550, y + 31, g, { size: 11.5, fill: i === 5 ? C.amber : C.ink2, anchor: 'start' })
|
||
if (i < gates.length - 1) b += `<line x1="550" y1="${y + 40}" x2="1150" y2="${y + 40}" stroke="${C.slate}"/>`
|
||
})
|
||
|
||
// loop
|
||
b += group(30, 410, 1140, 130, 'The audit trail is the evaluation dataset')
|
||
const loop = [['Production run', ['event log + immutable snapshots']], ['Replay datasets', ['representative days, frozen']], ['Eval run (archived)', ['committed baseline, CI --check']], ['ReviewFinding', ['D+1 attribution']], ['New eval cases', ['every finding → candidate case']]]
|
||
loop.forEach(([t, s], i) => {
|
||
b += box(50 + i * 228, 445, 200, 60, t, s, { fill: i === 2 ? C.tealSoft : C.white, stroke: i === 2 ? C.teal : C.line, tsize: 12.5, ssize: 11 })
|
||
if (i < loop.length - 1) b += arrow(250 + i * 228, 475, 278 + i * 228, 475)
|
||
})
|
||
b += arrow(1062, 505, 150, 505, '', { path: 'M1062,505 L1062,525 L150,525 L150,506', dashed: true })
|
||
out['07-eval-driven'] = svg(1200, 560, b)
|
||
}
|
||
|
||
for (const [name, html] of Object.entries(out)) writeFileSync(`${process.argv[2]}/${name}.html`, html)
|
||
console.log(Object.keys(out).join('\n'))
|