|
|
|
@@ -43,6 +43,50 @@ nav h2:first-child{margin-top:0}
|
|
|
|
|
.legend{display:flex;flex-wrap:wrap;gap:5px;padding:4px 6px}
|
|
|
|
|
.legend span{font-size:10.5px;padding:2px 6px;border-radius:99px;border:1px solid var(--line2);color:var(--dim)}
|
|
|
|
|
|
|
|
|
|
/* capability matrix and invariants */
|
|
|
|
|
.capsel{display:flex;gap:6px;margin:0 0 14px}
|
|
|
|
|
.capsel button{background:var(--panel2);border:1px solid var(--line);color:var(--dim);padding:5px 11px;border-radius:6px;cursor:pointer;font:inherit;font-size:12.5px}
|
|
|
|
|
.capsel button.on{background:#1d2c47;border-color:var(--line2);color:#fff}
|
|
|
|
|
table.mx{border-collapse:collapse;width:100%;font-size:12.5px}
|
|
|
|
|
table.mx th{text-align:left;font-weight:500;color:var(--dim2);font-size:10px;letter-spacing:.1em;text-transform:uppercase;padding:0 6px 7px;vertical-align:bottom}
|
|
|
|
|
table.mx th.d{text-align:center;width:64px}
|
|
|
|
|
table.mx tr.grp td{padding:16px 6px 5px;color:var(--dim);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;border-bottom:1px solid var(--line)}
|
|
|
|
|
table.mx tbody tr.cap{cursor:pointer}
|
|
|
|
|
table.mx tbody tr.cap:hover td{background:var(--panel2)}
|
|
|
|
|
table.mx tbody tr.cap.sel td{background:#1b2b45}
|
|
|
|
|
table.mx td{padding:4px 6px;border-bottom:1px solid #1b2230}
|
|
|
|
|
table.mx td.n{font-weight:600}
|
|
|
|
|
table.mx td.n em{font-style:normal;color:var(--dim2);font-weight:400;font-size:11px;margin-left:7px}
|
|
|
|
|
table.mx td.d{text-align:center}
|
|
|
|
|
.dot{display:inline-block;width:11px;height:11px;border-radius:3px;border:1px solid #0006}
|
|
|
|
|
.dot.yes{background:#4f9d69}.dot.partial{background:#c8992e}.dot.no{background:#3a4256}
|
|
|
|
|
.dot.speconly{background:#3a4256;border-style:dashed;border-color:#6b7896}
|
|
|
|
|
.gapc{font-size:10.5px;color:var(--dim2)}
|
|
|
|
|
.gapc.missing{color:#e08080}.gapc.unreachable{color:#e0a060}.gapc.partial{color:#c8992e}
|
|
|
|
|
.mark{font-size:10px;letter-spacing:.06em;text-transform:uppercase;padding:2px 7px;border-radius:99px;border:1px solid var(--line2)}
|
|
|
|
|
.mark.explicit{color:#7fbf7f;border-color:#3d6b43}
|
|
|
|
|
.mark.implied{color:#e0b050;border-color:#6b5a26}
|
|
|
|
|
.mark.unresolved{color:#e08080;border-color:#6b3838}
|
|
|
|
|
.inv{background:var(--panel2);border:1px solid var(--line);border-radius:9px;padding:12px 14px;margin-bottom:12px}
|
|
|
|
|
.inv h4{margin:0 0 6px;font-size:14px;display:flex;align-items:center;gap:10px}
|
|
|
|
|
.inv h4 span.n{color:var(--dim2);font-weight:400}
|
|
|
|
|
.inv .q{color:#e0b8b8;font-size:12.5px;margin:8px 0 0}
|
|
|
|
|
.bars{display:flex;gap:14px;margin:9px 0 4px;flex-wrap:wrap}
|
|
|
|
|
.bar{font-size:10.5px;color:var(--dim2)}
|
|
|
|
|
.bar b{display:block;font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim2);font-weight:500;margin-bottom:3px}
|
|
|
|
|
.bar .v{color:var(--fg);font-size:12px}
|
|
|
|
|
.bar .v.warn{color:#e08080}.bar .v.mid{color:#c8992e}.bar .v.ok{color:#7fbf7f}
|
|
|
|
|
.cchip{display:inline-block;background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:3px 8px;margin:3px 4px 0 0;
|
|
|
|
|
font-size:11.5px;cursor:pointer;color:var(--dim)}
|
|
|
|
|
.cchip:hover{border-color:var(--line2);color:var(--fg)}
|
|
|
|
|
.cchip.off{border-color:#6b5a26;color:#e0b050}
|
|
|
|
|
.cchip.pl{border-color:#6b3838;color:#e08080}
|
|
|
|
|
.crit{border-left:2px solid var(--line2);padding:0 0 0 10px;margin:0 0 11px}
|
|
|
|
|
.crit .vd{font-size:10px;letter-spacing:.08em;text-transform:uppercase;margin-right:8px}
|
|
|
|
|
.crit .vd.pass{color:#7fbf7f}.crit .vd.fail{color:#e08080}.crit .vd.blocked{color:#e0a060}
|
|
|
|
|
.crit .vd.untested{color:var(--dim2)}.crit .vd.unknown{color:#b79bf0}
|
|
|
|
|
.crit .rs{color:var(--dim2);font-size:11px}
|
|
|
|
|
.crit p{margin:5px 0 0;color:var(--dim);font-size:12px}
|
|
|
|
|
main{position:relative;overflow:auto;padding:18px 20px 60px}
|
|
|
|
|
.lane{margin-bottom:20px}
|
|
|
|
|
.lane-h{display:flex;align-items:baseline;gap:10px;margin:0 0 8px;cursor:pointer;user-select:none}
|
|
|
|
@@ -165,7 +209,7 @@ ol.steps .ev{display:block;color:var(--dim2);font-size:11.5px;margin-top:3px}
|
|
|
|
|
/*__DATA__*/
|
|
|
|
|
|
|
|
|
|
const byId = Object.fromEntries(ARCH.components.map(c => [c.id, c]));
|
|
|
|
|
const S = { view: 'v1', sel: null, flow: 'reminder', collapsed: {}, diaClosed: false, diaZoom: 1 };
|
|
|
|
|
const S = { view: 'v1', sel: null, selCap: null, capBy: 'section', flow: 'reminder', collapsed: {}, diaClosed: false, diaZoom: 1 };
|
|
|
|
|
|
|
|
|
|
/* ---------------- view definitions ---------------- */
|
|
|
|
|
const VIEWS = [
|
|
|
|
@@ -219,6 +263,10 @@ const VIEWS = [
|
|
|
|
|
['state layer', c => ['state.db'].includes(c.id) || c.type==='shared-state'],
|
|
|
|
|
['evaluation and gates', c => c.type==='test'],
|
|
|
|
|
]},
|
|
|
|
|
{ id:'c1', name:'6 · Capabilities', hint:'Every capability against the seven dimensions. Sorted by the spec, or by domain.', caps:true,
|
|
|
|
|
note:'<b>Nothing here is asserted.</b> The six build dimensions come from the <code>status</code> field of every component the capability maps to, and <code>verified</code> comes from a probe run against the deployed stack. A capability can be coded and unwired, wired and unconfigured, or configured and undeployed, and those are three different pieces of work — which is why this is not one <code>implemented</code> column. Source: <code>docs/capabilities/ledger.yaml</code>.' },
|
|
|
|
|
{ id:'c2', name:'7 · Invariants', hint:'The twelve cross-cutting rules, and which components participate in each.', inv:true,
|
|
|
|
|
note:'These rules run across all 51 capabilities and no capability\'s definition of done states any of them, so breaking one breaks many at once without producing a single failing criterion. <b>Target</b> is whether the rule is written down. <b>Implementation</b> is the status of the components that participate. <b>Runtime</b> is what the probe run observed for the capabilities it touches. Source: <code>docs/capabilities/invariants.yaml</code>, prose and evidence in <code>invariants.md</code>.' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/* ---------------- runtime flows ---------------- */
|
|
|
|
@@ -299,6 +347,8 @@ function renderView(){
|
|
|
|
|
const v = VIEWS.find(x=>x.id===S.view);
|
|
|
|
|
const main = document.getElementById('main');
|
|
|
|
|
if (v.flow) return renderFlow(main);
|
|
|
|
|
if (v.caps) return renderCaps(main);
|
|
|
|
|
if (v.inv) return renderInv(main);
|
|
|
|
|
let html = v.note ? `<div class="viewnote">${v.note}</div>` : '';
|
|
|
|
|
html += diagramPanel(diagramFileFor());
|
|
|
|
|
const used = new Set();
|
|
|
|
@@ -319,6 +369,137 @@ function renderView(){
|
|
|
|
|
requestAnimationFrame(drawWires);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------- capabilities and invariants ---------------- */
|
|
|
|
|
const DIMS = ['designed','code_present','wired','configured','deployed','reachable','verified'];
|
|
|
|
|
const DIMH = {designed:'design',code_present:'code',wired:'wired',configured:'config',
|
|
|
|
|
deployed:'deploy',reachable:'reach',verified:'verified'};
|
|
|
|
|
const capById = Object.fromEntries(CAPS.capabilities.map(c => [c.id, c]));
|
|
|
|
|
// A criterion is blocked when it was observed blocked, or when its reason names
|
|
|
|
|
// something outside the code as the thing in the way.
|
|
|
|
|
const BLOCKREASON = new Set(['configuration missing','deployment missing',
|
|
|
|
|
'external dependency unavailable','scenario missing']);
|
|
|
|
|
const dotCls = v => v==='spec-only' ? 'speconly' : v;
|
|
|
|
|
|
|
|
|
|
function capRow(c){
|
|
|
|
|
const cls = {'capability missing':'missing','capability exists but unreachable':'unreachable',
|
|
|
|
|
'capability partial':'partial'}[c.implementation.gap_class] || '';
|
|
|
|
|
const gap = c.implementation.gap_class==='none' ? '' : c.implementation.gap_class;
|
|
|
|
|
return `<tr class="cap" data-cap="${c.id}">
|
|
|
|
|
<td class="n">${c.title}${c.scope!=='v1'?'<em>deferred</em>':''}
|
|
|
|
|
<em class="gapc ${cls}">${gap}</em></td>
|
|
|
|
|
${DIMS.map(d=>`<td class="d"><span class="dot ${dotCls(c.implementation[d])}" title="${d}: ${c.implementation[d]}"></span></td>`).join('')}
|
|
|
|
|
</tr>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderCaps(main){
|
|
|
|
|
const v = VIEWS.find(x=>x.id===S.view);
|
|
|
|
|
const by = S.capBy;
|
|
|
|
|
const groups = {};
|
|
|
|
|
CAPS.capabilities.forEach(c => {
|
|
|
|
|
const keys = by==='domain' ? (c.domain.length?c.domain:['unassigned']) : [c.section];
|
|
|
|
|
keys.forEach(k => (groups[k] = groups[k]||[]).push(c));
|
|
|
|
|
});
|
|
|
|
|
const order = by==='domain' ? Object.keys(groups).sort()
|
|
|
|
|
: [...new Set(CAPS.capabilities.map(c=>c.section))];
|
|
|
|
|
main.innerHTML = `<div class="viewnote">${v.note}</div>
|
|
|
|
|
<div class="capsel">
|
|
|
|
|
<button data-by="section" class="${by==='section'?'on':''}">by spec section</button>
|
|
|
|
|
<button data-by="domain" class="${by==='domain'?'on':''}">by domain</button>
|
|
|
|
|
</div>
|
|
|
|
|
<table class="mx"><thead><tr><th>capability</th>
|
|
|
|
|
${DIMS.map(d=>`<th class="d">${DIMH[d]}</th>`).join('')}</tr></thead>
|
|
|
|
|
<tbody>${order.map(g=>`<tr class="grp"><td colspan="8">${g} · ${groups[g].length}</td></tr>`
|
|
|
|
|
+ groups[g].map(capRow).join('')).join('')}</tbody></table>`;
|
|
|
|
|
main.querySelectorAll('.capsel button').forEach(b=>b.onclick=()=>{S.capBy=b.dataset.by;renderCaps(main);});
|
|
|
|
|
main.querySelectorAll('tr.cap').forEach(r=>r.onclick=()=>selectCap(r.dataset.cap));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectCap(id){
|
|
|
|
|
S.sel = null; S.selCap = id;
|
|
|
|
|
document.querySelectorAll('tr.cap').forEach(r=>r.classList.toggle('sel', r.dataset.cap===id));
|
|
|
|
|
renderCapSide(id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function compChip(cid){
|
|
|
|
|
const c = byId[cid];
|
|
|
|
|
const k = c && (c.status==='configured-off' ? 'off'
|
|
|
|
|
: ['planned-unwired','dead','partially-wired'].includes(c.status) ? 'pl' : '');
|
|
|
|
|
return `<button class="cchip ${k}" data-id="${cid}" title="${c?c.status:'unknown'}">${cid}</button>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderCapSide(id){
|
|
|
|
|
const c = capById[id], side = document.getElementById('side');
|
|
|
|
|
const blockers = c.criteria.filter(cr => cr.verified==='blocked' || BLOCKREASON.has(cr.reason));
|
|
|
|
|
const qs = INV.filter(iv => iv.capabilities.includes(id) && iv.question);
|
|
|
|
|
const scen = c.scenarios || [];
|
|
|
|
|
side.innerHTML = `
|
|
|
|
|
<h3>${c.title}</h3>
|
|
|
|
|
<div class="sub">${c.section} · ${c.domain.join(', ')} · ${c.scope}${
|
|
|
|
|
c.implementation.gap_class==='none'?'':' · '+c.implementation.gap_class}</div>
|
|
|
|
|
<p>${c.state}</p>
|
|
|
|
|
<section><h4>Implementation</h4>
|
|
|
|
|
<div class="bars">${DIMS.map(d=>{
|
|
|
|
|
const v = c.implementation[d];
|
|
|
|
|
const k = v==='yes'?'ok':v==='partial'?'mid':'warn';
|
|
|
|
|
return `<div class="bar"><b>${DIMH[d]}</b><span class="v ${k}">${v}</span></div>`;
|
|
|
|
|
}).join('')}</div></section>
|
|
|
|
|
<section><h4>Components — ${(c.components||[]).length}</h4>
|
|
|
|
|
${(c.components||[]).length ? c.components.map(compChip).join('')
|
|
|
|
|
: '<div class="empty" style="margin:0">Nothing carries this capability. That is the finding.</div>'}</section>
|
|
|
|
|
<section><h4>Definition of done — ${c.criteria.length}</h4>
|
|
|
|
|
${c.criteria.map(cr=>`<div class="crit">
|
|
|
|
|
<span class="vd ${cr.verified}">${cr.verified}</span><span class="rs">${cr.reason}</span>
|
|
|
|
|
<p>${cr.text}</p>
|
|
|
|
|
${cr.detail?`<p style="color:var(--dim2)">${cr.detail}</p>`:''}
|
|
|
|
|
${(cr.evidence||[]).length?`<p class="mono" style="font-size:11px;color:var(--dim2)">${cr.evidence.join('<br>')}</p>`:''}
|
|
|
|
|
</div>`).join('')}</section>
|
|
|
|
|
<section><h4>Blockers — ${blockers.length}</h4>
|
|
|
|
|
${blockers.length ? '<ul class="plain">'+blockers.map(b=>`<li>${b.reason} · <span class="mono">${b.id}</span></li>`).join('')+'</ul>'
|
|
|
|
|
: '<div class="empty" style="margin:0">none. Nothing outside the code is in the way.</div>'}</section>
|
|
|
|
|
<section><h4>Scenarios — ${scen.length}</h4>
|
|
|
|
|
${scen.length ? '<ul class="plain">'+scen.map(x=>`<li class="mono">${x.name} ${x.exists?'':'<span class="b off">absent from disk</span>'}</li>`).join('')+'</ul>'
|
|
|
|
|
: '<div class="empty" style="margin:0">none named</div>'}</section>
|
|
|
|
|
<section><h4>Unresolved product questions — ${qs.length}</h4>
|
|
|
|
|
${qs.length ? qs.map(iv=>`<div class="crit"><span class="mark ${iv.mark}">invariant ${iv.id}</span>
|
|
|
|
|
<p>${iv.question}</p></div>`).join('')
|
|
|
|
|
: '<div class="empty" style="margin:0">none</div>'}</section>`;
|
|
|
|
|
side.querySelectorAll('.cchip').forEach(b=>b.onclick=()=>{S.sel=b.dataset.id;renderSide(b.dataset.id);});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function invRollup(iv){
|
|
|
|
|
const comps = iv.components.map(x=>byId[x]).filter(Boolean);
|
|
|
|
|
const bad = comps.filter(c=>c.status!=='implemented'&&c.status!=='temporary').length;
|
|
|
|
|
const caps = iv.capabilities.map(x=>capById[x]).filter(Boolean);
|
|
|
|
|
const ver = caps.filter(c=>c.implementation.verified==='yes').length;
|
|
|
|
|
const part = caps.filter(c=>c.implementation.verified==='partial').length;
|
|
|
|
|
return {comps, bad, caps, ver, part};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderInv(main){
|
|
|
|
|
const v = VIEWS.find(x=>x.id===S.view);
|
|
|
|
|
main.innerHTML = `<div class="viewnote">${v.note}</div>` + INV.map(iv=>{
|
|
|
|
|
const r = invRollup(iv);
|
|
|
|
|
const mk = iv.mark==='explicit'?'ok':iv.mark==='implied'?'mid':'warn';
|
|
|
|
|
const ik = r.bad?'mid':'ok';
|
|
|
|
|
const vk = r.ver===r.caps.length?'ok':(r.ver+r.part)?'mid':'warn';
|
|
|
|
|
return `<div class="inv" data-inv="${iv.id}">
|
|
|
|
|
<h4><span class="n">${iv.id}</span> ${iv.title}
|
|
|
|
|
<span class="mark ${iv.mark}">${iv.mark}${iv.split?' · split':''}</span></h4>
|
|
|
|
|
<div class="bars">
|
|
|
|
|
<div class="bar"><b>target</b><span class="v ${mk}">${
|
|
|
|
|
iv.mark==='explicit'?'written down':iv.mark==='implied'?'not stated':'no answer exists'}</span></div>
|
|
|
|
|
<div class="bar"><b>implementation</b><span class="v ${ik}">${r.comps.length} components, ${r.bad} not live</span></div>
|
|
|
|
|
<div class="bar"><b>runtime verification</b><span class="v ${vk}">${r.ver} of ${r.caps.length} capabilities verified${r.part?', '+r.part+' partly':''}</span></div>
|
|
|
|
|
</div>
|
|
|
|
|
${iv.question?`<p class="q">${iv.question}</p>`:''}
|
|
|
|
|
<div style="margin-top:8px">${iv.capabilities.map(c=>`<button class="cchip" data-cap="${c}">${c}</button>`).join('')}</div>
|
|
|
|
|
<div style="margin-top:4px">${iv.components.map(compChip).join('')}</div>
|
|
|
|
|
</div>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
main.querySelectorAll('.cchip[data-id]').forEach(b=>b.onclick=()=>{S.sel=b.dataset.id;renderSide(b.dataset.id);});
|
|
|
|
|
main.querySelectorAll('.cchip[data-cap]').forEach(b=>b.onclick=()=>renderCapSide(b.dataset.cap));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderFlow(main){
|
|
|
|
|
const f = FLOWS[S.flow];
|
|
|
|
|
main.innerHTML = `${diagramPanel(FLOWS[S.flow].file)}<div class="viewnote"><b>Three requests, traced through real code.</b> Steps marked in red are branches, fallbacks or refusals the implementation actually takes. Click a component name to open its record. The Mermaid sequence source for each flow is under the panel on the right.</div>
|
|
|
|
@@ -477,10 +658,15 @@ document.getElementById('dirty').textContent = ARCH.working_tree;
|
|
|
|
|
document.getElementById('views').innerHTML = VIEWS.map(v=>`<button class="viewbtn" data-v="${v.id}">${v.name}<small>${v.hint}</small></button>`).join('');
|
|
|
|
|
document.getElementById('legend').innerHTML = [...new Set(ARCH.components.map(c=>c.type))].sort().map(t=>`<span>${t}</span>`).join('');
|
|
|
|
|
|
|
|
|
|
function setView(id){ S.view=id; S.sel=null;
|
|
|
|
|
function setView(id){ S.view=id; S.sel=null; S.selCap=null;
|
|
|
|
|
document.querySelectorAll('.viewbtn').forEach(b=>b.classList.toggle('on', b.dataset.v===id));
|
|
|
|
|
renderView();
|
|
|
|
|
document.getElementById('side').innerHTML = '<div class="empty">Select a component to see its responsibility, the files and symbols it was read from, and every relation in and out.</div>';
|
|
|
|
|
const v = VIEWS.find(x=>x.id===id);
|
|
|
|
|
document.getElementById('side').innerHTML = v.caps
|
|
|
|
|
? '<div class="empty">Select a capability to see its definition of done, every verdict and its evidence, the components that carry it, its blockers and the product questions it waits on.<br><br>A dot is never an opinion. Six of the seven come from component status, the seventh from a probe run.</div>'
|
|
|
|
|
: v.inv
|
|
|
|
|
? '<div class="empty">Twelve rules that run across all 51 capabilities. Click a capability or a component to open its record.<br><br>An unresolved rule is a product question, not a defect.</div>'
|
|
|
|
|
: '<div class="empty">Select a component to see its responsibility, the files and symbols it was read from, and every relation in and out.</div>';
|
|
|
|
|
}
|
|
|
|
|
document.querySelectorAll('.viewbtn').forEach(b=>b.onclick=()=>setView(b.dataset.v));
|
|
|
|
|
['tLow','tMed','tOff','tUndeployed','tPlanned','tWires'].forEach(k=>
|
|
|
|
|