Reference implementation for the Phoenix Architecture. Work in progress. aicoding.leaflet.pub/
ai coding crazy
at main 1328 lines 61 kB view raw
1#!/usr/bin/env npx tsx 2/** 3 * Phoenix VCS — Walkthrough Demo 4 * 5 * Shows you every file, every data structure, every transformation. 6 * You see what Phoenix sees. 7 */ 8 9// ── Colors ── 10const R = '\x1b[0m'; // reset 11const B = '\x1b[1m'; // bold 12const D = '\x1b[2m'; // dim 13const GR = '\x1b[32m'; // green 14const YL = '\x1b[33m'; // yellow 15const RD = '\x1b[31m'; // red 16const CY = '\x1b[36m'; // cyan 17const MG = '\x1b[35m'; // magenta 18const BL = '\x1b[34m'; // blue 19const WH = '\x1b[37m'; 20const BG_GR = '\x1b[42m'; 21const BG_RD = '\x1b[41m'; 22const BG_YL = '\x1b[43m'; 23const BG_BL = '\x1b[44m'; 24const BG_MG = '\x1b[45m'; 25const BG_CY = '\x1b[46m'; 26 27function banner(step: number, text: string) { 28 const line = '━'.repeat(70); 29 console.log(`\n${CY}${line}${R}`); 30 console.log(`${CY}${R} ${B}STEP ${step}${R}${B}${text}${R}`); 31 console.log(`${CY}${line}${R}`); 32} 33 34function sub(text: string) { 35 console.log(`\n ${B}${MG}${text}${R}\n`); 36} 37 38function showFile(filename: string, content: string, highlight?: Map<number, string>) { 39 console.log(` ${BG_BL}${WH}${B} 📄 ${filename} ${R}\n`); 40 const lines = content.split('\n'); 41 for (let i = 0; i < lines.length; i++) { 42 const lineNum = String(i + 1).padStart(3); 43 const hl = highlight?.get(i + 1); 44 if (hl) { 45 console.log(` ${D}${lineNum}${R} ${hl}${lines[i]}${R}`); 46 } else { 47 console.log(` ${D}${lineNum}${R} ${lines[i]}`); 48 } 49 } 50 console.log(''); 51} 52 53function showJSON(label: string, obj: unknown) { 54 console.log(` ${BG_MG}${WH}${B} 💾 ${label} ${R}\n`); 55 const json = JSON.stringify(obj, null, 2); 56 for (const line of json.split('\n')) { 57 console.log(` ${D}${R} ${line}`); 58 } 59 console.log(''); 60} 61 62function showBox(lines: string[]) { 63 const maxLen = Math.max(...lines.map(l => stripAnsi(l).length)); 64 const top = `${'─'.repeat(maxLen + 2)}`; 65 const bot = `${'─'.repeat(maxLen + 2)}`; 66 console.log(top); 67 for (const line of lines) { 68 const pad = ' '.repeat(maxLen - stripAnsi(line).length); 69 console.log(`${line}${pad}`); 70 } 71 console.log(bot); 72} 73 74function stripAnsi(s: string): string { 75 return s.replace(/\x1b\[[0-9;]*m/g, ''); 76} 77 78function badge(text: string, bg: string) { 79 return `${bg}${WH}${B} ${text} ${R}`; 80} 81 82function arrow(from: string, to: string, label?: string) { 83 const lbl = label ? ` ${D}(${label})${R}` : ''; 84 console.log(` ${CY}${from}${R} ──${lbl}──▸ ${GR}${to}${R}`); 85} 86 87function wait(ms: number): Promise<void> { 88 return new Promise(resolve => setTimeout(resolve, ms)); 89} 90 91// ── Imports ── 92import { parseSpec } from './src/spec-parser.js'; 93import { normalizeText } from './src/normalizer.js'; 94import { clauseSemhash, contextSemhashCold } from './src/semhash.js'; 95import { diffClauses } from './src/diff.js'; 96import { extractCanonicalNodes } from './src/canonicalizer.js'; 97import { computeWarmHashes } from './src/warm-hasher.js'; 98import { classifyChanges } from './src/classifier.js'; 99import { DRateTracker } from './src/d-rate.js'; 100import { BootstrapStateMachine } from './src/bootstrap.js'; 101import { ChangeClass, BootstrapState, DRateLevel } from './src/models/classification.js'; 102import { DiffType } from './src/models/clause.js'; 103import { planIUs } from './src/iu-planner.js'; 104import { generateIU } from './src/regen.js'; 105import { extractDependencies } from './src/dep-extractor.js'; 106import { validateBoundary, detectBoundaryChanges } from './src/boundary-validator.js'; 107import { detectDrift } from './src/drift.js'; 108import { DriftStatus } from './src/models/manifest.js'; 109import type { GeneratedManifest } from './src/models/manifest.js'; 110import { sha256 } from './src/semhash.js'; 111import { evaluatePolicy } from './src/policy-engine.js'; 112import { computeCascade } from './src/cascade.js'; 113import { EvidenceKind, EvidenceStatus } from './src/models/evidence.js'; 114import type { EvidenceRecord } from './src/models/evidence.js'; 115import { runShadowPipeline } from './src/shadow-pipeline.js'; 116import { UpgradeClassification } from './src/models/pipeline.js'; 117import { runCompaction } from './src/compaction.js'; 118import { parseCommand, routeCommand } from './src/bot-router.js'; 119import type { BotCommand } from './src/models/bot.js'; 120import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs'; 121import { join } from 'node:path'; 122import { tmpdir } from 'node:os'; 123 124// ── The Spec File ── 125 126const SPEC_V1 = `# Authentication Service 127 128The authentication service handles user login, registration, and session management. 129 130## Requirements 131 132- Users must authenticate with email and password 133- Sessions expire after 24 hours 134- Failed login attempts are rate-limited to 5 per minute 135- Passwords must be hashed with bcrypt (cost factor 12) 136 137## API Endpoints 138 139### POST /auth/login 140 141Accepts email and password. Returns a JWT token on success. 142 143### POST /auth/register 144 145Creates a new user account. Requires email, password, and display name. 146 147### POST /auth/logout 148 149Invalidates the current session token. 150 151## Security Constraints 152 153- All endpoints must use HTTPS 154- Tokens must be signed with RS256 155- Password reset tokens expire after 1 hour`; 156 157const SPEC_V2 = `# Authentication Service 158 159The authentication service handles user login, registration, session management, and OAuth integration. 160 161## Requirements 162 163- Users must authenticate with email and password 164- Sessions expire after 12 hours 165- Failed login attempts are rate-limited to 3 per minute 166- Passwords must be hashed with argon2id (cost factor 3, memory 64MB) 167- OAuth2 providers (Google, GitHub) must be supported 168 169## API Endpoints 170 171### POST /auth/login 172 173Accepts email and password. Returns a JWT token on success. 174 175### POST /auth/register 176 177Creates a new user account. Requires email, password, and display name. 178 179### POST /auth/logout 180 181Invalidates the current session token. 182 183### GET /auth/oauth/:provider 184 185Initiates OAuth2 flow for the specified provider. 186 187## Security Constraints 188 189- All endpoints must use HTTPS 190- Tokens must be signed with RS256 191- Password reset tokens expire after 30 minutes 192- OAuth tokens must be stored encrypted at rest`; 193 194// ── Main ── 195 196async function main() { 197 console.clear(); 198 console.log(` 199${B}${CY} 🔥 P H O E N I X V C S${R} 200${D} Regenerative Version Control — A Causal Compiler for Intent 201 202 This walkthrough shows you every file, every transformation, 203 and every data structure Phoenix produces as it processes a spec.${R} 204`); 205 await wait(500); 206 207 // ══════════════════════════════════════════════════════════════════ 208 // STEP 1: Here's the input file 209 // ══════════════════════════════════════════════════════════════════ 210 211 banner(1, 'The Input — Your Spec File'); 212 213 console.log(` 214 ${D}This is the file you wrote. It's a plain Markdown spec describing 215 an authentication service. Phoenix will parse this into structured data.${R} 216`); 217 218 showFile('spec/auth.md (v1)', SPEC_V1); 219 220 await wait(400); 221 222 // ══════════════════════════════════════════════════════════════════ 223 // STEP 2: Clause Extraction — splitting the file 224 // ══════════════════════════════════════════════════════════════════ 225 226 banner(2, 'Clause Extraction — Splitting the Spec into Atoms'); 227 228 console.log(` 229 ${D}Phoenix splits on heading boundaries. Each heading + its body = one "clause". 230 Think of clauses as the atomic units of your spec — the smallest pieces 231 that can be independently tracked, hashed, and invalidated.${R} 232`); 233 234 const clausesV1 = parseSpec(SPEC_V1, 'spec/auth.md'); 235 236 // Show the file again with clause boundaries highlighted 237 const v1Lines = SPEC_V1.split('\n'); 238 const clauseHighlights = new Map<number, string>(); 239 const clauseColors = [GR, YL, CY, MG, BL, GR, YL]; 240 for (let ci = 0; ci < clausesV1.length; ci++) { 241 const c = clausesV1[ci]; 242 for (let ln = c.source_line_range[0]; ln <= c.source_line_range[1]; ln++) { 243 clauseHighlights.set(ln, clauseColors[ci % clauseColors.length]); 244 } 245 } 246 showFile('spec/auth.md — color-coded by clause', SPEC_V1, clauseHighlights); 247 248 console.log(` ${D}Each color = one clause. Phoenix found ${B}${clausesV1.length} clauses${R}${D}:${R}\n`); 249 250 for (let i = 0; i < clausesV1.length; i++) { 251 const c = clausesV1[i]; 252 const color = clauseColors[i % clauseColors.length]; 253 const path = c.section_path.join(' → '); 254 console.log(` ${color}${R} Clause ${i + 1}: ${B}${path}${R} ${D}(lines ${c.source_line_range[0]}${c.source_line_range[1]})${R}`); 255 } 256 console.log(''); 257 258 await wait(400); 259 260 // ══════════════════════════════════════════════════════════════════ 261 // STEP 3: Normalization — what Phoenix actually hashes 262 // ══════════════════════════════════════════════════════════════════ 263 264 banner(3, 'Normalization — Stripping Noise Before Hashing'); 265 266 console.log(` 267 ${D}Before hashing, Phoenix normalizes text to ignore formatting noise: 268 • Heading markers (##) removed • Bold/italic markers removed 269 • Everything lowercased • Whitespace collapsed 270 • List items sorted alphabetically • Empty lines removed 271 272 This means ${B}formatting-only changes don't produce false diffs${R}${D}.${R} 273`); 274 275 // Show raw → normalized for a meaty clause 276 const reqClause = clausesV1.find(c => c.section_path.includes('Requirements'))!; 277 278 sub('Raw text (what you wrote)'); 279 for (const line of reqClause.raw_text.split('\n')) { 280 console.log(` ${line}`); 281 } 282 283 sub('Normalized text (what Phoenix hashes)'); 284 for (const line of reqClause.normalized_text.split('\n')) { 285 console.log(` ${GR}${line}${R}`); 286 } 287 288 console.log(`\n ${D}Notice: list items are alphabetically sorted. If you reorder your bullet 289 points, the hash stays the same — because the ${B}meaning${R}${D} didn't change.${R}\n`); 290 291 // Prove it 292 sub('Proof: formatting changes don\'t affect the hash'); 293 const raw1 = '**Phoenix** is a VCS.'; 294 const raw2 = 'Phoenix is a VCS.'; 295 const norm1 = normalizeText(raw1); 296 const norm2 = normalizeText(raw2); 297 const hash1 = clauseSemhash(norm1); 298 const hash2 = clauseSemhash(norm2); 299 console.log(` Input A: "${raw1}"`); 300 console.log(` Input B: "${raw2}"`); 301 console.log(` Normalized A: "${GR}${norm1}${R}"`); 302 console.log(` Normalized B: "${GR}${norm2}${R}"`); 303 console.log(` Hash A: ${D}${hash1.slice(0, 24)}${R}`); 304 console.log(` Hash B: ${D}${hash2.slice(0, 24)}${R}`); 305 console.log(` Match: ${hash1 === hash2 ? `${GR}${B}✓ YES — same hash!${R}` : `${RD}✗ NO${R}`}`); 306 console.log(''); 307 308 await wait(400); 309 310 // ══════════════════════════════════════════════════════════════════ 311 // STEP 4: Semantic Hashing — the clause data structure 312 // ══════════════════════════════════════════════════════════════════ 313 314 banner(4, 'Semantic Hashing — Content-Addressed Clause Objects'); 315 316 console.log(` 317 ${D}Each clause gets two hashes: 318 319 ${B}clause_semhash${R}${D} = SHA-256(normalized_text) 320 Pure content identity. Same text → same hash. Always. 321 322 ${B}context_semhash_cold${R}${D} = SHA-256(normalized_text + section_path + neighbor hashes) 323 Knows WHERE in the document this clause lives 324 and what's around it. Detects structural shifts.${R} 325`); 326 327 // Show the full data structure for one clause 328 const loginClause = clausesV1.find(c => 329 c.section_path[c.section_path.length - 1] === 'POST /auth/login' 330 )!; 331 332 showJSON('Clause Object — POST /auth/login', { 333 clause_id: loginClause.clause_id, 334 source_doc_id: loginClause.source_doc_id, 335 source_line_range: loginClause.source_line_range, 336 section_path: loginClause.section_path, 337 raw_text: loginClause.raw_text, 338 normalized_text: loginClause.normalized_text, 339 clause_semhash: loginClause.clause_semhash, 340 context_semhash_cold: loginClause.context_semhash_cold, 341 }); 342 343 console.log(` ${D}This object is stored by its ${B}clause_id${R}${D} (a content hash). 344 If the content changes, the ID changes. If it doesn't, the ID is stable. 345 This is how Phoenix knows exactly what changed and what didn't.${R}\n`); 346 347 // Show all clause hashes in a table 348 sub('All Clause Hashes'); 349 console.log(` ${'Section'.padEnd(40)} ${'semhash'.padEnd(14)} ${'context_cold'.padEnd(14)}`); 350 console.log(` ${D}${'─'.repeat(40)} ${'─'.repeat(14)} ${'─'.repeat(14)}${R}`); 351 for (const c of clausesV1) { 352 const name = c.section_path[c.section_path.length - 1] || '(root)'; 353 console.log(` ${name.padEnd(40)} ${D}${c.clause_semhash.slice(0, 12)}${R} ${D}${c.context_semhash_cold.slice(0, 12)}${R}`); 354 } 355 console.log(''); 356 357 await wait(400); 358 359 // ══════════════════════════════════════════════════════════════════ 360 // STEP 5: Canonicalization — extracting structured requirements 361 // ══════════════════════════════════════════════════════════════════ 362 363 banner(5, 'Canonicalization — Extracting the Requirements Graph'); 364 365 console.log(` 366 ${D}Phoenix scans each clause for semantic signals and extracts 367 ${B}canonical nodes${R}${D} — structured representations of what the spec actually requires. 368 369 It looks for patterns like: 370 "must", "shall", "required" → ${R}${BG_GR}${WH}${B} REQUIREMENT ${R}${D} 371 "must not", "forbidden", "limited to" → ${R}${BG_RD}${WH}${B} CONSTRAINT ${R}${D} 372 "always", "never" → ${R}${BG_MG}${WH}${B} INVARIANT ${R}${D} 373 ": ", "is defined as" → ${R}${BG_BL}${WH}${B} DEFINITION ${R}${D} 374 375 Heading context also matters: a line under "## Security Constraints" 376 gets classified as a constraint even without magic words.${R} 377`); 378 379 const canonV1 = extractCanonicalNodes(clausesV1); 380 381 // Show each canonical node with its source 382 for (const node of canonV1) { 383 const typeBg = node.type === 'REQUIREMENT' ? BG_GR 384 : node.type === 'CONSTRAINT' ? BG_RD 385 : node.type === 'INVARIANT' ? BG_MG 386 : BG_BL; 387 const sourceClause = clausesV1.find(c => c.clause_id === node.source_clause_ids[0]); 388 const sourceName = sourceClause 389 ? sourceClause.section_path[sourceClause.section_path.length - 1] 390 : '?'; 391 const links = node.linked_canon_ids.length; 392 const linkStr = links > 0 ? ` ${YL}⟷ linked to ${links} other node${links > 1 ? 's' : ''}${R}` : ''; 393 394 console.log(` ${badge(node.type, typeBg)} ${node.statement}`); 395 console.log(` ${D}source: ${sourceName} | tags: [${node.tags.slice(0, 5).join(', ')}]${R}${linkStr}`); 396 console.log(''); 397 } 398 399 // Show the full data for one node 400 const sampleNode = canonV1[0]; 401 showJSON('Canonical Node Object (first node)', { 402 canon_id: sampleNode.canon_id, 403 type: sampleNode.type, 404 statement: sampleNode.statement, 405 source_clause_ids: sampleNode.source_clause_ids, 406 linked_canon_ids: sampleNode.linked_canon_ids, 407 tags: sampleNode.tags, 408 }); 409 410 sub('Provenance Chain'); 411 console.log(` ${D}Every canonical node traces back to its source clause, which traces 412 back to exact line numbers in the spec file. Nothing is disconnected:${R}\n`); 413 const provClause = clausesV1.find(c => c.clause_id === sampleNode.source_clause_ids[0])!; 414 arrow('spec/auth.md:L' + provClause.source_line_range[0] + '–' + provClause.source_line_range[1], 415 'Clause "' + provClause.section_path[provClause.section_path.length - 1] + '"', 416 'parsed into'); 417 arrow('Clause ' + provClause.clause_id.slice(0, 8) + '…', 418 'Canon "' + sampleNode.statement.slice(0, 35) + '…"', 419 'extracted as ' + sampleNode.type); 420 console.log(''); 421 422 await wait(400); 423 424 // ══════════════════════════════════════════════════════════════════ 425 // STEP 6: Warm Hashes — incorporating canonical context 426 // ══════════════════════════════════════════════════════════════════ 427 428 banner(6, 'Warm Context Hashes — Adding Graph Awareness'); 429 430 console.log(` 431 ${D}The cold hash only knows about text + neighbors. Now that we have the 432 canonical graph, we compute a ${B}warm hash${R}${D} that also knows which 433 ${B}requirement nodes${R}${D} are linked to this clause. 434 435 Why? If someone adds a requirement in a different section that ${B}links to${R}${D} 436 this clause's requirements, the warm hash changes — telling Phoenix that 437 this clause's ${B}context${R}${D} shifted even though its ${B}content${R}${D} didn't.${R} 438`); 439 440 const warmV1 = computeWarmHashes(clausesV1, canonV1); 441 442 sub('Cold vs Warm Hashes — Side by Side'); 443 console.log(` ${'Section'.padEnd(32)} ${'Cold Hash'.padEnd(16)} ${'Warm Hash'.padEnd(16)} ${'Status'}`); 444 console.log(` ${D}${'─'.repeat(32)} ${'─'.repeat(16)} ${'─'.repeat(16)} ${'─'.repeat(10)}${R}`); 445 446 for (const c of clausesV1) { 447 const name = (c.section_path[c.section_path.length - 1] || '(root)').slice(0, 30); 448 const cold = c.context_semhash_cold.slice(0, 14); 449 const warm = warmV1.get(c.clause_id)!.slice(0, 14); 450 const status = cold !== warm 451 ? `${YL}differs${R} ${D}← canonical context added${R}` 452 : `${GR}same${R}`; 453 console.log(` ${name.padEnd(32)} ${D}${cold}${R} ${D}${warm}${R} ${status}`); 454 } 455 456 console.log(`\n ${D}All warm hashes differ from cold — because every clause now has 457 canonical nodes linked to it, enriching its context signature.${R}\n`); 458 459 await wait(400); 460 461 // ══════════════════════════════════════════════════════════════════ 462 // STEP 7: Bootstrap state machine 463 // ══════════════════════════════════════════════════════════════════ 464 465 banner(7, 'Bootstrap — State Machine Transition'); 466 467 console.log(` 468 ${D}Phoenix tracks system trust state: 469 470 ${badge('BOOTSTRAP_COLD', BG_BL)} → Parsing only, no canonical graph yet 471 ${badge('BOOTSTRAP_WARMING', BG_YL)} → Canonical graph exists, hashes stabilizing 472 ${badge('STEADY_STATE', BG_GR)} → D-rate acceptable, system trusted 473 474 D-rate alarms are ${B}suppressed during cold${R}${D} (no point — everything is new). 475 Severity is ${B}downgraded during warming${R}${D} (still stabilizing).${R} 476`); 477 478 const bootstrap = new BootstrapStateMachine(); 479 console.log(` State: ${badge(bootstrap.getState(), BG_BL)} Alarms suppressed: ${GR}yes${R}`); 480 481 bootstrap.markWarmPassComplete(); 482 console.log(` State: ${badge(bootstrap.getState(), BG_YL)} Severity downgraded: ${YL}yes${R}`); 483 484 const tracker = new DRateTracker(20); 485 for (let i = 0; i < 18; i++) tracker.recordOne(ChangeClass.A); 486 for (let i = 0; i < 2; i++) tracker.recordOne(ChangeClass.B); 487 const dStatus = tracker.getStatus(); 488 bootstrap.evaluateTransition(dStatus); 489 console.log(` State: ${badge(bootstrap.getState(), BG_GR)} D-rate: ${(dStatus.rate * 100).toFixed(0)}% → ${GR}trusted${R}`); 490 491 showJSON('Bootstrap State (persisted to .phoenix/state.json)', bootstrap.toJSON()); 492 493 await wait(400); 494 495 // ══════════════════════════════════════════════════════════════════ 496 // STEP 8: The spec changes — show the diff 497 // ══════════════════════════════════════════════════════════════════ 498 499 banner(8, 'The Spec Evolves — v1 → v2'); 500 501 console.log(` 502 ${D}A developer edits spec/auth.md. Let's see exactly what changed:${R} 503`); 504 505 // Show v2 with highlights on changed lines 506 const v2Lines = SPEC_V2.split('\n'); 507 const v1Set = new Set(SPEC_V1.split('\n')); 508 const v2Highlights = new Map<number, string>(); 509 for (let i = 0; i < v2Lines.length; i++) { 510 if (!v1Set.has(v2Lines[i])) { 511 v2Highlights.set(i + 1, `${YL}`); 512 } 513 } 514 showFile('spec/auth.md (v2) — yellow = changed/new lines', SPEC_V2, v2Highlights); 515 516 sub('What changed (human-readable)'); 517 console.log(` ${YL}~${R} Line 3: "…and session management" → "…session management, ${B}and OAuth integration${R}"`); 518 console.log(` ${YL}~${R} Line 8: "24 hours" → "${B}12 hours${R}"`); 519 console.log(` ${YL}~${R} Line 9: "5 per minute" → "${B}3 per minute${R}"`); 520 console.log(` ${YL}~${R} Line 10: "bcrypt (cost factor 12)" → "${B}argon2id (cost factor 3, memory 64MB)${R}"`); 521 console.log(` ${GR}+${R} Line 11: ${B}OAuth2 providers (Google, GitHub) must be supported${R} ${D}← new${R}`); 522 console.log(` ${GR}+${R} Line 25: ${B}GET /auth/oauth/:provider${R} ${D}← new endpoint${R}`); 523 console.log(` ${YL}~${R} Line 31: "1 hour" → "${B}30 minutes${R}"`); 524 console.log(` ${GR}+${R} Line 32: ${B}OAuth tokens must be stored encrypted at rest${R} ${D}← new${R}`); 525 console.log(''); 526 527 await wait(400); 528 529 // ══════════════════════════════════════════════════════════════════ 530 // STEP 9: Clause-level diff 531 // ══════════════════════════════════════════════════════════════════ 532 533 banner(9, 'Clause Diff — What Phoenix Sees'); 534 535 console.log(` 536 ${D}Phoenix doesn't diff lines. It diffs ${B}clauses${R}${D} — semantic units. 537 It re-parses v2, compares clause hashes, and classifies each one:${R} 538`); 539 540 const clausesV2 = parseSpec(SPEC_V2, 'spec/auth.md'); 541 const diffs = diffClauses(clausesV1, clausesV2); 542 543 const diffColors: Record<string, string> = { 544 UNCHANGED: GR, MODIFIED: YL, ADDED: GR, REMOVED: RD, MOVED: BL, 545 }; 546 const diffIcons: Record<string, string> = { 547 UNCHANGED: '═', MODIFIED: '~', ADDED: '+', REMOVED: '-', MOVED: '→', 548 }; 549 550 for (const diff of diffs) { 551 const clause = diff.clause_after || diff.clause_before!; 552 const name = clause.section_path[clause.section_path.length - 1] || '(root)'; 553 const color = diffColors[diff.diff_type]; 554 const icon = diffIcons[diff.diff_type]; 555 556 console.log(` ${color}${B}${icon}${R} ${badge(diff.diff_type.padEnd(10), diff.diff_type === 'UNCHANGED' ? BG_GR : diff.diff_type === 'ADDED' ? BG_CY : diff.diff_type === 'REMOVED' ? BG_RD : BG_YL)} ${B}${name}${R}`); 557 558 // Show what changed for MODIFIED clauses 559 if (diff.diff_type === 'MODIFIED' && diff.clause_before && diff.clause_after) { 560 const beforeLines = diff.clause_before.normalized_text.split('\n'); 561 const afterLines = diff.clause_after.normalized_text.split('\n'); 562 const beforeSet = new Set(beforeLines); 563 const afterSet = new Set(afterLines); 564 565 const removed = beforeLines.filter(l => !afterSet.has(l)); 566 const added = afterLines.filter(l => !beforeSet.has(l)); 567 568 for (const line of removed) { 569 if (line.trim()) console.log(` ${RD}- ${line}${R}`); 570 } 571 for (const line of added) { 572 if (line.trim()) console.log(` ${GR}+ ${line}${R}`); 573 } 574 } 575 if (diff.diff_type === 'ADDED' && diff.clause_after) { 576 for (const line of diff.clause_after.normalized_text.split('\n').slice(0, 3)) { 577 if (line.trim()) console.log(` ${GR}+ ${line}${R}`); 578 } 579 } 580 console.log(''); 581 } 582 583 await wait(400); 584 585 // ══════════════════════════════════════════════════════════════════ 586 // STEP 10: Canonicalize v2 + show graph delta 587 // ══════════════════════════════════════════════════════════════════ 588 589 banner(10, 'Canonical Graph Delta — How Requirements Changed'); 590 591 console.log(` 592 ${D}Phoenix canonicalizes v2 and compares the two requirement graphs. 593 This is where it understands the ${B}real impact${R}${D} of the spec change.${R} 594`); 595 596 const canonV2 = extractCanonicalNodes(clausesV2); 597 598 // Find new nodes 599 const v1Stmts = new Set(canonV1.map(n => n.statement)); 600 const v2Stmts = new Set(canonV2.map(n => n.statement)); 601 const newNodes = canonV2.filter(n => !v1Stmts.has(n.statement)); 602 const removedNodes = canonV1.filter(n => !v2Stmts.has(n.statement)); 603 const keptNodes = canonV2.filter(n => v1Stmts.has(n.statement)); 604 605 sub(`Canonical graph: v1 had ${canonV1.length} nodes → v2 has ${canonV2.length} nodes`); 606 607 if (keptNodes.length > 0) { 608 console.log(` ${GR}Unchanged nodes (${keptNodes.length}):${R}`); 609 for (const n of keptNodes) { 610 console.log(` ${GR}${R} ${n.statement.slice(0, 70)}`); 611 } 612 console.log(''); 613 } 614 615 if (removedNodes.length > 0) { 616 console.log(` ${RD}Removed nodes (${removedNodes.length}):${R}`); 617 for (const n of removedNodes) { 618 console.log(` ${RD}- ${n.statement.slice(0, 70)}${R}`); 619 } 620 console.log(''); 621 } 622 623 if (newNodes.length > 0) { 624 console.log(` ${CY}New nodes (${newNodes.length}):${R}`); 625 for (const n of newNodes) { 626 const typeBg = n.type === 'REQUIREMENT' ? BG_GR : n.type === 'CONSTRAINT' ? BG_RD : BG_BL; 627 console.log(` ${CY}+${R} ${badge(n.type, typeBg)} ${n.statement.slice(0, 60)}`); 628 } 629 console.log(''); 630 } 631 632 await wait(400); 633 634 // ══════════════════════════════════════════════════════════════════ 635 // STEP 11: Classify changes A/B/C/D 636 // ══════════════════════════════════════════════════════════════════ 637 638 banner(11, 'Change Classification — A / B / C / D'); 639 640 console.log(` 641 ${D}Now Phoenix classifies each diff using multiple signals: 642 643 ${badge('A', BG_GR)} ${B}Trivial${R}${D} — formatting only, no semantic change 644 ${badge('B', BG_BL)} ${B}Local Semantic${R}${D} — content changed, limited blast radius 645 ${badge('C', BG_YL)} ${B}Contextual Shift${R}${D} — affects canonical graph or structural context 646 ${badge('D', BG_RD)} ${B}Uncertain${R}${D} — classifier can't decide; needs human review 647 648 Signals used: edit distance, semhash delta, context hash delta, 649 term overlap (Jaccard), section structure, # of canonical nodes affected.${R} 650`); 651 652 const warmV2 = computeWarmHashes(clausesV2, canonV2); 653 const classifications = classifyChanges(diffs, canonV1, canonV2, warmV1, warmV2); 654 655 const classColors: Record<string, string> = { A: BG_GR, B: BG_BL, C: BG_YL, D: BG_RD }; 656 const classLabels: Record<string, string> = { 657 A: 'Trivial', B: 'Local Semantic', C: 'Contextual Shift', D: 'Uncertain', 658 }; 659 660 for (let i = 0; i < diffs.length; i++) { 661 const diff = diffs[i]; 662 const cls = classifications[i]; 663 const clause = diff.clause_after || diff.clause_before!; 664 const name = clause.section_path[clause.section_path.length - 1] || '(root)'; 665 666 console.log(` ${badge(cls.change_class, classColors[cls.change_class])} ${B}${name}${R} ${D}(${diff.diff_type})${R}${classLabels[cls.change_class]} ${D}${(cls.confidence * 100).toFixed(0)}% confidence${R}`); 667 668 // Show signal breakdown for non-trivial changes 669 if (cls.change_class !== 'A') { 670 const s = cls.signals; 671 const parts: string[] = []; 672 if (s.semhash_delta) parts.push(`content: ${RD}changed${R}`); 673 else parts.push(`content: ${GR}same${R}`); 674 if (s.context_cold_delta) parts.push(`context: ${YL}shifted${R}`); 675 if (s.norm_diff > 0) parts.push(`edit dist: ${(s.norm_diff * 100).toFixed(0)}%`); 676 if (s.term_ref_delta > 0) parts.push(`term overlap: ${((1 - s.term_ref_delta) * 100).toFixed(0)}%`); 677 if (s.canon_impact > 0) parts.push(`canon impact: ${B}${s.canon_impact} nodes${R}`); 678 console.log(` ${D}signals: ${parts.join(' │ ')}${R}`); 679 } 680 console.log(''); 681 } 682 683 // Show one full classification object 684 const interestingCls = classifications.find(c => c.change_class === 'C' && c.signals.canon_impact > 0)!; 685 if (interestingCls) { 686 showJSON('Full Classification Object (most interesting change)', interestingCls); 687 } 688 689 await wait(400); 690 691 // ══════════════════════════════════════════════════════════════════ 692 // STEP 12: Trust Dashboard 693 // ══════════════════════════════════════════════════════════════════ 694 695 banner(12, 'Trust Dashboard — phoenix status'); 696 697 console.log(` 698 ${D}This is what ${B}phoenix status${R}${D} would show. It's the primary UX of Phoenix. 699 If this is trustworthy, Phoenix works. If it's noisy or wrong, it's useless.${R} 700`); 701 702 const liveTracker = new DRateTracker(50); 703 liveTracker.record(classifications); 704 const liveStatus = liveTracker.getStatus(); 705 706 // Summary table 707 const counts: Record<string, number> = { A: 0, B: 0, C: 0, D: 0 }; 708 for (const c of classifications) counts[c.change_class]++; 709 710 showBox([ 711 `${B}Classification Summary${R}`, 712 ``, 713 ` ${badge('A', BG_GR)} Trivial ${'█'.repeat(counts.A * 4)}${'░'.repeat((8 - counts.A) * 4)} ${counts.A}`, 714 ` ${badge('B', BG_BL)} Local Semantic ${'█'.repeat(counts.B * 4)}${'░'.repeat((8 - counts.B) * 4)} ${counts.B}`, 715 ` ${badge('C', BG_YL)} Contextual Shift ${'█'.repeat(counts.C * 4)}${'░'.repeat((8 - counts.C) * 4)} ${counts.C}`, 716 ` ${badge('D', BG_RD)} Uncertain ${'░'.repeat(8 * 4)} ${counts.D}`, 717 ``, 718 ` ${B}D-Rate:${R} ${(liveStatus.rate * 100).toFixed(1)}% ${badge(liveStatus.level, BG_GR)}`, 719 ` ${D}[${GR}${'█'.repeat(Math.round((1 - liveStatus.rate) * 40))}${R}${D}${'░'.repeat(40 - Math.round((1 - liveStatus.rate) * 40))}] target ≤5% alarm >15%${R}`, 720 ``, 721 ` ${B}Canonical Graph:${R} ${canonV1.length}${canonV2.length} nodes ${GR}(+${newNodes.length} new, -${removedNodes.length} removed)${R}`, 722 ` ${B}System State:${R} ${badge('STEADY_STATE', BG_GR)}`, 723 ]); 724 725 console.log(''); 726 727 // ══════════════════════════════════════════════════════════════════ 728 // Summary 729 // ══════════════════════════════════════════════════════════════════ 730 731 // ══════════════════════════════════════════════════════════════════ 732 // STEP 13: IU Planning — mapping requirements to code units 733 // ══════════════════════════════════════════════════════════════════ 734 735 banner(13, 'IU Planning — Mapping Requirements → Code Modules'); 736 737 console.log(` 738 ${D}Now Phoenix groups canonical nodes into ${B}Implementation Units (IUs)${R}${D} 739 the stable compilation boundaries that will hold generated code. 740 741 Grouping strategy: 742 • Canonical nodes from the same source clause → same IU 743 • Linked nodes (shared terms) → same IU 744 • Each IU gets a risk tier, contract, boundary policy, and evidence policy${R} 745`); 746 747 const iusV1 = planIUs(canonV1, clausesV1); 748 749 for (const iu of iusV1) { 750 const riskBg = iu.risk_tier === 'high' ? BG_RD : iu.risk_tier === 'medium' ? BG_YL : BG_GR; 751 console.log(` ${badge(iu.risk_tier.toUpperCase(), riskBg)} ${B}${iu.name}${R} ${D}(${iu.kind})${R}`); 752 console.log(` ${D}canon nodes: ${iu.source_canon_ids.length} | output: ${iu.output_files.join(', ')}${R}`); 753 console.log(` ${D}evidence required: ${iu.evidence_policy.required.join(', ')}${R}`); 754 console.log(''); 755 } 756 757 showJSON('Implementation Unit Object (first IU)', { 758 iu_id: iusV1[0].iu_id, 759 kind: iusV1[0].kind, 760 name: iusV1[0].name, 761 risk_tier: iusV1[0].risk_tier, 762 contract: { 763 description: iusV1[0].contract.description.slice(0, 120) + '…', 764 invariants: iusV1[0].contract.invariants, 765 }, 766 source_canon_ids: iusV1[0].source_canon_ids.map(id => id.slice(0, 16) + '…'), 767 output_files: iusV1[0].output_files, 768 evidence_policy: iusV1[0].evidence_policy, 769 }); 770 771 await wait(400); 772 773 // ══════════════════════════════════════════════════════════════════ 774 // STEP 14: Code Generation — producing the actual files 775 // ══════════════════════════════════════════════════════════════════ 776 777 banner(14, 'Code Generation — Producing TypeScript Module Stubs'); 778 779 console.log(` 780 ${D}Phoenix generates code for each IU. In v1 this is a stub generator; 781 in production it would invoke an LLM with a structured promptpack. 782 783 The regen engine records: 784 • model_id (which generator produced this) 785 • promptpack hash (what instructions were used) 786 • toolchain version 787 • per-file content hashes (for drift detection later)${R} 788`); 789 790 const regenResults = iusV1.map(iu => generateIU(iu)); 791 792 for (const result of regenResults) { 793 for (const [filePath, content] of result.files) { 794 console.log(` ${BG_CY}${WH}${B} 📄 ${filePath} ${R} ${D}(${content.length} bytes)${R}\n`); 795 const lines = content.split('\n'); 796 for (let i = 0; i < lines.length; i++) { 797 const ln = String(i + 1).padStart(3); 798 console.log(` ${D}${ln}${R} ${lines[i]}`); 799 } 800 console.log(''); 801 } 802 } 803 804 sub('Generated Manifest Entry'); 805 showJSON('.phoenix/manifests/generated_manifest.json (excerpt)', { 806 iu_manifests: { 807 [regenResults[0].manifest.iu_id.slice(0, 16) + '…']: { 808 iu_name: regenResults[0].manifest.iu_name, 809 files: Object.fromEntries( 810 Object.entries(regenResults[0].manifest.files).map(([k, v]) => [k, { 811 content_hash: v.content_hash.slice(0, 16) + '…', 812 size: v.size, 813 }]) 814 ), 815 regen_metadata: regenResults[0].manifest.regen_metadata, 816 }, 817 }, 818 }); 819 820 await wait(400); 821 822 // ══════════════════════════════════════════════════════════════════ 823 // STEP 15: Drift Detection — checking for manual edits 824 // ══════════════════════════════════════════════════════════════════ 825 826 banner(15, 'Drift Detection — Has Anyone Edited Generated Code?'); 827 828 console.log(` 829 ${D}Phoenix compares the working tree against the generated manifest. 830 Every file is hashed. If a hash doesn't match and there's no waiver, 831 that's a ${B}drift violation${R}${D} — someone edited generated code directly, 832 breaking the provenance chain. 833 834 Possible statuses: 835 ${GR}CLEAN${R}${D} — file matches manifest exactly 836 ${RD}DRIFTED${R}${D} — file was modified without a waiver → ${B}ERROR${R}${D} 837 ${YL}WAIVED${R}${D} — file was modified but has an approved waiver 838 ${RD}MISSING${R}${D} — manifest says file should exist but it doesn't${R} 839`); 840 841 // Set up a temp project to demonstrate drift 842 const demoRoot = mkdtempSync(join(tmpdir(), 'phoenix-demo-')); 843 844 // Write generated files to disk 845 for (const result of regenResults) { 846 for (const [path, content] of result.files) { 847 const fullPath = join(demoRoot, path); 848 mkdirSync(join(fullPath, '..'), { recursive: true }); 849 writeFileSync(fullPath, content, 'utf8'); 850 } 851 } 852 853 // Build the manifest 854 const manifest: GeneratedManifest = { 855 iu_manifests: Object.fromEntries(regenResults.map(r => [r.manifest.iu_id, r.manifest])), 856 generated_at: new Date().toISOString(), 857 }; 858 859 // Check — should be clean 860 sub('Scenario 1: Fresh generation — all files clean'); 861 const cleanReport = detectDrift(manifest, demoRoot); 862 for (const entry of cleanReport.entries) { 863 const icon = entry.status === DriftStatus.CLEAN ? `${GR}${R}` : `${RD}${R}`; 864 console.log(` ${icon} ${badge(entry.status, BG_GR)} ${entry.file_path}`); 865 } 866 console.log(`\n ${D}${cleanReport.summary}${R}`); 867 868 // Now tamper with a file 869 sub('Scenario 2: Someone manually edits a generated file'); 870 const firstFile = [...regenResults[0].files.keys()][0]; 871 const fullPath = join(demoRoot, firstFile); 872 const original = regenResults[0].files.get(firstFile)!; 873 writeFileSync(fullPath, '// HACKED BY DEV AT 3AM\n' + original, 'utf8'); 874 console.log(` ${YL}Simulating:${R} Added "// HACKED BY DEV AT 3AM" to ${B}${firstFile}${R}\n`); 875 876 const driftReport = detectDrift(manifest, demoRoot); 877 for (const entry of driftReport.entries) { 878 const icon = entry.status === DriftStatus.CLEAN ? `${GR}${R}` : 879 entry.status === DriftStatus.DRIFTED ? `${RD}${R}` : `${YL}!${R}`; 880 const bg = entry.status === DriftStatus.CLEAN ? BG_GR : BG_RD; 881 console.log(` ${icon} ${badge(entry.status, bg)} ${entry.file_path}`); 882 if (entry.status === DriftStatus.DRIFTED) { 883 console.log(` ${D}expected: ${entry.expected_hash?.slice(0, 16)}… actual: ${entry.actual_hash?.slice(0, 16)}${R}`); 884 } 885 } 886 console.log(`\n ${RD}${B}${driftReport.summary}${R}`); 887 console.log(`\n ${D}To fix: label the edit as ${B}promote_to_requirement${R}${D}, ${B}waiver${R}${D}, or ${B}temporary_patch${R}${D}.${R}`); 888 889 await wait(400); 890 891 // ══════════════════════════════════════════════════════════════════ 892 // STEP 16: Boundary Validation — architectural linting 893 // ══════════════════════════════════════════════════════════════════ 894 895 banner(16, 'Boundary Validation — Architectural Linter'); 896 897 console.log(` 898 ${D}Each IU declares what it's ${B}allowed${R}${D} and ${B}forbidden${R}${D} to touch: 899 • Which packages it may import 900 • Which IUs it may depend on 901 • Which side channels (databases, env vars, APIs) it may use 902 903 Phoenix extracts the dependency graph from the code and validates it 904 against the boundary policy. Violations become diagnostics in ${B}phoenix status${R}${D}.${R} 905`); 906 907 // Show a realistic code sample with violations 908 const naughtyCode = `/** 909 * AuthIU — generated module 910 */ 911import express from 'express'; 912import axios from 'axios'; 913import { adminSecret } from './internal/admin-keys.js'; 914 915const dbUrl = process.env.DATABASE_URL; 916const apiKey = process.env.STRIPE_API_KEY; 917 918const resp = fetch('https://external-service.com/api'); 919 920export function authenticate(email: string, password: string) { 921 // ...implementation 922}`; 923 924 showFile('src/generated/auth-iu.ts (with violations)', naughtyCode); 925 926 sub('Dependency Extraction'); 927 const depGraph = extractDependencies(naughtyCode, 'src/generated/auth-iu.ts'); 928 929 console.log(` ${B}Imports found:${R}`); 930 for (const dep of depGraph.imports) { 931 const rel = dep.is_relative ? `${D}(relative)${R}` : `${D}(package)${R}`; 932 console.log(` L${dep.source_line}: ${CY}${dep.source}${R} ${rel}`); 933 } 934 console.log(`\n ${B}Side channels found:${R}`); 935 for (const sc of depGraph.side_channels) { 936 console.log(` L${sc.source_line}: ${YL}${sc.kind}${R}${sc.identifier}`); 937 } 938 939 showJSON('DependencyGraph object', { 940 file_path: depGraph.file_path, 941 imports: depGraph.imports, 942 side_channels: depGraph.side_channels, 943 }); 944 945 sub('Boundary Validation'); 946 947 // Create a strict boundary policy 948 const strictIU = { 949 ...iusV1[0], 950 boundary_policy: { 951 code: { 952 allowed_ius: [], 953 allowed_packages: ['express', 'bcrypt'], 954 forbidden_ius: [], 955 forbidden_packages: ['axios'], 956 forbidden_paths: ['./internal/**'], 957 }, 958 side_channels: { 959 databases: [], queues: [], caches: [], 960 config: ['DATABASE_URL'], // only this one is declared 961 external_apis: [], 962 files: [], 963 }, 964 }, 965 enforcement: { 966 dependency_violation: { severity: 'error' as const }, 967 side_channel_violation: { severity: 'warning' as const }, 968 }, 969 }; 970 971 console.log(` ${D}Boundary policy for this IU:${R}`); 972 console.log(` ${GR}allowed_packages:${R} [express, bcrypt]`); 973 console.log(` ${RD}forbidden_packages:${R} [axios]`); 974 console.log(` ${RD}forbidden_paths:${R} [./internal/**]`); 975 console.log(` ${GR}declared config:${R} [DATABASE_URL]`); 976 console.log(''); 977 978 const diags = validateBoundary(depGraph, strictIU); 979 980 for (const diag of diags) { 981 const sevBg = diag.severity === 'error' ? BG_RD : BG_YL; 982 const icon = diag.severity === 'error' ? `${RD}${R}` : `${YL}!${R}`; 983 console.log(` ${icon} ${badge(diag.severity.toUpperCase(), sevBg)} ${badge(diag.category, BG_BL)}`); 984 console.log(` ${B}${diag.subject}${R}: ${diag.message}`); 985 console.log(` ${D}at ${diag.source_file}:${diag.source_line}${R}`); 986 console.log(` ${D}fix: ${diag.recommended_actions[0]}${R}`); 987 console.log(''); 988 } 989 990 console.log(` ${D}Total: ${diags.filter(d => d.severity === 'error').length} errors, ${diags.filter(d => d.severity === 'warning').length} warnings${R}`); 991 992 await wait(400); 993 994 // ══════════════════════════════════════════════════════════════════ 995 // STEP 17: Boundary Change Detection 996 // ══════════════════════════════════════════════════════════════════ 997 998 banner(17, 'Boundary Change Detection — Policy Evolution'); 999 1000 console.log(` 1001 ${D}When an IU's boundary policy changes, Phoenix detects it and triggers 1002 re-validation of the IU and all its dependents. This prevents silent 1003 coupling drift.${R} 1004`); 1005 1006 const updatedIU = { 1007 ...strictIU, 1008 boundary_policy: { 1009 ...strictIU.boundary_policy, 1010 code: { 1011 ...strictIU.boundary_policy.code, 1012 allowed_packages: ['express', 'bcrypt', 'argon2'], 1013 forbidden_packages: ['axios', 'got'], 1014 }, 1015 side_channels: { 1016 ...strictIU.boundary_policy.side_channels, 1017 config: ['DATABASE_URL', 'STRIPE_API_KEY'], 1018 external_apis: ['https://external-service.com/api'], 1019 }, 1020 }, 1021 }; 1022 1023 const boundaryChange = detectBoundaryChanges(strictIU, updatedIU); 1024 1025 if (boundaryChange) { 1026 console.log(` ${badge('BOUNDARY CHANGE', BG_YL)} ${B}${boundaryChange.iu_name}${R}\n`); 1027 for (const change of boundaryChange.changes) { 1028 console.log(` ${YL}~${R} ${change}`); 1029 } 1030 console.log(`\n ${D}This triggers: re-extract deps → re-validate → update status for this IU + dependents${R}`); 1031 } 1032 1033 console.log(`\n ${D}After updating the policy to declare the new deps:${R}`); 1034 const diagsAfter = validateBoundary(depGraph, updatedIU); 1035 if (diagsAfter.length === 0) { 1036 console.log(` ${GR}${B}✓ All boundary checks pass${R}`); 1037 } else { 1038 for (const diag of diagsAfter) { 1039 const sevBg = diag.severity === 'error' ? BG_RD : BG_YL; 1040 console.log(` ${badge(diag.severity.toUpperCase(), sevBg)} ${diag.subject}: ${diag.message}`); 1041 } 1042 } 1043 1044 await wait(400); 1045 1046 // ══════════════════════════════════════════════════════════════════ 1047 // STEP 18: Updated Trust Dashboard 1048 // ══════════════════════════════════════════════════════════════════ 1049 1050 banner(18, 'Trust Dashboard — phoenix status (Full)'); 1051 1052 console.log(` 1053 ${D}Everything feeds into the trust dashboard. This is what a developer 1054 sees when they run ${B}phoenix status${R}${D}:${R} 1055`); 1056 1057 showBox([ 1058 `${B}phoenix status${R} ${D}STEADY_STATE | spec/auth.md v1 → v2${R}`, 1059 ``, 1060 `${B}Classification Summary${R} A:${GR}3${R} B:${BL}1${R} C:${YL}4${R} D:${RD}0${R} │ D-Rate: ${GR}0.0%${R} ${badge('TARGET', BG_GR)}`, 1061 ``, 1062 `${B}Canonical Graph${R} 8 → 10 nodes │ ${GR}+${newNodes.length} new${R} ${RD}-${removedNodes.length} removed${R} ${GR}${keptNodes.length} kept${R}`, 1063 ``, 1064 `${B}Implementation Units${R} ${iusV1.length} IU${iusV1.length > 1 ? 's' : ''}${regenResults.reduce((s,r) => s + r.files.size, 0)} generated files`, 1065 ``, 1066 `${B}Drift${R} ${driftReport.drifted_count > 0 ? `${RD}${B}${driftReport.drifted_count} DRIFTED${R}` : `${GR}all clean${R}`}${driftReport.clean_count} clean ${driftReport.drifted_count} drifted`, 1067 ``, 1068 `${B}Boundary${R} ${diags.length > 0 ? `${RD}${diags.filter(d=>d.severity==='error').length} errors${R} ${YL}${diags.filter(d=>d.severity==='warning').length} warnings${R}` : `${GR}all clear${R}`}`, 1069 ``, 1070 `${B}Actions Required:${R}`, 1071 ` ${RD}ERROR${R} drift ${firstFile} Drifted from manifest → label or reconcile`, 1072 ` ${RD}ERROR${R} boundary axios Forbidden package → remove import`, 1073 ` ${RD}ERROR${R} boundary ./internal/** Forbidden path → remove import`, 1074 ` ${YL}WARN ${R} boundary STRIPE_API_KEY Undeclared config → declare or remove`, 1075 ` ${YL}WARN ${R} boundary external-svc Undeclared API → declare or remove`, 1076 ]); 1077 1078 console.log(''); 1079 1080 await wait(400); 1081 1082 // ══════════════════════════════════════════════════════════════════ 1083 // STEP 19: Evidence & Policy Engine (Phase D) 1084 // ══════════════════════════════════════════════════════════════════ 1085 1086 banner(19, 'Evidence & Policy — Risk-Tiered Proof'); 1087 1088 console.log(` 1089 ${D}Each IU has a risk tier that determines what evidence is required 1090 before its generated code is accepted: 1091 1092 ${badge('LOW', BG_GR)} typecheck + lint + boundary validation 1093 ${badge('MEDIUM', BG_YL)} + unit tests 1094 ${badge('HIGH', BG_RD)} + property tests + threat note + static analysis 1095 ${badge('CRITICAL', BG_MG)} + human signoff or formal verification${R} 1096`); 1097 1098 const demoIU = iusV1[0]; 1099 sub(`Evaluating ${demoIU.name} (${demoIU.risk_tier} tier)`); 1100 console.log(` ${D}Required evidence: ${demoIU.evidence_policy.required.join(', ')}${R}\n`); 1101 1102 // No evidence yet 1103 const eval1 = evaluatePolicy(demoIU, []); 1104 console.log(` ${RD}Before evidence:${R} verdict = ${badge(eval1.verdict, BG_RD)}`); 1105 console.log(` ${D}missing: ${eval1.missing.join(', ')}${R}\n`); 1106 1107 // Submit all passing evidence 1108 const passingEvidence: EvidenceRecord[] = demoIU.evidence_policy.required.map(kind => ({ 1109 evidence_id: 'ev-' + kind, kind: kind as EvidenceKind, 1110 status: EvidenceStatus.PASS, iu_id: demoIU.iu_id, 1111 canon_ids: demoIU.source_canon_ids, timestamp: new Date().toISOString(), 1112 })); 1113 1114 const eval2 = evaluatePolicy(demoIU, passingEvidence); 1115 console.log(` ${GR}After all evidence passes:${R} verdict = ${badge(eval2.verdict, BG_GR)}`); 1116 console.log(` ${D}satisfied: ${eval2.satisfied.join(', ')}${R}\n`); 1117 1118 // Simulate failure 1119 const failedEvidence = [...passingEvidence, { 1120 evidence_id: 'ev-fail', kind: EvidenceKind.TYPECHECK, 1121 status: EvidenceStatus.FAIL, iu_id: demoIU.iu_id, 1122 canon_ids: [], message: 'TS2322: Type error in auth module', 1123 timestamp: new Date(Date.now() + 1000).toISOString(), 1124 }]; 1125 1126 const eval3 = evaluatePolicy(demoIU, failedEvidence); 1127 console.log(` ${RD}After typecheck fails:${R} verdict = ${badge(eval3.verdict, BG_RD)}`); 1128 console.log(` ${D}failed: ${eval3.failed.join(', ')}${R}`); 1129 1130 showJSON('PolicyEvaluation object', eval3); 1131 1132 await wait(400); 1133 1134 // ══════════════════════════════════════════════════════════════════ 1135 // STEP 20: Cascading Failures (Phase D) 1136 // ══════════════════════════════════════════════════════════════════ 1137 1138 banner(20, 'Cascading Failures — Graph-Based Propagation'); 1139 1140 console.log(` 1141 ${D}When an IU's evidence fails, Phoenix propagates through the dependency 1142 graph. The failed IU is ${B}BLOCKED${R}${D}. Its dependents must ${B}RE_VALIDATE${R}${D} 1143 (re-run typecheck + boundary checks + tagged tests). 1144 1145 This prevents a broken module from silently poisoning downstream code.${R} 1146`); 1147 1148 // Create a scenario with dependencies 1149 const cascadeIUs = [ 1150 { ...demoIU, iu_id: 'auth-iu', name: 'AuthIU', dependencies: [] as string[] }, 1151 { ...demoIU, iu_id: 'session-iu', name: 'SessionIU', dependencies: ['auth-iu'] }, 1152 { ...demoIU, iu_id: 'api-iu', name: 'ApiIU', dependencies: ['session-iu'] }, 1153 ]; 1154 1155 console.log(` ${D}Dependency graph:${R} AuthIU ← SessionIU ← ApiIU\n`); 1156 1157 const cascadeEvals = [{ ...eval3, iu_id: 'auth-iu', iu_name: 'AuthIU' }]; 1158 const cascadeEvents = computeCascade(cascadeEvals, cascadeIUs); 1159 1160 for (const event of cascadeEvents) { 1161 console.log(` ${badge('CASCADE', BG_RD)} from ${B}${event.source_iu_name}${R} (${event.failure_kind})\n`); 1162 for (const action of event.actions) { 1163 const actionBg = action.action === 'BLOCK' ? BG_RD : BG_YL; 1164 console.log(` ${badge(action.action, actionBg)} ${B}${action.iu_name}${R}`); 1165 console.log(` ${D}${action.reason}${R}`); 1166 } 1167 } 1168 console.log(''); 1169 1170 await wait(400); 1171 1172 // ══════════════════════════════════════════════════════════════════ 1173 // STEP 21: Shadow Pipeline (Phase E) 1174 // ══════════════════════════════════════════════════════════════════ 1175 1176 banner(21, 'Shadow Pipeline — Safe Canonicalization Upgrades'); 1177 1178 console.log(` 1179 ${D}When upgrading the canonicalization pipeline (new model, new rules), 1180 Phoenix runs ${B}both old and new pipelines in parallel${R}${D} and compares output. 1181 1182 Classification: 1183 ${badge('SAFE', BG_GR)} node change ≤3%, no risk escalations 1184 ${badge('COMPACTION_EVENT', BG_YL)} node change ≤25%, no orphans 1185 ${badge('REJECT', BG_RD)} orphan nodes, excessive churn, or high drift${R} 1186`); 1187 1188 const oldP = { pipeline_id: 'v1.0', model_id: 'rule-based/1.0', promptpack_version: '1.0', extraction_rules_version: '1.0', diff_policy_version: '1.0' }; 1189 const newP = { pipeline_id: 'v1.1', model_id: 'rule-based/1.1', promptpack_version: '1.1', extraction_rules_version: '1.1', diff_policy_version: '1.0' }; 1190 1191 // Scenario 1: identical output → SAFE 1192 sub('Scenario 1: Minor rule tweak, same output'); 1193 const safe = runShadowPipeline(oldP, newP, canonV1, canonV1); 1194 console.log(` ${badge(safe.classification, BG_GR)} ${safe.reason}`); 1195 console.log(` ${D}node change: ${safe.metrics.node_change_pct}% orphans: ${safe.metrics.orphan_nodes}${R}\n`); 1196 1197 // Scenario 2: v1 → v2 output → COMPACTION_EVENT 1198 sub('Scenario 2: Major extraction rules upgrade'); 1199 const canonV2ForShadow = extractCanonicalNodes(clausesV2); 1200 const compact = runShadowPipeline(oldP, { ...newP, pipeline_id: 'v2.0' }, canonV1, canonV2ForShadow); 1201 console.log(` ${badge(compact.classification, compact.classification === 'REJECT' ? BG_RD : compact.classification === 'SAFE' ? BG_GR : BG_YL)} ${compact.reason}`); 1202 console.log(` ${D}node change: ${compact.metrics.node_change_pct}% drift: ${compact.metrics.semantic_stmt_drift}% orphans: ${compact.metrics.orphan_nodes}${R}`); 1203 1204 showJSON('ShadowResult metrics', compact.metrics); 1205 1206 await wait(400); 1207 1208 // ══════════════════════════════════════════════════════════════════ 1209 // STEP 22: Compaction (Phase E) 1210 // ══════════════════════════════════════════════════════════════════ 1211 1212 banner(22, 'Compaction — Storage Lifecycle'); 1213 1214 console.log(` 1215 ${D}Phoenix compacts old data into cold storage while ${B}never deleting${R}${D}: 1216 • Node headers (identity preserved forever) 1217 • Provenance edges (traceability preserved forever) 1218 • Approvals & signatures (audit trail preserved forever) 1219 1220 Storage tiers: ${badge('HOT', BG_GR)} (30 days) → ${badge('ANCESTRY', BG_YL)} (metadata forever) → ${badge('COLD', BG_BL)} (blobs archived)${R} 1221`); 1222 1223 const compactObjects = [ 1224 { object_id: '1', object_type: 'clause_body', age_days: 90, size_bytes: 50000, preserve: false }, 1225 { object_id: '2', object_type: 'clause_body', age_days: 60, size_bytes: 30000, preserve: false }, 1226 { object_id: '3', object_type: 'node_header', age_days: 90, size_bytes: 500, preserve: true }, 1227 { object_id: '4', object_type: 'provenance_edge', age_days: 120, size_bytes: 200, preserve: true }, 1228 { object_id: '5', object_type: 'approval', age_days: 180, size_bytes: 300, preserve: true }, 1229 { object_id: '6', object_type: 'clause_body', age_days: 10, size_bytes: 20000, preserve: false }, 1230 ]; 1231 1232 const compactEvent = runCompaction(compactObjects, 'size_threshold', 30); 1233 console.log(` ${badge('CompactionEvent', BG_MG)}\n`); 1234 console.log(` Trigger: ${compactEvent.trigger}`); 1235 console.log(` Compacted: ${compactEvent.nodes_compacted} objects (${(compactEvent.bytes_freed / 1024).toFixed(1)} KB freed)`); 1236 console.log(` ${GR}Preserved:${R} ${compactEvent.preserved.node_headers} headers, ${compactEvent.preserved.provenance_edges} provenance, ${compactEvent.preserved.approvals} approvals, ${compactEvent.preserved.signatures} signatures`); 1237 console.log(''); 1238 1239 await wait(400); 1240 1241 // ══════════════════════════════════════════════════════════════════ 1242 // STEP 23: Bot Interface (Phase F) 1243 // ══════════════════════════════════════════════════════════════════ 1244 1245 banner(23, 'Freeq Bot Interface — Structured Commands'); 1246 1247 console.log(` 1248 ${D}Bots interact with Phoenix using a strict command grammar. 1249 No fuzzy NLU — commands are deterministic and parseable. 1250 1251 Three bots: 1252 ${CY}SpecBot${R}${D} — ingest, diff, clauses 1253 ${CY}ImplBot${R}${D} — plan, regen, drift 1254 ${CY}PolicyBot${R}${D} — status, evidence, cascade 1255 1256 Mutating commands require ${B}confirmation${R}${D}. 1257 Read-only commands execute immediately.${R} 1258`); 1259 1260 const botExamples = [ 1261 'SpecBot: ingest spec/auth.md', 1262 'ImplBot: regen iu=AuthIU', 1263 'PolicyBot: status', 1264 'SpecBot: help', 1265 ]; 1266 1267 for (const raw of botExamples) { 1268 console.log(` ${BG_BL}${WH}${B} > ${raw} ${R}\n`); 1269 const parsed = parseCommand(raw); 1270 if ('error' in parsed) { 1271 console.log(` ${RD}Error: ${parsed.error}${R}\n`); 1272 continue; 1273 } 1274 const resp = routeCommand(parsed); 1275 if (resp.mutating) { 1276 console.log(` ${YL}⚠ Mutating command — confirmation required${R}`); 1277 console.log(` ${D}Intent:${R} ${resp.intent}`); 1278 console.log(` ${D}Confirm:${R} ${GR}ok${R} or ${GR}phx confirm ${resp.confirm_id}${R}\n`); 1279 } else { 1280 console.log(` ${GR}✓ Read-only — executing immediately${R}`); 1281 console.log(` ${D}${resp.message}${R}\n`); 1282 } 1283 } 1284 1285 await wait(400); 1286 1287 banner(0, 'Recap — What You Just Saw'); 1288 1289 console.log(` 1290 ${B}The Full Pipeline (Phases A → F):${R} 1291 1292 ${CY}spec/auth.md${R} ${D}← your spec file${R} 12931294${D}A: parse + normalize + hash${R} 1295 ${CY}Clauses + Hashes${R} ${D}← content-addressed atoms${R} 12961297${D}B: canonicalize + classify changes${R} 1298 ${CY}Canonical Graph + A/B/C/D${R} ${D}← requirements + change classes${R} 12991300${D}C1: plan IUs + generate + manifest${R} 1301 ${CY}IUs + Generated Code${R} ${D}← compilation boundaries${R} 13021303 ├──▸ ${D}C1: drift detection${R} ${CY}CLEAN / DRIFTED / WAIVED${R} 1304 ├──▸ ${D}C2: boundary validation${R} ${CY}Diagnostics${R} 1305 ├──▸ ${D}D: evidence + policy eval${R} ${CY}PASS / FAIL / INCOMPLETE${R} 1306 ├──▸ ${D}D: cascade on failure${R} ${CY}BLOCK + RE_VALIDATE${R} 1307 ├──▸ ${D}E: shadow pipeline upgrade${R} ${CY}SAFE / COMPACTION / REJECT${R} 1308 ├──▸ ${D}E: compaction${R} ${CY}Hot → Ancestry → Cold${R} 130913101311 ${CY}Trust Dashboard${R} ${D}← phoenix status${R} 13121313${D}F: bot interface${R} 1314 ${CY}SpecBot / ImplBot / PolicyBot${R} ${D}← structured commands${R} 1315 1316 ${B}Key insight:${R} Change ${YL}"bcrypt"${R} to ${YL}"argon2id"${R} on line 10 and Phoenix 1317 traces impact through clauses → canonical nodes → IUs → generated 1318 files → boundary policies → evidence → dependent IUs. Only the 1319 affected subtree is invalidated and regenerated. 1320 1321 ${D}That's selective invalidation — the defining capability. 1322 Not "rebuild everything." Just the dependent subtree.${R} 1323 1324 ${B}${CY}Trust > Cleverness.${R} 1325`); 1326} 1327 1328main().catch(console.error);