docs: add GraphComplete architecture synthesis prototype - #352
docs: add GraphComplete architecture synthesis prototype#352vishaltandale00 wants to merge 2 commits into
Conversation
Claude Code loads CLAUDE.md, not AGENTS.md, so the repo agent instructions were not reaching Claude sessions. Import instead of duplicating so AGENTS.md stays the single source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cf125f6d6d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| function layerShape(id,x,y,selected=false){const g=S("g",{class:`layer layer-${id}`,transform:`translate(${x} ${y})`,"data-id":id});g.append(S("rect",{class:"shell",x:0,y:0,width:106,height:64,rx:19}));g.append(S("text",{class:"layer-label",x:53,y:14,fill:id==="build"?colors.purple:colors.green},id.toUpperCase()));g.append(S("path",{d:"M53 31 L28 46 M53 31 V45 M53 31 L78 46",fill:"none",stroke:colors.line,"stroke-width":1.4}));[[53,27],[26,50],[53,50],[80,50]].forEach(([cx,cy],i)=>g.append(S("circle",{class:`node ${i===0?"root":""} ${selected&&i===3?"selected":""}`,cx,cy,r:i===0?6:5,"data-node-index":i})));return g} | ||
| function completeGlyph(x,y,angle=0,extra=""){const g=S("g",{class:`complete ${extra}`,transform:`translate(${x} ${y}) rotate(${angle})`});g.append(S("rect",{x:-24,y:-9,width:48,height:18,rx:9,fill:"#fbfaf6"}),S("path",{d:"M-22 0 H22",stroke:colors.orange,"stroke-width":2}),S("path",{d:"M-14 -6 L-8 0 L-14 6 L-20 0 Z",fill:"#f8e9de",stroke:colors.orange}),S("circle",{r:4.5,fill:colors.orange}),S("path",{d:"M14 -6 L20 0 L14 6 L8 0 Z",fill:"#f8e9de",stroke:colors.orange}));return g} | ||
| function buildGraph(svg,layoutName){const config=layouts[layoutName],map={};svg.append(defs(),S("rect",{x:18,y:18,width:1164,height:684,rx:30,fill:"#fbfaf6",stroke:"#c5cad1","stroke-width":2}));config.layers.forEach(([id,x,y])=>map[id]={id,x,y,cx:x+53,cy:y+32});const edgeGroup=S("g"),completeGroup=S("g"),layerGroup=S("g"),actionGroup=S("g"),childGroup=S("g",{class:"invoke-chain"}),pointer=S("g",{class:"pointer",transform:`translate(${config.center[0]} ${config.center[1]-112})`}); | ||
| commonEdges.filter(([a,b])=>map[a]&&map[b]).forEach(([a,b],i)=>{const A=map[a],B=map[b],mx=(A.cx+B.cx)/2,my=(A.cy+B.cy)/2,qx=mx-10,qy=my+12,px=.25*A.cx+.5*qx+.25*B.cx,py=.25*A.cy+.5*qy+.25*B.cy,angle=Math.atan2(B.cy-A.cy,B.cx-A.cx)*180/Math.PI,edge=S("path",{class:`edge ${i%4===0?"soft":""}`,d:`M${A.cx} ${A.cy} Q${qx} ${qy} ${B.cx} ${B.cy}`,"data-edge-index":i});edgeGroup.append(edge);if(i%3===0){const glyph=completeGlyph(px,py,angle);glyph.setAttribute("data-edge-index",i);glyph.setAttribute("data-anchor",`${px},${py}`);completeGroup.append(glyph)}}); |
There was a problem hiding this comment.
Reserve complete markers for interaction completion
This loop adds a completeGlyph to every third ordinary commonEdges entry, so the architecture view depicts multiple complete operations between layers such as Contract and Context. Completion actually occurs at the interaction-level complete(inputGraph)/graph.submit(interactionNode) boundary and is not an ordinary graph-edge relation; using these markers throughout the graph teaches the wrong execution contract.
AGENTS.md reference: AGENTS.md:L7-L9
Useful? React with 👍 / 👎.
| pointer.append(S("path",{class:"current-link",d:"M49 0 H62",fill:"none"}),S("rect",{x:-49,y:-10,width:98,height:20,rx:10}),S("text",{x:0,y:3},"CURRENT · I₀")); | ||
| const goals=S("g");config.goals.forEach(([label,x,y,t])=>{const c=t==="orange"?colors.orange:t==="purple"?colors.purple:colors.green,g=S("g",{class:"goal",transform:`translate(${x} ${y})`});g.append(S("rect",{width:112,height:28,rx:12,fill:t==="orange"?"#f8e9de":t==="purple"?"#eee8f5":"#e5f1eb",stroke:c}),S("text",{x:56,y:18,fill:c},label));goals.append(g)}); | ||
| svg.append(edgeGroup,completeGroup,layerGroup,input,actionGroup,childGroup,pointer,goals);svg._graph={config,map,pointer,childGroup,selected:"build",stage:0};} | ||
| function nodeStory(id,index){const stories=mockNodes[id]||["Inspect graph work","Trace its inputs","Review its evidence","Choose its next action"];return{copy:stories[index]||stories[0],state:index===0?"accepted":index===1?"current work":index===2?"evidence attached":"action available",input:mockInputs[id]||"prior visible layers",action:invokeLayers.has(id)&&index>=2?"invoke → inputGraph":"expand → response layer"}} |
There was a problem hiding this comment.
Report GraphComplete's actual record states
When a user inspects nodes n2–n4, the state field reports index-derived labels such as current work, evidence attached, and action available, even though GraphComplete's explicit record states are draft, accepted, and stopped. Because the tooltip labels this value as state and mixes these labels with accepted, the prototype obscures the state contract it is intended to explain; keep status/role metadata separate from the real record state.
AGENTS.md reference: AGENTS.md:L9-L9
Useful? React with 👍 / 👎.
| function hideNodeTooltip(svg){const id=svg._graph?.pinnedLayer;if(id){const g=svg.querySelector(`.layer-${id}`);if(g)showLayerInspector(svg,id,g)}else svg.closest(".frame").querySelector(".tooltip")?.classList.remove("open")} | ||
| function selectNode(svg,g,event){svg.querySelectorAll(".layer").forEach(x=>x.classList.remove("selected"));g.classList.add("selected");svg._graph.selected=g.dataset.id;if(svg.closest(".variant-a")){const lens=svg.closest(".frame").querySelector(".lens"),frame=svg.closest(".frame").getBoundingClientRect(),box=g.getBoundingClientRect();lens.querySelector("h2").textContent=g.dataset.id[0].toUpperCase()+g.dataset.id.slice(1);lens.style.left=`${Math.min(frame.width-255,Math.max(14,box.left-frame.left+box.width*.65))}px`;lens.style.top=`${Math.min(frame.height-235,Math.max(58,box.top-frame.top-118))}px`;lens.classList.add("open")}if(svg.closest(".variant-c,.interactive-graph")){svg._graph.pinnedLayer=g.dataset.id;showLayerInspector(svg,g.dataset.id,g)}} | ||
| function setStage(svg,stage){svg._graph.stage=stage;svg.classList.remove("stage-0","stage-1","stage-2","stage-3","stage-4");svg.classList.add(`stage-${stage}`);svg.querySelectorAll(".layer").forEach(x=>x.classList.toggle("selected",stage>=1&&x.dataset.id==="build"));svg.querySelectorAll(".invoke-complete").forEach(x=>x.classList.toggle("hot",stage>=3));const p=svg._graph.pointer,label=p.querySelector("text");if(stage===4){const build=svg._graph.map.build,x=Math.min(1110,build.cx+150),responseY=Math.min(620,build.cy+265);p.setAttribute("transform",`translate(${x-115} ${responseY})`);label.textContent="CURRENT RESPONSE"}else{const [cx,cy]=svg._graph.config.center;p.setAttribute("transform",`translate(${cx} ${cy-112})`);label.textContent="CURRENT · I₀"}} | ||
| function runAction(svg,action){if(action==="expand"){svg.classList.add("dim-actions");setTimeout(()=>svg.classList.remove("dim-actions"),650)}else if(action==="invoke"){setStage(svg,2);setTimeout(()=>setStage(svg,3),550);setTimeout(()=>setStage(svg,4),1100)}else if(action==="complete"){setStage(svg,3)}} |
There was a problem hiding this comment.
Reveal the response at the Complete stage
Selecting stage 04 or clicking any Show/Resolve complete button calls setStage(svg, 3), but .child-response becomes visible only in stage 4. Consequently the step whose copy says the accepted response graph is attached still hides that graph, and clicking Complete in the initially completed D–G variants makes an already visible response disappear; stage 3 should expose the accepted response while stage 4 only advances the pointer.
Useful? React with 👍 / 👎.
| function showNodeTooltip(svg,id,index,node){const tip=svg.closest(".frame").querySelector(".tooltip"),frame=svg.closest(".frame").getBoundingClientRect(),box=node.getBoundingClientRect(),story=nodeStory(id,index);tip.classList.remove("layer-view");tip.querySelector("strong").textContent=`${id} · node n${index+1}`;tip.querySelector(".tooltip-copy").textContent=story.copy;tip.querySelector(".tooltip-state").textContent=story.state;tip.querySelector(".tooltip-input").textContent=story.input;tip.querySelector(".tooltip-action").textContent=story.action;positionTooltip(tip,frame,box,230,145);tip.classList.add("open")} | ||
| function showLayerInspector(svg,id,g){const tip=svg.closest(".frame").querySelector(".tooltip"),frame=svg.closest(".frame").getBoundingClientRect(),box=g.getBoundingClientRect(),stories=mockNodes[id]||[];tip.classList.add("layer-view");tip.querySelector("strong").textContent=`${id} layer · ${stories.length} nodes`;tip.querySelector(".tooltip-copy").textContent=layerPurposes[id]||"A bounded layer of connected graph work.";const list=tip.querySelector(".tooltip-node-list");list.innerHTML=stories.map((copy,index)=>`<div class="tooltip-node-row"><b>n${index+1}</b><span>${copy}</span><em>${index===0?"accepted":index===1?"work":index===2?"evidence":"action"}</em></div>`).join("");positionTooltip(tip,frame,box,280,245);tip.classList.add("open")} | ||
| function hideNodeTooltip(svg){const id=svg._graph?.pinnedLayer;if(id){const g=svg.querySelector(`.layer-${id}`);if(g)showLayerInspector(svg,id,g)}else svg.closest(".frame").querySelector(".tooltip")?.classList.remove("open")} | ||
| function selectNode(svg,g,event){svg.querySelectorAll(".layer").forEach(x=>x.classList.remove("selected"));g.classList.add("selected");svg._graph.selected=g.dataset.id;if(svg.closest(".variant-a")){const lens=svg.closest(".frame").querySelector(".lens"),frame=svg.closest(".frame").getBoundingClientRect(),box=g.getBoundingClientRect();lens.querySelector("h2").textContent=g.dataset.id[0].toUpperCase()+g.dataset.id.slice(1);lens.style.left=`${Math.min(frame.width-255,Math.max(14,box.left-frame.left+box.width*.65))}px`;lens.style.top=`${Math.min(frame.height-235,Math.max(58,box.top-frame.top-118))}px`;lens.classList.add("open")}if(svg.closest(".variant-c,.interactive-graph")){svg._graph.pinnedLayer=g.dataset.id;showLayerInspector(svg,g.dataset.id,g)}} |
There was a problem hiding this comment.
Refresh all lens fields for the selected layer
In variant A, clicking a non-Build layer updates only the lens heading. The eyebrow, description, input, context, and actions remain the hard-coded Build n2 contract, so selecting Contract, Research, or another layer produces a visibly mismatched detail lens; populate the entire lens from the selected layer/node or restrict selection to the contract it actually describes.
Useful? React with 👍 / 👎.
| .page{height:100%;display:grid;grid-template-rows:auto minmax(0,1fr);padding:18px 20px 72px;gap:13px} | ||
| header{display:grid;grid-template-columns:230px minmax(0,1fr) auto;align-items:end;gap:28px}.brand small{display:block;margin-bottom:5px;color:var(--orange);font-size:9px;font-weight:900;letter-spacing:.16em;text-transform:uppercase}.brand strong,.copy h1{font-family:Georgia,"Times New Roman",serif;font-weight:500}.brand strong{font-size:22px}.copy h1{margin:0 0 4px;font-size:clamp(22px,3vw,34px);letter-spacing:-.025em}.copy p{margin:0;color:var(--muted);font-size:11px}.prototype-id{padding:7px 10px;border:1px solid #c8cdd3;border-radius:999px;background:rgba(255,253,248,.7);font-size:9px;font-weight:850;letter-spacing:.08em;text-transform:uppercase} | ||
| .variant{display:none;min-height:0}.variant.active{display:block}.frame{height:100%;min-height:0;position:relative;overflow:hidden;border:1px solid #bfc5cd;border-radius:26px;background:#fbfaf6;box-shadow:var(--shadow)} | ||
| svg.graph{display:block;width:100%;height:100%;touch-action:none}.edge{fill:none;stroke:var(--line);stroke-width:2;marker-end:url(#gray);transition:opacity .25s,stroke .25s}.edge.soft{stroke-dasharray:5 6;opacity:.7}.layer{cursor:pointer;transition:opacity .2s}.layer .shell{fill:var(--surface);stroke:#8792a0;stroke-width:1.8;transition:stroke .2s,stroke-width .2s,filter .2s}.layer:hover .shell,.layer.selected .shell{stroke:var(--purple);stroke-width:3;filter:url(#shadow)}.node{fill:#f6f6f3;stroke:#8d98a5;stroke-width:1.4}.node.root{fill:var(--blue-soft);stroke:var(--blue)}.node.selected{fill:var(--purple-soft);stroke:var(--purple);stroke-width:2.5}.layer-label{font-size:8px;font-weight:850;letter-spacing:.7px;text-anchor:middle}.complete{opacity:.76;transition:opacity .2s,filter .2s}.complete.hot{opacity:1;filter:drop-shadow(0 0 6px rgba(180,92,45,.6));animation:pulse .8s ease-in-out infinite alternate}.expand-arrow{fill:none;stroke:var(--blue);stroke-width:3.5;marker-end:url(#blue);stroke-linecap:round}.invoke-arrow{fill:none;stroke:var(--purple);stroke-width:3.5;marker-end:url(#purple);stroke-linecap:round}.input-ring{fill:var(--surface);stroke:var(--purple);stroke-width:3}.input-title{font-size:10px;font-weight:900;fill:var(--purple);text-anchor:middle;letter-spacing:.7px}.pointer rect{fill:var(--purple-soft);stroke:var(--purple);stroke-width:1.4}.pointer text{font-size:7px;font-weight:900;fill:var(--purple);text-anchor:middle;letter-spacing:.25px}.pointer .current-link{opacity:0;stroke:var(--purple);stroke-width:1.5}.stage-4 .pointer .current-link{opacity:1}.goal rect{stroke-width:1.5}.goal text{font-size:8px;font-weight:850;text-anchor:middle}.invoke-chain,.child-response{opacity:0;transition:opacity .3s}.stage-2 .invoke-chain,.stage-3 .invoke-chain,.stage-4 .invoke-chain{opacity:1}.stage-3 .invoke-complete,.stage-4 .invoke-complete{opacity:1;filter:drop-shadow(0 0 7px rgba(180,92,45,.7))}.stage-4 .child-response{opacity:1}.graph.dim-continuity .edge,.graph.dim-continuity .layer{opacity:.15}.graph.dim-complete .complete,.graph.dim-complete .layer{opacity:.15}.graph.dim-actions .edge,.graph.dim-actions .complete,.graph.dim-actions .layer{opacity:.15} |
There was a problem hiding this comment.
Keep the selected legend category visible
When a user clicks the complete or continuity legend entries in variants C–G, the handler applies dim-complete or dim-continuity, but these selectors reduce the opacity of the very .complete or .edge elements being selected. The interaction therefore hides the category it promises to emphasize; dim the other categories instead, as the dim-actions treatment already does.
Useful? React with 👍 / 👎.
| .page{height:100%;display:grid;grid-template-rows:auto minmax(0,1fr);padding:18px 20px 72px;gap:13px} | ||
| header{display:grid;grid-template-columns:230px minmax(0,1fr) auto;align-items:end;gap:28px}.brand small{display:block;margin-bottom:5px;color:var(--orange);font-size:9px;font-weight:900;letter-spacing:.16em;text-transform:uppercase}.brand strong,.copy h1{font-family:Georgia,"Times New Roman",serif;font-weight:500}.brand strong{font-size:22px}.copy h1{margin:0 0 4px;font-size:clamp(22px,3vw,34px);letter-spacing:-.025em}.copy p{margin:0;color:var(--muted);font-size:11px}.prototype-id{padding:7px 10px;border:1px solid #c8cdd3;border-radius:999px;background:rgba(255,253,248,.7);font-size:9px;font-weight:850;letter-spacing:.08em;text-transform:uppercase} | ||
| .variant{display:none;min-height:0}.variant.active{display:block}.frame{height:100%;min-height:0;position:relative;overflow:hidden;border:1px solid #bfc5cd;border-radius:26px;background:#fbfaf6;box-shadow:var(--shadow)} | ||
| svg.graph{display:block;width:100%;height:100%;touch-action:none}.edge{fill:none;stroke:var(--line);stroke-width:2;marker-end:url(#gray);transition:opacity .25s,stroke .25s}.edge.soft{stroke-dasharray:5 6;opacity:.7}.layer{cursor:pointer;transition:opacity .2s}.layer .shell{fill:var(--surface);stroke:#8792a0;stroke-width:1.8;transition:stroke .2s,stroke-width .2s,filter .2s}.layer:hover .shell,.layer.selected .shell{stroke:var(--purple);stroke-width:3;filter:url(#shadow)}.node{fill:#f6f6f3;stroke:#8d98a5;stroke-width:1.4}.node.root{fill:var(--blue-soft);stroke:var(--blue)}.node.selected{fill:var(--purple-soft);stroke:var(--purple);stroke-width:2.5}.layer-label{font-size:8px;font-weight:850;letter-spacing:.7px;text-anchor:middle}.complete{opacity:.76;transition:opacity .2s,filter .2s}.complete.hot{opacity:1;filter:drop-shadow(0 0 6px rgba(180,92,45,.6));animation:pulse .8s ease-in-out infinite alternate}.expand-arrow{fill:none;stroke:var(--blue);stroke-width:3.5;marker-end:url(#blue);stroke-linecap:round}.invoke-arrow{fill:none;stroke:var(--purple);stroke-width:3.5;marker-end:url(#purple);stroke-linecap:round}.input-ring{fill:var(--surface);stroke:var(--purple);stroke-width:3}.input-title{font-size:10px;font-weight:900;fill:var(--purple);text-anchor:middle;letter-spacing:.7px}.pointer rect{fill:var(--purple-soft);stroke:var(--purple);stroke-width:1.4}.pointer text{font-size:7px;font-weight:900;fill:var(--purple);text-anchor:middle;letter-spacing:.25px}.pointer .current-link{opacity:0;stroke:var(--purple);stroke-width:1.5}.stage-4 .pointer .current-link{opacity:1}.goal rect{stroke-width:1.5}.goal text{font-size:8px;font-weight:850;text-anchor:middle}.invoke-chain,.child-response{opacity:0;transition:opacity .3s}.stage-2 .invoke-chain,.stage-3 .invoke-chain,.stage-4 .invoke-chain{opacity:1}.stage-3 .invoke-complete,.stage-4 .invoke-complete{opacity:1;filter:drop-shadow(0 0 7px rgba(180,92,45,.7))}.stage-4 .child-response{opacity:1}.graph.dim-continuity .edge,.graph.dim-continuity .layer{opacity:.15}.graph.dim-complete .complete,.graph.dim-complete .layer{opacity:.15}.graph.dim-actions .edge,.graph.dim-actions .complete,.graph.dim-actions .layer{opacity:.15} |
There was a problem hiding this comment.
Render ordinary graph edges as undirected
Every gray .edge receives marker-end:url(#gray), so the continuity network is rendered as a directed graph. The repository's GraphEdge contract stores an unordered endpoint pair and the README explicitly defines these edges as undirected; direction belongs to node-owned navigate or invoke actions. Readers of this architecture prototype will otherwise infer ordering and authority that the graph does not contain.
Useful? React with 👍 / 👎.
|
|
||
| <section class="variant variant-a" data-variant="A" data-name="Anchored lens"> | ||
| <div class="frame"> | ||
| <div class="grammar"><span><i class="swatch blue"></i>expand</span><span><i class="swatch purple"></i>invoke</span><span><i class="swatch orange"></i>complete</span></div> |
There was a problem hiding this comment.
Include reference in the action grammar
The legends present expand, invoke, and complete as the graph grammar but omit the other supported navigate relation, reference. Because the dense diagram includes shared and cyclic supporting context—the use case references are designed for—readers will interpret all navigation as expansion and miss the contract that expansion must be acyclic while references may revisit accepted context; add a distinct reference treatment and identify the relevant links.
Useful? React with 👍 / 👎.
| function hideNodeTooltip(svg){const id=svg._graph?.pinnedLayer;if(id){const g=svg.querySelector(`.layer-${id}`);if(g)showLayerInspector(svg,id,g)}else svg.closest(".frame").querySelector(".tooltip")?.classList.remove("open")} | ||
| function selectNode(svg,g,event){svg.querySelectorAll(".layer").forEach(x=>x.classList.remove("selected"));g.classList.add("selected");svg._graph.selected=g.dataset.id;if(svg.closest(".variant-a")){const lens=svg.closest(".frame").querySelector(".lens"),frame=svg.closest(".frame").getBoundingClientRect(),box=g.getBoundingClientRect();lens.querySelector("h2").textContent=g.dataset.id[0].toUpperCase()+g.dataset.id.slice(1);lens.style.left=`${Math.min(frame.width-255,Math.max(14,box.left-frame.left+box.width*.65))}px`;lens.style.top=`${Math.min(frame.height-235,Math.max(58,box.top-frame.top-118))}px`;lens.classList.add("open")}if(svg.closest(".variant-c,.interactive-graph")){svg._graph.pinnedLayer=g.dataset.id;showLayerInspector(svg,g.dataset.id,g)}} | ||
| function setStage(svg,stage){svg._graph.stage=stage;svg.classList.remove("stage-0","stage-1","stage-2","stage-3","stage-4");svg.classList.add(`stage-${stage}`);svg.querySelectorAll(".layer").forEach(x=>x.classList.toggle("selected",stage>=1&&x.dataset.id==="build"));svg.querySelectorAll(".invoke-complete").forEach(x=>x.classList.toggle("hot",stage>=3));const p=svg._graph.pointer,label=p.querySelector("text");if(stage===4){const build=svg._graph.map.build,x=Math.min(1110,build.cx+150),responseY=Math.min(620,build.cy+265);p.setAttribute("transform",`translate(${x-115} ${responseY})`);label.textContent="CURRENT RESPONSE"}else{const [cx,cy]=svg._graph.config.center;p.setAttribute("transform",`translate(${cx} ${cy-112})`);label.textContent="CURRENT · I₀"}} | ||
| function runAction(svg,action){if(action==="expand"){svg.classList.add("dim-actions");setTimeout(()=>svg.classList.remove("dim-actions"),650)}else if(action==="invoke"){setStage(svg,2);setTimeout(()=>setStage(svg,3),550);setTimeout(()=>setStage(svg,4),1100)}else if(action==="complete"){setStage(svg,3)}} |
There was a problem hiding this comment.
Cancel prior invoke animation timers
Each Invoke click starts two new uncancelled timers. If the user clicks again before the first animation finishes, the callbacks interleave—for example, the first run can advance to stage 4 and the second run then regress it to stage 3—causing the response and current pointer to disappear and reappear out of sequence. Track and cancel the outstanding animation or disable the trigger until the run completes.
Useful? React with 👍 / 👎.
| .variant-c .frame{background:#f7f5ef}.variant-c .grammar{left:14px;top:14px;flex-direction:column;align-items:flex-start}.variant-c .mock-question{position:absolute;z-index:4;left:50%;top:14px;width:min(430px,52%);transform:translateX(-50%);padding:9px 14px;border:1px solid #c9c0d5;border-radius:15px;background:rgba(255,253,248,.96);box-shadow:0 8px 26px rgba(34,49,63,.08);text-align:center}.variant-c .poster-title{position:absolute;z-index:3;left:16px;bottom:17px}.poster-title small{color:var(--orange);font-size:8px;font-weight:900;letter-spacing:.12em;text-transform:uppercase}.poster-title h2{margin:4px 0 0;font:500 22px/1.12 Georgia,serif}.variant-c .c-hint{left:auto;right:15px;bottom:14px} | ||
| .variant-d .frame{background:#faf9f5}.variant-d .mock-question{position:absolute;z-index:4;left:24px;top:20px;width:355px;padding:13px 16px;border-left:3px solid var(--purple);background:rgba(255,253,248,.94);box-shadow:0 9px 28px rgba(34,49,63,.08)}.variant-d .mock-question strong{text-align:left;font-size:15px}.variant-d .ports{display:flex;right:20px;top:20px}.variant-d .port{min-width:auto;border-radius:999px;padding:8px 12px}.variant-d .grammar{right:22px;bottom:17px}.variant-d .edition-label{position:absolute;z-index:3;left:25px;bottom:24px;max-width:250px;color:var(--muted);font-size:8px;font-weight:850;letter-spacing:.1em;text-transform:uppercase}.variant-d .layer .shell{fill:rgba(255,253,248,.88)} | ||
| .variant-e .frame{background:#f7f5ef}.variant-e .mock-question{position:absolute;z-index:4;left:22px;top:22px;width:300px;padding:13px 15px;border:1px solid #d5d0dd;border-radius:17px;background:rgba(255,253,248,.96);box-shadow:0 10px 28px rgba(34,49,63,.09)}.variant-e .mock-question strong{text-align:left}.variant-e .grammar{right:21px;bottom:20px;flex-direction:column;align-items:flex-start}.variant-e .ports{right:20px;top:20px}.variant-e .port.expand{display:none}.variant-e .frame:after{content:"SELECTED EXECUTION PATH";position:absolute;z-index:4;right:27px;top:111px;padding:5px 8px;border-radius:999px;background:rgba(255,253,248,.92);color:var(--purple);font-size:7px;font-weight:900;letter-spacing:.14em}.variant-e .layer:not(.layer-build) .shell{stroke-width:1.4}.variant-e .layer-build .shell{fill:#fbf8ff} | ||
| .variant-f .frame{display:grid;grid-template-columns:minmax(0,1fr) 292px;background:#f8f6f1}.variant-f .graph-pane{position:relative;min-width:0;overflow:hidden;border-right:1px solid #d2d6dc}.variant-f .mock-question{position:absolute;z-index:4;left:20px;top:18px;width:370px;padding:12px 15px;border:1px solid #d4cedd;border-radius:15px;background:rgba(255,253,248,.96);box-shadow:0 8px 24px rgba(34,49,63,.08)}.variant-f .mock-question strong{text-align:left}.variant-f .grammar{left:20px;bottom:18px}.detail-rail{position:relative;z-index:5;padding:24px 21px;background:rgba(255,253,248,.93)}.detail-rail small{color:var(--purple);font-size:8px;font-weight:900;letter-spacing:.13em;text-transform:uppercase}.detail-rail h2{margin:8px 0 5px;font:500 23px/1.08 Georgia,serif}.detail-rail>p{margin:0 0 18px;color:var(--muted);font-size:10px;line-height:1.5}.detail-grid{border-top:1px solid #dfe2e5}.detail-grid div{display:grid;grid-template-columns:54px 1fr;gap:9px;padding:10px 0;border-bottom:1px solid #e2e4e7;font-size:9px;line-height:1.4}.detail-grid b{color:var(--muted)}.detail-grid span{color:var(--ink)}.path-stack{display:grid;grid-template-columns:1fr;gap:7px;margin:20px 0}.path-step{position:relative;padding:10px 11px;border:1px solid #d5d9dd;border-radius:12px;background:#fff;font-size:9px;font-weight:850}.path-step.purple{border-color:#b9a7cf;color:var(--purple)}.path-step.orange{border-color:#d8a98f;color:var(--orange)}.path-step:not(:last-child):after{content:"↓";position:absolute;left:50%;bottom:-13px;z-index:2;color:var(--muted)}.rail-action{width:100%;border:0;border-radius:999px;background:var(--purple);color:#fff;padding:10px 12px;font-size:9px;font-weight:900;cursor:pointer}.rail-note{position:absolute;left:21px;right:21px;bottom:22px;color:var(--muted);font-size:8px;line-height:1.45} |
There was a problem hiding this comment.
Stack the node-detail rail on narrow viewports
On a phone-sized viewport, variant F retains a fixed 292px detail rail beside the graph even though the media query reduces the page to roughly the same total width. At 375px wide the graph pane is left only about 59px before borders and padding, while its 370px question card is clipped by the frame, making the diagram unusable; switch this frame to a single-column layout at the existing narrow breakpoint.
Useful? React with 👍 / 👎.
| function completeGlyph(x,y,angle=0,extra=""){const g=S("g",{class:`complete ${extra}`,transform:`translate(${x} ${y}) rotate(${angle})`});g.append(S("rect",{x:-24,y:-9,width:48,height:18,rx:9,fill:"#fbfaf6"}),S("path",{d:"M-22 0 H22",stroke:colors.orange,"stroke-width":2}),S("path",{d:"M-14 -6 L-8 0 L-14 6 L-20 0 Z",fill:"#f8e9de",stroke:colors.orange}),S("circle",{r:4.5,fill:colors.orange}),S("path",{d:"M14 -6 L20 0 L14 6 L8 0 Z",fill:"#f8e9de",stroke:colors.orange}));return g} | ||
| function buildGraph(svg,layoutName){const config=layouts[layoutName],map={};svg.append(defs(),S("rect",{x:18,y:18,width:1164,height:684,rx:30,fill:"#fbfaf6",stroke:"#c5cad1","stroke-width":2}));config.layers.forEach(([id,x,y])=>map[id]={id,x,y,cx:x+53,cy:y+32});const edgeGroup=S("g"),completeGroup=S("g"),layerGroup=S("g"),actionGroup=S("g"),childGroup=S("g",{class:"invoke-chain"}),pointer=S("g",{class:"pointer",transform:`translate(${config.center[0]} ${config.center[1]-112})`}); | ||
| commonEdges.filter(([a,b])=>map[a]&&map[b]).forEach(([a,b],i)=>{const A=map[a],B=map[b],mx=(A.cx+B.cx)/2,my=(A.cy+B.cy)/2,qx=mx-10,qy=my+12,px=.25*A.cx+.5*qx+.25*B.cx,py=.25*A.cy+.5*qy+.25*B.cy,angle=Math.atan2(B.cy-A.cy,B.cx-A.cx)*180/Math.PI,edge=S("path",{class:`edge ${i%4===0?"soft":""}`,d:`M${A.cx} ${A.cy} Q${qx} ${qy} ${B.cx} ${B.cy}`,"data-edge-index":i});edgeGroup.append(edge);if(i%3===0){const glyph=completeGlyph(px,py,angle);glyph.setAttribute("data-edge-index",i);glyph.setAttribute("data-anchor",`${px},${py}`);completeGroup.append(glyph)}}); | ||
| config.layers.forEach(([id,x,y])=>{const g=layerShape(id,x,y,id==="build");g.addEventListener("click",e=>selectNode(svg,g,e));if(svg.closest(".variant-c,.interactive-graph")){g.querySelectorAll(".node").forEach(node=>{node.addEventListener("pointerenter",()=>showNodeTooltip(svg,id,+node.dataset.nodeIndex,node));node.addEventListener("pointerleave",()=>hideNodeTooltip(svg))})}layerGroup.append(g)}); |
There was a problem hiding this comment.
Make graph-layer inspection keyboard accessible
All layer inspection is wired only through click and pointer-hover listeners on SVG <g> elements. Those groups have no tabindex, interactive role, or key handler, so keyboard-only users cannot open the lens or inspectors that expose the prototype's core node details; make each selectable layer focusable and support Enter/Space activation alongside pointer input.
Useful? React with 👍 / 👎.
Summary
Validation
npm run buildpassesnpm run checkreaches 1,046 passing tests but remains red ontest/evidence-capture-integrity.test.mjs: expected child status 0, received status 9