Reference implementation for the Phoenix Architecture. Work in progress.
aicoding.leaflet.pub/
ai
coding
crazy
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}
1293 │
1294 ▼ ${D}A: parse + normalize + hash${R}
1295 ${CY}Clauses + Hashes${R} ${D}← content-addressed atoms${R}
1296 │
1297 ▼ ${D}B: canonicalize + classify changes${R}
1298 ${CY}Canonical Graph + A/B/C/D${R} ${D}← requirements + change classes${R}
1299 │
1300 ▼ ${D}C1: plan IUs + generate + manifest${R}
1301 ${CY}IUs + Generated Code${R} ${D}← compilation boundaries${R}
1302 │
1303 ├──▸ ${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}
1309 │
1310 ▼
1311 ${CY}Trust Dashboard${R} ${D}← phoenix status${R}
1312 │
1313 ▼ ${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);