Reference implementation for the Phoenix Architecture. Work in progress. aicoding.leaflet.pub/
ai coding crazy
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: interactive Spec Text view in Phoenix Inspect

New "Spec" mode in the pipeline visualizer. Shows the actual spec
text with line numbers. Lines mapping to clauses have a blue left
border. Click any line to trace the full path:

Spec text → Clause → Canonical Nodes → IUs → Generated Files

Right panel shows the trace with color-coded canonical types
(REQUIREMENT=blue, CONSTRAINT=red, INVARIANT=purple), risk tiers,
file sizes, and drift status.

Also: phoenix ingest now shows clause diffs before overwriting,
and includes the raw spec file content in inspect data for the
text view.

+175 -6
+1
src/cli.ts
··· 1337 1337 ius, 1338 1338 manifest, 1339 1339 driftReport, 1340 + projectRoot, 1340 1341 ); 1341 1342 1342 1343 const html = renderInspectHTML(data);
+174 -6
src/inspect.ts
··· 10 10 */ 11 11 12 12 import { createServer } from 'node:http'; 13 + import { readFileSync, existsSync } from 'node:fs'; 14 + import { join } from 'node:path'; 13 15 import type { Clause } from './models/clause.js'; 14 16 import type { CanonicalNode } from './models/canonical.js'; 15 17 import type { ImplementationUnit } from './models/iu.js'; ··· 34 36 id: string; 35 37 path: string; 36 38 clauseCount: number; 39 + /** Raw content lines for the spec-text view */ 40 + lines?: string[]; 37 41 } 38 42 39 43 export interface ClauseInfo { ··· 111 115 ius: ImplementationUnit[], 112 116 manifest: GeneratedManifest, 113 117 driftReport: DriftReport | null, 118 + projectRoot?: string, 114 119 ): InspectData { 115 120 const edges: Edge[] = []; 116 121 ··· 121 126 list.push(c); 122 127 docMap.set(c.source_doc_id, list); 123 128 } 124 - const specFiles: SpecFileInfo[] = [...docMap.entries()].map(([docId, docClauses]) => ({ 125 - id: `spec:${docId}`, 126 - path: docId, 127 - clauseCount: docClauses.length, 128 - })); 129 + const specFiles: SpecFileInfo[] = [...docMap.entries()].map(([docId, docClauses]) => { 130 + let lines: string[] | undefined; 131 + if (projectRoot) { 132 + const fullPath = join(projectRoot, docId); 133 + if (existsSync(fullPath)) { 134 + lines = readFileSync(fullPath, 'utf8').split('\n'); 135 + } 136 + } 137 + return { id: `spec:${docId}`, path: docId, clauseCount: docClauses.length, lines }; 138 + }); 129 139 130 140 // Clauses + spec→clause edges 131 141 const clauseInfos: ClauseInfo[] = clauses.map(c => { ··· 306 316 svg.graph-edges path{fill:none;stroke:var(--cyan);stroke-width:2;opacity:.5} 307 317 svg.graph-edges path.primary{stroke-width:3;opacity:.9;filter:drop-shadow(0 0 4px rgba(34,211,238,.4))} 308 318 .graph-hint{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);font-size:11px;color:var(--dim);text-align:center} 319 + 320 + /* ── Spec Text View ── */ 321 + .spec-view{display:none;height:calc(100vh - 52px);overflow:hidden} 322 + .spec-view.open{display:flex} 323 + .spec-left{width:50%;overflow-y:auto;border-right:1px solid var(--border);padding:0} 324 + .spec-right{width:50%;overflow-y:auto;padding:16px} 325 + .spec-file-tab{padding:8px 16px;background:var(--surface);border-bottom:1px solid var(--border);font-weight:700;font-size:12px;color:var(--blue);position:sticky;top:0;z-index:5} 326 + .spec-line{padding:2px 16px 2px 50px;position:relative;font-size:13px;line-height:1.7;cursor:default;border-left:3px solid transparent;transition:all .1s} 327 + .spec-line:hover{background:var(--surface2)} 328 + .spec-line .ln{position:absolute;left:0;width:42px;text-align:right;color:var(--dim);font-size:11px;user-select:none} 329 + .spec-line.has-clause{cursor:pointer;border-left-color:var(--blue)} 330 + .spec-line.has-clause:hover{border-left-color:var(--cyan);background:#142535} 331 + .spec-line.active{border-left-color:var(--cyan);background:#1a3040;box-shadow:inset 0 0 20px rgba(34,211,238,.08)} 332 + .spec-line .heading{color:var(--blue);font-weight:700} 333 + .spec-line .bullet{color:var(--dim)} 334 + .trace-panel{background:var(--surface);border-radius:8px;border:1px solid var(--border);margin-bottom:12px;overflow:hidden} 335 + .trace-header{padding:10px 14px;border-bottom:1px solid var(--border);font-weight:700;font-size:12px;display:flex;justify-content:space-between;align-items:center} 336 + .trace-header .label{color:var(--dim);font-size:10px;text-transform:uppercase;letter-spacing:.5px} 337 + .trace-body{padding:10px 14px} 338 + .trace-item{padding:6px 0;border-bottom:1px solid var(--border);font-size:12px} 339 + .trace-item:last-child{border-bottom:none} 340 + .trace-item .ti-type{font-weight:600;margin-right:6px} 341 + .trace-item .ti-stmt{color:var(--text)} 342 + .trace-item .ti-tags{margin-top:3px} 343 + .trace-empty{text-align:center;padding:40px;color:var(--dim)} 309 344 </style> 310 345 </head> 311 346 <body> ··· 313 348 <h1>🔥 Phoenix</h1> 314 349 <div class="state">${esc(data.systemState)}</div> 315 350 <div class="mode-btns"> 351 + <button class="mode-btn" onclick="setMode('spec')" id="btn-spec">📄 Spec</button> 316 352 <button class="mode-btn active" onclick="setMode('all')" id="btn-all">All</button> 317 353 <button class="mode-btn" onclick="setMode('focus')" id="btn-focus">Focus</button> 318 354 <button class="mode-btn" onclick="openGraph()" id="btn-graph">⬡ Graph</button> ··· 329 365 <div class="pipeline-wrap"> 330 366 <svg class="lines" id="svg-lines"></svg> 331 367 <div class="pipeline" id="pipeline"></div> 368 + </div> 369 + <div class="spec-view" id="spec-view"> 370 + <div class="spec-left" id="spec-text"></div> 371 + <div class="spec-right" id="spec-trace"> 372 + <div class="trace-empty">Click a highlighted line in the spec to trace its path through the pipeline</div> 373 + </div> 332 374 </div> 333 375 <div class="graph-overlay" id="graph-overlay"> 334 376 <div class="graph-bar"> ··· 405 447 mode=m; 406 448 document.getElementById('btn-all').classList.toggle('active',m==='all'); 407 449 document.getElementById('btn-focus').classList.toggle('active',m==='focus'); 408 - applyView(); 450 + document.getElementById('btn-spec').classList.toggle('active',m==='spec'); 451 + // Toggle between pipeline and spec views 452 + document.querySelector('.pipeline-wrap').style.display=m==='spec'?'none':'flex'; 453 + document.getElementById('spec-view').classList.toggle('open',m==='spec'); 454 + if(m==='spec')renderSpecView(); 455 + else applyView(); 409 456 } 410 457 411 458 function selectCard(id){ ··· 538 585 if(!e.target.closest('.card')&&!e.target.closest('.graph-overlay')&&!e.target.closest('.header'))deselect(); 539 586 }); 540 587 window.addEventListener('resize',()=>{if(selId&&mode==='focus')requestAnimationFrame(drawLines)}); 588 + 589 + // ── Spec Text View ── 590 + function renderSpecView(){ 591 + const container=document.getElementById('spec-text'); 592 + let html=''; 593 + // Build line→clause mapping 594 + const lineClauseMap={}; 595 + D.clauses.forEach(cl=>{ 596 + const match=cl.lineRange.match(/L(\\d+)–(\\d+)/); 597 + if(!match)return; 598 + const start=parseInt(match[1]),end=parseInt(match[2]); 599 + for(let i=start;i<=end;i++){ 600 + lineClauseMap[cl.docId+'::'+i]=cl; 601 + } 602 + }); 603 + 604 + D.specFiles.forEach(sf=>{ 605 + if(!sf.lines)return; 606 + html+='<div class="spec-file-tab">'+E(sf.path)+'</div>'; 607 + sf.lines.forEach((line,i)=>{ 608 + const lineNum=i+1; 609 + const cl=lineClauseMap[sf.path+'::'+lineNum]; 610 + const hasCl=!!cl; 611 + const isHeading=/^#{1,6}\\s/.test(line); 612 + const isBullet=/^\\s*[-*•]/.test(line); 613 + const content=E(line)||'&nbsp;'; 614 + const displayContent=isHeading?'<span class="heading">'+content+'</span>' 615 + :isBullet?'<span class="bullet">- </span>'+content.replace(/^\\s*[-*•]\\s*/,'') 616 + :content; 617 + html+='<div class="spec-line'+(hasCl?' has-clause':'')+'" data-line="'+lineNum+'" data-doc="'+E(sf.path)+'"' 618 + +(cl?' data-clause="'+cl.id+'"':'') 619 + +'><span class="ln">'+lineNum+'</span>'+displayContent+'</div>'; 620 + }); 621 + }); 622 + container.innerHTML=html; 623 + 624 + // Click handler for spec lines 625 + container.querySelectorAll('.spec-line.has-clause').forEach(el=>{ 626 + el.addEventListener('click',()=>{ 627 + container.querySelectorAll('.spec-line.active').forEach(a=>a.classList.remove('active')); 628 + // Highlight all lines in this clause's range 629 + const clauseId=el.dataset.clause; 630 + const clause=D.clauses.find(c=>c.id===clauseId); 631 + if(!clause)return; 632 + const match=clause.lineRange.match(/L(\\d+)–(\\d+)/); 633 + if(!match)return; 634 + const start=parseInt(match[1]),end=parseInt(match[2]); 635 + for(let i=start;i<=end;i++){ 636 + const ln=container.querySelector('[data-line="'+i+'"][data-doc="'+el.dataset.doc+'"]'); 637 + if(ln)ln.classList.add('active'); 638 + } 639 + showTrace(clauseId); 640 + }); 641 + }); 642 + } 643 + 644 + function showTrace(clauseId){ 645 + const panel=document.getElementById('spec-trace'); 646 + const clause=D.clauses.find(c=>c.id===clauseId); 647 + if(!clause){panel.innerHTML='<div class="trace-empty">No clause data</div>';return;} 648 + 649 + // Find canon nodes from this clause 650 + const canonIds=new Set(); 651 + D.edges.filter(e=>e.from==='clause:'+clauseId&&e.type==='clause→canon').forEach(e=>canonIds.add(e.to.replace('canon:',''))); 652 + const canonNodes=D.canonNodes.filter(n=>canonIds.has(n.id)); 653 + 654 + // Find IUs from these canon nodes 655 + const iuIds=new Set(); 656 + D.edges.filter(e=>e.type==='canon→iu'&&canonIds.has(e.from.replace('canon:',''))).forEach(e=>iuIds.add(e.to.replace('iu:',''))); 657 + const ius=D.ius.filter(u=>iuIds.has(u.id)); 658 + 659 + // Find generated files from these IUs 660 + const fileIds=new Set(); 661 + D.edges.filter(e=>e.type==='iu→file'&&iuIds.has(e.from.replace('iu:',''))).forEach(e=>fileIds.add(e.to.replace('file:',''))); 662 + const files=D.generatedFiles.filter(f=>fileIds.has(f.path)); 663 + 664 + let html=''; 665 + 666 + // Clause info 667 + html+='<div class="trace-panel"><div class="trace-header"><span>📋 Clause</span><span class="label">'+clause.lineRange+'</span></div>'; 668 + html+='<div class="trace-body"><div class="trace-item"><span class="ti-type" style="color:var(--blue)">'+E(clause.sectionPath)+'</span></div>'; 669 + html+='<div class="trace-item" style="color:var(--dim);font-size:11px">'+E(clause.preview)+'</div></div></div>'; 670 + 671 + // Canon nodes 672 + if(canonNodes.length>0){ 673 + html+='<div class="trace-panel"><div class="trace-header"><span>📐 Canonical Nodes</span><span class="label">'+canonNodes.length+' nodes</span></div><div class="trace-body">'; 674 + const typeColors={REQUIREMENT:'var(--blue)',CONSTRAINT:'var(--red)',INVARIANT:'var(--purple)',DEFINITION:'var(--green)',CONTEXT:'var(--yellow)'}; 675 + canonNodes.forEach(n=>{ 676 + html+='<div class="trace-item"><span class="ti-type" style="color:'+(typeColors[n.type]||'var(--dim)')+'">'+n.type+'</span>'; 677 + html+='<span class="ti-stmt">'+E(n.statement.slice(0,100))+'</span>'; 678 + if(n.tags.length>0)html+='<div class="ti-tags">'+n.tags.slice(0,5).map(t=>'<span class="tag">'+E(t)+'</span>').join('')+'</div>'; 679 + html+='</div>'; 680 + }); 681 + html+='</div></div>'; 682 + } 683 + 684 + // IUs 685 + if(ius.length>0){ 686 + html+='<div class="trace-panel"><div class="trace-header"><span>📦 Implementation Units</span><span class="label">'+ius.length+' IUs</span></div><div class="trace-body">'; 687 + ius.forEach(u=>{ 688 + const riskColor={low:'var(--green)',medium:'var(--yellow)',high:'var(--orange)',critical:'var(--red)'}[u.riskTier]||'var(--dim)'; 689 + html+='<div class="trace-item"><span class="ti-type">'+E(u.name)+'</span> <span class="badge" style="background:color-mix(in srgb,'+riskColor+' 20%,transparent);color:'+riskColor+'">'+u.riskTier+'</span>'; 690 + html+='<div style="color:var(--dim);font-size:11px;margin-top:2px">'+u.canonCount+' canon nodes → '+u.outputFiles.length+' file(s)</div></div>'; 691 + }); 692 + html+='</div></div>'; 693 + } 694 + 695 + // Files 696 + if(files.length>0){ 697 + html+='<div class="trace-panel"><div class="trace-header"><span>⚡ Generated Files</span><span class="label">'+files.length+' files</span></div><div class="trace-body">'; 698 + files.forEach(f=>{ 699 + const driftColor=f.driftStatus==='CLEAN'?'var(--green)':f.driftStatus==='DRIFTED'?'var(--red)':'var(--dim)'; 700 + html+='<div class="trace-item"><span class="ti-type">'+E(f.path.split('/').pop())+'</span>'; 701 + html+=' <span class="badge" style="background:color-mix(in srgb,'+driftColor+' 20%,transparent);color:'+driftColor+'">'+f.driftStatus+'</span>'; 702 + html+='<div style="color:var(--dim);font-size:11px;margin-top:2px">'+(f.size/1024).toFixed(1)+'KB · '+f.contentHash+'</div></div>'; 703 + }); 704 + html+='</div></div>'; 705 + } 706 + 707 + panel.innerHTML=html||'<div class="trace-empty">No traceability data for this clause</div>'; 708 + } 541 709 542 710 render(); 543 711 </script>