forma-arch 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -1
- package/lib/check.mjs +8 -1
- package/lib/docmap.mjs +43 -4
- package/lib/gen.mjs +13 -6
- package/lib/schema/c4-model.schema.json +314 -1
- package/lib/validate.mjs +219 -7
- package/lib/verify.mjs +10 -3
- package/lib/viewer/c4-hologram.html +241 -45
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@ h1{font-size:16px;letter-spacing:.16em;color:var(--cyan);text-shadow:0 0 14px rg
|
|
|
21
21
|
.sub{font-size:10.5px;color:var(--sec);margin-top:3px}
|
|
22
22
|
.top{display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap;margin-bottom:8px}
|
|
23
23
|
.ctrls{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
24
|
+
#legacytime{display:inline-flex;gap:8px}
|
|
24
25
|
button,select{font:inherit;font-size:10.5px;letter-spacing:.09em;cursor:pointer;border-radius:6px;padding:6px 12px;
|
|
25
26
|
background:transparent;border:1px solid var(--border);color:var(--sec)}
|
|
26
27
|
button:hover,select:hover{background:rgba(62,231,255,.10);color:var(--cyan)}
|
|
@@ -28,6 +29,21 @@ button.on{background:rgba(62,231,255,.18);color:var(--cyan);border-color:var(--c
|
|
|
28
29
|
#back{border-color:var(--cyan);color:var(--cyan)}
|
|
29
30
|
#back[disabled]{opacity:.28;cursor:default;border-color:var(--border);color:var(--ter)}
|
|
30
31
|
select#skin option{background:#08202e;color:var(--nm)}
|
|
32
|
+
#timeline{display:none;margin:10px 0 8px;padding:9px 10px 7px;border:1px solid var(--stageborder);border-radius:9px;background:var(--stagebg)}
|
|
33
|
+
#timeline-track{display:flex;align-items:flex-start;position:relative;overflow-x:auto;padding-bottom:2px}
|
|
34
|
+
#timeline-track:before{content:"";position:absolute;left:28px;right:28px;top:7px;height:1px;background:var(--border)}
|
|
35
|
+
.cpstep{position:relative;z-index:1;flex:1 0 118px;min-width:118px;border:0;padding:0 5px;background:transparent;text-align:center;color:var(--ter);font-size:9px;letter-spacing:.06em}
|
|
36
|
+
.cpstep:hover{background:transparent;color:var(--nm)}
|
|
37
|
+
.cpstep .dot{display:block;width:15px;height:15px;margin:0 auto 5px;border:2px solid var(--border);background:var(--bg);border-radius:50%}
|
|
38
|
+
.cpstep .cpbadge{display:block;margin-top:2px;color:var(--ter2);font-size:8px;letter-spacing:.02em;white-space:normal}
|
|
39
|
+
.cpstep.real .dot{border-color:var(--done);background:var(--done)}
|
|
40
|
+
.cpstep.active{color:var(--cyan)}
|
|
41
|
+
.cpstep.active .dot{border-color:var(--cyan);background:var(--bg);box-shadow:0 0 10px rgba(62,231,255,.8)}
|
|
42
|
+
#checkpoint-summary{text-align:right;color:var(--ter2);font-size:8.5px;letter-spacing:.08em;margin-top:5px;min-height:11px}
|
|
43
|
+
#checkpoint-summary b{color:var(--cyan);font-weight:600}
|
|
44
|
+
#checkpoint-changes{display:grid;gap:3px;margin-top:7px;max-height:92px;overflow:auto}
|
|
45
|
+
.cpchange{color:var(--text);font-size:9px;line-height:1.35;padding-left:8px;border-left:2px solid var(--cyan)}
|
|
46
|
+
.cpchange b{color:var(--cyan);font-weight:600;letter-spacing:.08em}.cpchange span{color:var(--ter2)}
|
|
31
47
|
#crumb{font-size:11px;color:var(--sec);margin:8px 0}
|
|
32
48
|
#crumb .agg{float:right;color:var(--ter2);letter-spacing:.05em}
|
|
33
49
|
#crumb .agg i{display:inline-block;width:7px;height:7px;border-radius:50%;margin:0 3px 0 8px;vertical-align:baseline}
|
|
@@ -52,6 +68,12 @@ svg{width:100%;height:100%;display:block}
|
|
|
52
68
|
.nd .drillhit:hover{fill:rgba(62,231,255,.12)}
|
|
53
69
|
.s-done rect{stroke:var(--done);filter:drop-shadow(0 0 7px rgba(47,224,138,.55))}
|
|
54
70
|
.s-done .tt,.s-done .pp{fill:var(--done)}
|
|
71
|
+
/* The hollow green the legend has promised since #38: a done a repo document DECLARES is not a
|
|
72
|
+
done a closed issue PROVES, and until now the canvas drew them identically. Hollow = the glow
|
|
73
|
+
comes off, which is exactly what the legend swatch says (filled pill vs outline).
|
|
74
|
+
ponytail: the blueprint skin zeroes every filter, so there the two greens still read alike —
|
|
75
|
+
upgrade path is a second, skin-independent mark if anyone actually presents in blueprint. */
|
|
76
|
+
.s-done.decl rect{filter:none}
|
|
55
77
|
.s-prog rect{stroke:var(--prog);filter:drop-shadow(0 0 7px rgba(255,199,77,.5))}
|
|
56
78
|
.s-prog .tt,.s-prog .pp{fill:var(--prog)}
|
|
57
79
|
.s-next rect{stroke:var(--next);stroke-width:2.2;filter:drop-shadow(0 0 11px rgba(62,231,255,.75))}
|
|
@@ -60,14 +82,20 @@ svg{width:100%;height:100%;display:block}
|
|
|
60
82
|
.s-plan .tt,.s-plan .pp{fill:var(--planTxt)}
|
|
61
83
|
.s-prob rect{stroke:var(--prob);filter:drop-shadow(0 0 8px rgba(255,93,93,.6))}
|
|
62
84
|
.s-prob .tt,.s-prob .pp{fill:var(--prob)}
|
|
63
|
-
.
|
|
85
|
+
/* NO dash. legHint teaches the reader that a dashed box is `da costruire`, and wearing that dash
|
|
86
|
+
made "nobody has ruled on this" read as "this does not exist" — on a repo where the module was
|
|
87
|
+
right there. Not-ruled is now a plain grey outline: no dash (planned), no glow (done). */
|
|
88
|
+
.s-unk rect{stroke:var(--unk)}
|
|
64
89
|
.s-unk .tt,.s-unk .pp{fill:var(--unk)}
|
|
90
|
+
.nd.delta rect{stroke:var(--cyan)!important;stroke-width:2.3!important;filter:drop-shadow(0 0 12px rgba(62,231,255,.72))!important}
|
|
91
|
+
.nd.delta .tt{fill:var(--cyan)!important}
|
|
65
92
|
.mode-t .nd rect{stroke:var(--next)!important;stroke-dasharray:none!important;filter:drop-shadow(0 0 8px rgba(62,231,255,.5))!important}
|
|
66
93
|
.mode-t .nd .tt,.mode-t .nd .pp{fill:var(--next)!important}
|
|
67
94
|
.edge{stroke:var(--edge);stroke-width:1.3;fill:none;marker-end:url(#ar);cursor:help}
|
|
68
95
|
.edge.flow{stroke-dasharray:5 7;animation:dash 1.1s linear infinite}
|
|
69
96
|
.edge.plan{stroke:var(--edgeplan);stroke-dasharray:3 6;opacity:.6;marker-end:url(#arp)}
|
|
70
97
|
.edge.hot{stroke:var(--cyan);stroke-width:2.6;opacity:1}
|
|
98
|
+
.edge.delta{stroke:var(--cyan);stroke-width:2.5;opacity:1;filter:drop-shadow(0 0 4px rgba(62,231,255,.7))}
|
|
71
99
|
/* A level past the label threshold is a hairball, and no layout saves it: on a real Go repo 172 of
|
|
72
100
|
189 arrows cross a box that is not one of their endpoints, and that stays 85-91% at EVERY grid
|
|
73
101
|
shape tried. Only drawing less helps. They stop competing with the names — structure survives as
|
|
@@ -121,7 +149,7 @@ svg.dense .edge.hot{opacity:1;stroke-width:2.6}
|
|
|
121
149
|
@media print{
|
|
122
150
|
body{background:#fff}
|
|
123
151
|
#wrap{background:none;padding:0}
|
|
124
|
-
.ctrls,#legend,.sub,#etip,#pmenu{display:none!important}
|
|
152
|
+
.ctrls,#timeline,#legend,.sub,#etip,#pmenu{display:none!important}
|
|
125
153
|
#stage{border:1px solid #999;height:auto;min-height:0}
|
|
126
154
|
#stage svg{height:70vh}
|
|
127
155
|
}
|
|
@@ -134,6 +162,7 @@ html[data-skin="blueprint"] h1{text-shadow:none}
|
|
|
134
162
|
html[data-skin="blueprint"] .s-done rect,html[data-skin="blueprint"] .s-prog rect,html[data-skin="blueprint"] .s-next rect,html[data-skin="blueprint"] .s-prob rect{filter:none}
|
|
135
163
|
html[data-skin="blueprint"] .nd.pulse rect{animation:none}
|
|
136
164
|
html[data-skin="blueprint"] button.on{text-shadow:none}
|
|
165
|
+
html[data-skin="blueprint"] .nd.delta rect,html[data-skin="blueprint"] .edge.delta{filter:none}
|
|
137
166
|
</style>
|
|
138
167
|
</head>
|
|
139
168
|
<body>
|
|
@@ -143,8 +172,8 @@ html[data-skin="blueprint"] button.on{text-shadow:none}
|
|
|
143
172
|
<div class="sub" id="subhelp"></div></div>
|
|
144
173
|
<div class="ctrls">
|
|
145
174
|
<button id="back" type="button" disabled></button>
|
|
146
|
-
<button id="bnow" class="on" type="button"></button>
|
|
147
|
-
<button id="btgt" type="button"></button>
|
|
175
|
+
<span id="legacytime"><button id="bnow" class="on" type="button"></button>
|
|
176
|
+
<button id="btgt" type="button"></button></span>
|
|
148
177
|
<button id="brf" type="button"></button>
|
|
149
178
|
<button id="blbl" type="button"></button>
|
|
150
179
|
<button id="brl" type="button"></button>
|
|
@@ -160,6 +189,11 @@ html[data-skin="blueprint"] button.on{text-shadow:none}
|
|
|
160
189
|
<select id="skin" title="skin"></select>
|
|
161
190
|
</div>
|
|
162
191
|
</div>
|
|
192
|
+
<nav id="timeline">
|
|
193
|
+
<div id="timeline-track"></div>
|
|
194
|
+
<div id="checkpoint-summary"></div>
|
|
195
|
+
<div id="checkpoint-changes"></div>
|
|
196
|
+
</nav>
|
|
163
197
|
<div id="crumb"></div>
|
|
164
198
|
<div id="stamp"></div>
|
|
165
199
|
<div id="stage"><div id="etip"></div><div id="err" style="display:none"></div></div>
|
|
@@ -174,6 +208,7 @@ html[data-skin="blueprint"] button.on{text-shadow:none}
|
|
|
174
208
|
<span><i style="background:#2d5a72"></i><span id="lg-plan"></span></span>
|
|
175
209
|
<span><i style="background:#ff5d5d"></i><span id="lg-prob"></span></span>
|
|
176
210
|
<span><i style="background:#7d8f99"></i><span id="lg-unk"></span></span>
|
|
211
|
+
<span id="lg-checkpoint-wrap" style="display:none"><i style="border:1.5px solid #3ee7ff"></i><span id="lg-checkpoint"></span></span>
|
|
177
212
|
<span class="hint" id="lg-hint"></span>
|
|
178
213
|
</div>
|
|
179
214
|
<div id="detail"></div>
|
|
@@ -191,19 +226,21 @@ var STRINGS={
|
|
|
191
226
|
printExport:"PRINT / EXPORT", exportSvg:"Export SVG", exportPng:"Export PNG", exportLayout:"Export layout JSON", print:"Print",
|
|
192
227
|
legDone:"DONE (proven)", legDecl:"DONE (declared)", legProg:"IN PROGRESS", legNext:"NEXT", legPlan:"PLAN ONLY", legProb:"PROBLEM",
|
|
193
228
|
legUnk:"UNKNOWN (no curated state)", stUnk:"?",
|
|
194
|
-
detDecl:"Declared in", detCover:"Coverage", coverWhole:"the document names this module itself",
|
|
229
|
+
detDecl:"Declared in", detCover:"Coverage", detRoll:"Rolled up from", rollPart:"mean of the {n} of {t} parts inside that anyone ruled on", coverWhole:"the document names this module itself",
|
|
195
230
|
coverPart:"{n} of {t} units named by the document",
|
|
196
|
-
legHint:"— animated arrow = active flow · dashed = to build",
|
|
231
|
+
legHint:"— animated arrow = active flow · dashed = to build", legCheckpoint:"CHANGE IN THIS CHECKPOINT",
|
|
232
|
+
timelineLabel:"Architecture checkpoints", asIs:"AS-IS", asIsBadge:"verified code", cumulativeHint:"the graph is always cumulative",
|
|
233
|
+
deltaAdd:"ADD", deltaUpdate:"UPDATE", deltaRewire:"REWIRE", deltaRemove:"REMOVE",
|
|
197
234
|
crumbRoot:"Context", crumbLevel:"C4", lvlContext:"CONTEXT",
|
|
198
235
|
lvlNames:{context:"Context",container:"Container",component:"Components",leaf:"Leaves"},
|
|
199
|
-
detClose:"[x] close", detFunc:"What it does", detNow:"Current state (facts)",
|
|
236
|
+
detClose:"[x] close", detFunc:"What it does", detNow:"Current state (facts)", detCheckpoint:"At this checkpoint", detChange:"Change from previous",
|
|
200
237
|
detTgt:"Target state (project)", detSrc:"Verification source", detIssue:"Issue",
|
|
201
238
|
detTarget:"TARGET",
|
|
202
239
|
rosterSearch:"filter…", rosterCount:"members", rosterEmpty:"no match", aggProgress:"progress",
|
|
203
|
-
stampBase:"Fact base:", stampMethod:"method:", stampVerify:"code+topology", staticSrc:"(static source)",
|
|
240
|
+
stampBase:"Fact base:", stampMethod:"method:", stampVerify:"code+topology", stampProjection:"projection:", staticSrc:"(static source)",
|
|
204
241
|
errEmpty:"Model unreachable or empty.",
|
|
205
242
|
errNotFound:"Model not found (./c4-model.json). Open via `forma serve` or inject window.__C4_MODEL__.",
|
|
206
|
-
errUnavail:"Model unavailable."
|
|
243
|
+
errUnavail:"Model unavailable.", errTimeline:"Invalid architecture timeline."
|
|
207
244
|
},
|
|
208
245
|
it:{
|
|
209
246
|
subhelp:"click = spiega (ogni livello) · doppio-click o [+] = entra · trascina per spostare · hover su una freccia = etichetta · ESC/BACK",
|
|
@@ -213,19 +250,21 @@ var STRINGS={
|
|
|
213
250
|
printExport:"STAMPA / ESPORTA", exportSvg:"Esporta SVG", exportPng:"Esporta PNG", exportLayout:"Esporta layout JSON", print:"Stampa",
|
|
214
251
|
legDone:"FATTO (provato)", legDecl:"FATTO (dichiarato)", legProg:"IN CORSO", legNext:"PROSSIMO", legPlan:"SOLO PROGETTO", legProb:"PROBLEMA",
|
|
215
252
|
legUnk:"IGNOTO (stato non curato)", stUnk:"?",
|
|
216
|
-
detDecl:"Dichiarato in", detCover:"Copertura", coverWhole:"il documento nomina il modulo stesso",
|
|
253
|
+
detDecl:"Dichiarato in", detCover:"Copertura", detRoll:"Somma dal basso", rollPart:"media delle {n} parti su {t} qui dentro su cui qualcuno si e pronunciato", coverWhole:"il documento nomina il modulo stesso",
|
|
217
254
|
coverPart:"{n} unità su {t} nominate dal documento",
|
|
218
|
-
legHint:"— freccia animata = flusso attivo · tratteggio = da costruire",
|
|
255
|
+
legHint:"— freccia animata = flusso attivo · tratteggio = da costruire", legCheckpoint:"CAMBIO IN QUESTO CHECKPOINT",
|
|
256
|
+
timelineLabel:"Checkpoint architetturali", asIs:"AS-IS", asIsBadge:"codice verificato", cumulativeHint:"il grafo è sempre cumulativo",
|
|
257
|
+
deltaAdd:"ADD", deltaUpdate:"UPDATE", deltaRewire:"REWIRE", deltaRemove:"REMOVE",
|
|
219
258
|
crumbRoot:"Contesto", crumbLevel:"C4", lvlContext:"CONTESTO",
|
|
220
259
|
lvlNames:{context:"Contesto",container:"Container",component:"Componenti",leaf:"Foglie"},
|
|
221
|
-
detClose:"[x] chiudi", detFunc:"Cosa fa", detNow:"Stato attuale (fatti)",
|
|
260
|
+
detClose:"[x] chiudi", detFunc:"Cosa fa", detNow:"Stato attuale (fatti)", detCheckpoint:"A questo checkpoint", detChange:"Modifica dal precedente",
|
|
222
261
|
detTgt:"Stato finito (progetto)", detSrc:"Fonte verifica", detIssue:"Issue",
|
|
223
262
|
detTarget:"TARGET",
|
|
224
263
|
rosterSearch:"filtra…", rosterCount:"elementi", rosterEmpty:"nessun risultato", aggProgress:"avanzamento",
|
|
225
|
-
stampBase:"Base fatti:", stampMethod:"metodo:", stampVerify:"code+topology", staticSrc:"(fonte statica)",
|
|
264
|
+
stampBase:"Base fatti:", stampMethod:"metodo:", stampVerify:"code+topology", stampProjection:"proiezione:", staticSrc:"(fonte statica)",
|
|
226
265
|
errEmpty:"Modello non raggiungibile o vuoto.",
|
|
227
266
|
errNotFound:"Modello non trovato (./c4-model.json). Apri via `forma serve` o inietta window.__C4_MODEL__.",
|
|
228
|
-
errUnavail:"Modello non disponibile."
|
|
267
|
+
errUnavail:"Modello non disponibile.", errTimeline:"Timeline architetturale non valida."
|
|
229
268
|
}
|
|
230
269
|
};
|
|
231
270
|
var LANG="en", STR=STRINGS.en;
|
|
@@ -260,8 +299,50 @@ var STMAP={done:"done","in-progress":"prog",next:"next",planned:"plan",problem:"
|
|
|
260
299
|
// would contribute ONE unknown instead of nine, so 25/53 would print as 25/45 and the coverage
|
|
261
300
|
// would read better than it is — which is the number this whole function exists to keep honest.
|
|
262
301
|
// One consequence, deliberate: on a grouped level the dots count units (53), not boxes (6).
|
|
302
|
+
// What a box that GROUPS others can honestly say about itself. Nobody ruled on a domain — `gen`
|
|
303
|
+
// refuses to derive a verdict, everywhere, on purpose — so its badge read `?` while the 22 packages
|
|
304
|
+
// inside it carried 14 verdicts. Four of six domains showed `?` on the one screen a stakeholder is
|
|
305
|
+
// shown, and the board looked less finished than the repo was.
|
|
306
|
+
// The way out is not to invent a verdict but to report a FACT: the mean of the descendants somebody
|
|
307
|
+
// did rule on, and how many that was. `100%` alone would be the invented green this tool exists to
|
|
308
|
+
// kill — 14 of 14 ruled done says nothing about the other 8 — so the coverage is not optional
|
|
309
|
+
// decoration, it is half the claim. Same shape as the level tally and the docmap coverage.
|
|
310
|
+
// Returns null when the box has its own verdict (it speaks for itself) or when nothing below it
|
|
311
|
+
// was ruled (there is no fact to report).
|
|
312
|
+
// A VERDICT is what counts as ruled, not a percentage: a document declares a capability finished,
|
|
313
|
+
// it does not measure it, so those nodes carry `done` with no completion at all. Counting only the
|
|
314
|
+
// numbers put `?` back on a domain holding nine packages a document calls finished — the same
|
|
315
|
+
// silence #42 closed. The mean is null when nobody below carries one; the coverage is not.
|
|
316
|
+
function rollStatus(node,kidsOf){
|
|
317
|
+
if(node.completion!=null||node.statusWord)return null;
|
|
318
|
+
var lv=[],q=[node],n,k,i,guard=0;
|
|
319
|
+
while(q.length&&guard++<4096){n=q.pop();k=kidsOf(n.id);if(k.length){for(i=0;i<k.length;i++)q.push(k[i]);}else if(n!==node)lv.push(n);}
|
|
320
|
+
if(!lv.length)return null;
|
|
321
|
+
var sum=0,nc=0,nr=0,rank={problem:5,next:4,"in-progress":3,planned:2,unknown:1.5,done:1},best=0,key="unknown",s2;
|
|
322
|
+
for(i=0;i<lv.length;i++){
|
|
323
|
+
if(lv[i].completion!=null){sum+=(+lv[i].completion||0);nc++;}
|
|
324
|
+
s2=lv[i].status2||(lv[i].status==="planned"?"planned":"unknown");
|
|
325
|
+
if(s2!=="unknown")nr++;
|
|
326
|
+
if((rank[s2]||0)>best){best=rank[s2]||0;key=s2;}
|
|
327
|
+
}
|
|
328
|
+
if(!nr)return null;
|
|
329
|
+
// #43: the mean and the coverage must share a denominator or the badge lies twice in one string.
|
|
330
|
+
// Averaging over the children that carry a number while claiming coverage over the children that
|
|
331
|
+
// carry a verdict printed "100% 25/53" from a SINGLE measured child - the owner's complaint,
|
|
332
|
+
// verbatim. A mean is emitted only when every ruled child carries a number; otherwise the badge
|
|
333
|
+
// shows coverage alone, which is the thing actually known.
|
|
334
|
+
return {status2:key,mean:(nc&&nc===nr)?Math.round(sum/nc):null,ruled:nr,measured:nc,total:lv.length};
|
|
335
|
+
}
|
|
336
|
+
// The badge: the first number a stakeholder reads, so it is a function like everything else that
|
|
337
|
+
// makes a claim. Order of authority: a curated word, the node's own measurement, the roll-up, then
|
|
338
|
+
// nothing. `?` is reserved for a box NOBODY ruled on — printing it over a box a document declares
|
|
339
|
+
// finished made the badge contradict its own colour. A declaration has no number, and inventing
|
|
340
|
+
// one for it is the whole defect this file is being corrected for.
|
|
341
|
+
function badgeOf(n,roll){
|
|
342
|
+
return n.statusWord||(n.completion!=null?n.completion+"%":roll?(roll.mean!=null?roll.mean+"% ":"")+roll.ruled+"/"+roll.total:(n.status2&&n.status2!=="unknown"?"":STR.stUnk))
|
|
343
|
+
}
|
|
263
344
|
function tallyOf(kids,kidsOf){
|
|
264
|
-
var cnt={},tot=0,sum=0,nc=0,st3,i,j,k,q,lv,use,n;
|
|
345
|
+
var cnt={},tot=0,sum=0,nc=0,nr=0,st3,i,j,k,q,lv,use,n;
|
|
265
346
|
for(i=0;i<kids.length;i++){
|
|
266
347
|
lv=[];q=[kids[i]];
|
|
267
348
|
while(q.length){n=q.pop();k=kidsOf(n.id);if(k.length){for(j=0;j<k.length;j++)q.push(k[j]);}else lv.push(n);}
|
|
@@ -273,15 +354,65 @@ function tallyOf(kids,kidsOf){
|
|
|
273
354
|
st3=STMAP[n.status2]||(n.status==="planned"?"plan":"unk");
|
|
274
355
|
cnt[st3]=(cnt[st3]||0)+1;tot++;
|
|
275
356
|
if(n.completion!=null){sum+=(+n.completion||0);nc++;}
|
|
357
|
+
if(st3!=="unk")nr++;
|
|
276
358
|
}
|
|
277
359
|
}
|
|
278
|
-
|
|
360
|
+
// #43: `ruled` must mean the same thing here as in rollStatus - how many boxes somebody judged -
|
|
361
|
+
// or the breadcrumb and the badge contradict each other on the same screen. The mean stays
|
|
362
|
+
// subject to the same rule: it is emitted only when every judged box carries a number.
|
|
363
|
+
return {cnt:cnt,tot:tot,ruled:nr,measured:nc,mean:(nc&&nc===nr)?Math.round(sum/nc):null};
|
|
364
|
+
}
|
|
365
|
+
function jsonCopy(v){return JSON.parse(JSON.stringify(v));}
|
|
366
|
+
function timelineMatches(edges,match){
|
|
367
|
+
var out=[],i,e;for(i=0;i<edges.length;i++){e=edges[i];
|
|
368
|
+
if(e.from===match.from&&e.to===match.to&&(!match.hasOwnProperty("label")||(e.label||"")===match.label)&&(!match.hasOwnProperty("kind")||(e.kind||"")===match.kind))out.push(i);
|
|
369
|
+
}return out;
|
|
279
370
|
}
|
|
280
|
-
|
|
371
|
+
// The browser materializer mirrors lib/validate.mjs after generation has rejected every ambiguous
|
|
372
|
+
// patch. It returns baseline+checkpoint states without storing a second graph in the model.
|
|
373
|
+
function timelineStates(base){
|
|
374
|
+
var tl=base&&base.timeline;if(!tl)return {errors:[],states:[]};
|
|
375
|
+
var errors=[],states=[],current=jsonCopy(base),c,cp,p,n,e,i,j,found,target,before;
|
|
376
|
+
if(!tl.checkpoints||!tl.checkpoints.length)return {errors:["timeline.checkpoints"],states:[]};
|
|
377
|
+
for(c=0;c<tl.checkpoints.length;c++){
|
|
378
|
+
cp=tl.checkpoints[c];p=cp.patch||{};n=p.nodes||{};e=p.edges||{};
|
|
379
|
+
var d={nodes:[],edges:[],removedNodes:[],removedEdges:[],counts:{add:0,update:0,rewire:0,remove:0}};
|
|
380
|
+
var adds=n.add||[],updates=n.update||[],nrem=n.remove||[],erem=e.remove||[],rew=e.rewire||[],eadd=e.add||[];
|
|
381
|
+
for(i=0;i<adds.length;i++){current.nodes.push(jsonCopy(adds[i].node));d.nodes.push({id:adds[i].node.id,type:"ADD",change:adds[i].change});d.counts.add++;}
|
|
382
|
+
for(i=0;i<updates.length;i++){target=null;for(j=0;j<current.nodes.length;j++)if(current.nodes[j].id===updates[i].id){target=current.nodes[j];break;}
|
|
383
|
+
if(!target){errors.push(cp.id+":node:"+updates[i].id);continue;}for(var k in updates[i].set)if(updates[i].set.hasOwnProperty(k))target[k]=jsonCopy(updates[i].set[k]);
|
|
384
|
+
d.nodes.push({id:updates[i].id,type:"UPDATE",change:updates[i].change});d.counts.update++;}
|
|
385
|
+
for(i=0;i<erem.length;i++){found=timelineMatches(current.edges,erem[i].match);if(found.length!==1){errors.push(cp.id+":remove-edge");continue;}
|
|
386
|
+
target=current.edges.splice(found[0],1)[0];d.removedEdges.push({from:target.from,to:target.to,label:target.label,kind:target.kind,type:"REMOVE",change:erem[i].change});d.counts.remove++;}
|
|
387
|
+
for(i=0;i<rew.length;i++){found=timelineMatches(current.edges,rew[i].match);if(found.length!==1){errors.push(cp.id+":rewire-edge");continue;}
|
|
388
|
+
target=current.edges[found[0]];before=jsonCopy(target);for(k in rew[i].set)if(rew[i].set.hasOwnProperty(k))target[k]=jsonCopy(rew[i].set[k]);
|
|
389
|
+
d.edges.push({from:target.from,to:target.to,label:target.label,kind:target.kind,type:"REWIRE",change:rew[i].change,before:before});d.counts.rewire++;}
|
|
390
|
+
for(i=0;i<eadd.length;i++){current.edges.push(jsonCopy(eadd[i].edge));target=eadd[i].edge;
|
|
391
|
+
d.edges.push({from:target.from,to:target.to,label:target.label,kind:target.kind,type:"ADD",change:eadd[i].change});d.counts.add++;}
|
|
392
|
+
for(i=0;i<nrem.length;i++){target=null;for(j=0;j<current.nodes.length;j++)if(current.nodes[j].id===nrem[i].id){target=current.nodes[j];break;}
|
|
393
|
+
if(!target){errors.push(cp.id+":remove-node:"+nrem[i].id);continue;}current.nodes=current.nodes.filter(function(x){return x.id!==nrem[i].id;});
|
|
394
|
+
d.removedNodes.push({id:nrem[i].id,name:target.name,type:"REMOVE",change:nrem[i].change});d.counts.remove++;}
|
|
395
|
+
states.push({id:cp.id,label:cp.label,badge:cp.badge||"",model:jsonCopy(current),delta:d});
|
|
396
|
+
}
|
|
397
|
+
return {errors:errors,states:states};
|
|
398
|
+
}
|
|
399
|
+
var BASE=null,M=null,TSTATES=[],CPID="as-is",CPDELTA={nodes:[],edges:[],removedNodes:[],removedEdges:[],counts:{add:0,update:0,rewire:0,remove:0}}
|
|
400
|
+
var stack=[null],mode="now",POS={},POSALL={},drag=null,justDragged=false,CATNODES={},FETCHED=false
|
|
281
401
|
var VBOX={}; // last fitted viewBox per level — frozen while dragging so the canvas does not rescale
|
|
282
402
|
function $(i){return document.getElementById(i)}
|
|
283
403
|
function esc(s){return String(s==null?"":s).replace(/&/g,"&").replace(/</g,"<").replace(/"/g,""")}
|
|
284
404
|
function setSkin(s){var k=SKIN_IDS.indexOf(s)>-1?s:"holo";document.documentElement.dataset.skin=k;var el=$("skin");if(el)el.value=k;}
|
|
405
|
+
function nodeChange(id){for(var i=0;i<CPDELTA.nodes.length;i++)if(CPDELTA.nodes[i].id===id)return CPDELTA.nodes[i];return null;}
|
|
406
|
+
function nodeProjected(id){
|
|
407
|
+
var projected=false;
|
|
408
|
+
for(var i=0;i<TSTATES.length;i++){
|
|
409
|
+
var d=TSTATES[i].delta,j;
|
|
410
|
+
for(j=0;j<d.nodes.length;j++)if(d.nodes[j].id===id)projected=true;
|
|
411
|
+
for(j=0;j<d.removedNodes.length;j++)if(d.removedNodes[j].id===id)projected=false;
|
|
412
|
+
if(TSTATES[i].id===CPID)break;
|
|
413
|
+
}
|
|
414
|
+
return projected;
|
|
415
|
+
}
|
|
285
416
|
// Layout key = the id of the level's parent ("root" at the top). Stable and documentable: it is
|
|
286
417
|
// exactly the key used by meta.layout hints and by the exported layout JSON.
|
|
287
418
|
function keyOf(){return stack[stack.length-1]||"root";}
|
|
@@ -332,8 +463,52 @@ function makeCatalogNode(pid,g){
|
|
|
332
463
|
function hasKids(id){for(var i=0;i<M.nodes.length;i++)if(M.nodes[i].parent===id)return true;return false;}
|
|
333
464
|
function node(id){for(var i=0;i<M.nodes.length;i++)if(M.nodes[i].id===id)return M.nodes[i];return null;}
|
|
334
465
|
function nameOf(id){var n=node(id);return n?n.name:id;}
|
|
335
|
-
function srcOf(n){if(
|
|
336
|
-
|
|
466
|
+
function srcOf(n){if(CPID!=="as-is"&&nodeProjected(n.id)&&BASE&&BASE.timeline)return BASE.timeline.source;
|
|
467
|
+
if(n.verify){if(typeof n.verify==="string")return n.verify;if(n.verify.source)return n.verify.source;if(n.verify.ref)return n.verify.ref;if(n.verify.issue)return "#"+n.verify.issue;}
|
|
468
|
+
var ev=(n.evidence||[]).filter(function(e){return e.type==="doc"||e.type==="path";})[0];
|
|
469
|
+
return ev?ev.ref:((M.meta&&M.meta.verifyMethod)||"");}
|
|
470
|
+
function edgeChange(e){
|
|
471
|
+
for(var i=0;i<CPDELTA.edges.length;i++){var d=CPDELTA.edges[i];
|
|
472
|
+
if(d.from===e.from&&d.to===e.to&&(d.label||"")===(e.label||"")&&(d.kind||"")===(e.kind||""))return d;
|
|
473
|
+
}return null;
|
|
474
|
+
}
|
|
475
|
+
function emptyDelta(){return {nodes:[],edges:[],removedNodes:[],removedEdges:[],counts:{add:0,update:0,rewire:0,remove:0}};}
|
|
476
|
+
function renderTimeline(){
|
|
477
|
+
var nav=$("timeline"),legacy=$("legacytime"),legend=$("lg-checkpoint-wrap");
|
|
478
|
+
if(!BASE||!BASE.timeline){nav.style.display="none";legacy.style.display="inline-flex";legend.style.display="none";$("checkpoint-changes").innerHTML="";return;}
|
|
479
|
+
nav.style.display="block";legacy.style.display="none";legend.style.display="inline";
|
|
480
|
+
nav.setAttribute("aria-label",STR.timelineLabel);
|
|
481
|
+
var html='<button class="cpstep real'+(CPID==="as-is"?" active":"")+'" data-checkpoint="as-is" type="button"><span class="dot"></span>'+esc(STR.asIs)+'<span class="cpbadge">'+esc(STR.asIsBadge)+'</span></button>';
|
|
482
|
+
for(var i=0;i<TSTATES.length;i++){var s=TSTATES[i];html+='<button class="cpstep'+(CPID===s.id?" active":"")+'" data-checkpoint="'+esc(s.id)+'" type="button"><span class="dot"></span>'+esc(s.label)+(s.badge?'<span class="cpbadge">'+esc(s.badge)+'</span>':'')+'</button>';}
|
|
483
|
+
$("timeline-track").innerHTML=html;
|
|
484
|
+
var c=CPDELTA.counts,parts=[];
|
|
485
|
+
if(c.add)parts.push('<b>'+STR.deltaAdd+'</b> '+c.add);
|
|
486
|
+
if(c.update)parts.push('<b>'+STR.deltaUpdate+'</b> '+c.update);
|
|
487
|
+
if(c.rewire)parts.push('<b>'+STR.deltaRewire+'</b> '+c.rewire);
|
|
488
|
+
if(c.remove)parts.push('<b>'+STR.deltaRemove+'</b> '+c.remove);
|
|
489
|
+
$("checkpoint-summary").innerHTML=(parts.length?parts.join(" · ")+" · ":"")+esc(STR.cumulativeHint);
|
|
490
|
+
var changes=[],d=CPDELTA,i,nm,subject;
|
|
491
|
+
function row(type,what,why){changes.push('<div class="cpchange"><b>'+esc(type)+'</b> '+esc(what)+' <span>— '+esc(why)+'</span></div>');}
|
|
492
|
+
for(i=0;i<d.nodes.length;i++){nm=node(d.nodes[i].id);row(d.nodes[i].type,nm?nm.name:d.nodes[i].id,d.nodes[i].change);}
|
|
493
|
+
for(i=0;i<d.edges.length;i++){subject=nameOf(d.edges[i].from)+" → "+nameOf(d.edges[i].to)+(d.edges[i].label?" · "+d.edges[i].label:"");row(d.edges[i].type,subject,d.edges[i].change);}
|
|
494
|
+
for(i=0;i<d.removedEdges.length;i++){subject=nameOf(d.removedEdges[i].from)+" → "+nameOf(d.removedEdges[i].to)+(d.removedEdges[i].label?" · "+d.removedEdges[i].label:"");row(d.removedEdges[i].type,subject,d.removedEdges[i].change);}
|
|
495
|
+
for(i=0;i<d.removedNodes.length;i++)row(d.removedNodes[i].type,d.removedNodes[i].name||d.removedNodes[i].id,d.removedNodes[i].change);
|
|
496
|
+
$("checkpoint-changes").innerHTML=changes.join("");
|
|
497
|
+
}
|
|
498
|
+
function checkpointUrl(id){
|
|
499
|
+
try{var u=new URL(location.href);if(id==="as-is")u.searchParams.delete("checkpoint");else u.searchParams.set("checkpoint",id);history.replaceState(null,"",u.toString());}catch(e){}
|
|
500
|
+
}
|
|
501
|
+
function setCheckpoint(id,writeUrl,animate,preserveLegacyMode){
|
|
502
|
+
var state=null,i;for(i=0;i<TSTATES.length;i++)if(TSTATES[i].id===id){state=TSTATES[i];break;}
|
|
503
|
+
if(!state){id="as-is";M=jsonCopy(BASE);CPDELTA=emptyDelta();}else{M=jsonCopy(state.model);CPDELTA=jsonCopy(state.delta);}
|
|
504
|
+
CPID=id;mode=preserveLegacyMode&&(!BASE||!BASE.timeline)&&mode==="target"?"target":"now";
|
|
505
|
+
$("bnow").className=mode==="now"?"on":"";$("btgt").className=mode==="target"?"on":"";
|
|
506
|
+
var posKey="checkpoint:"+id;if(!POSALL[posKey])POSALL[posKey]={};POS=POSALL[posKey];VBOX={};
|
|
507
|
+
while(stack.length>1&&!node(stack[stack.length-1]))stack.pop();
|
|
508
|
+
if(!stack.length)stack=[null];
|
|
509
|
+
$("detail").style.display="none";
|
|
510
|
+
renderTimeline();stamp("");if(writeUrl)checkpointUrl(id);draw(animate!==false);
|
|
511
|
+
}
|
|
337
512
|
|
|
338
513
|
var NW=228,NH=118; // NH fits 3 description lines without hitting the [+] DRILL
|
|
339
514
|
// Past DENSE_MIN siblings a full-size card cannot stay legible: the fit-to-content viewBox scales
|
|
@@ -440,30 +615,38 @@ function draw(animate){
|
|
|
440
615
|
+'<marker id="arp" markerWidth="9" markerHeight="7" refX="9" refY="3.5" orient="auto"><polygon points="0 0,9 3.5,0 7" fill="'+cssv("--edgeplan")+'"/></marker></defs>';
|
|
441
616
|
for(j=0;j<geo.length;j++){
|
|
442
617
|
var e=geo[j].g,ed=geo[j].e,cl=(ed.estatus==="to-build"?"plan":"flow");
|
|
443
|
-
s+='<path class="edge '+cl+'" data-e="'+geo[j].i+'" d="'+e.d+'"/>';
|
|
618
|
+
s+='<path class="edge '+cl+(ed.delta?" delta":"")+'" data-e="'+geo[j].i+'" d="'+e.d+'"/>';
|
|
444
619
|
s+='<path class="edgehit" data-e="'+geo[j].i+'" data-label="'+esc(ed.label||"")+'" d="'+e.d+'"/>';
|
|
445
620
|
if(showLbl&&ed.label){var lb=String(ed.label);if(lb.length>28)lb=lb.slice(0,27)+"…";
|
|
446
621
|
s+='<text class="elbl" x="'+e.mx+'" y="'+(e.my-4)+'" text-anchor="middle">'+esc(lb)+'</text>';}
|
|
447
622
|
}
|
|
448
623
|
for(var j=0;j<kids.length;j++){
|
|
449
624
|
var n=kids[j],p=pos[n.id];
|
|
450
|
-
|
|
625
|
+
// A grouping box borrows its children's colour and badge — see rollStatus. The MODEL keeps saying
|
|
626
|
+
// `unknown` for it; this is the viewer reporting what is underneath, not gen inventing a verdict.
|
|
627
|
+
var roll=(n.__catalog||mode==="target")?null:rollStatus(n,childrenOf);
|
|
628
|
+
var st=STMAP[(roll?roll.status2:n.status2)]||(n.status==="planned"?"plan":"unk");
|
|
451
629
|
var pulse=(st==="next"||st==="prog")&&mode!=="target"&&!n.__catalog;
|
|
452
|
-
|
|
630
|
+
// `decl`: this verdict was DERIVED from a repo document, not proven. The legend has carried a
|
|
631
|
+
// hollow green for it since #38 and the canvas never drew one, so every declared done shipped
|
|
632
|
+
// dressed as proof.
|
|
633
|
+
var isCat=!!n.__catalog,dr=!isCat&&hasKids(n.id),change=isCat?null:nodeChange(n.id),cls="nd s-"+st+((n.verify&&n.verify.derived)?" decl":"")+(dr?" drill":"")+(isCat?" cat":"")+(pulse?" pulse":"")+(change?" delta":"");
|
|
453
634
|
var desc=isCat?(n.count+" "+STR.rosterCount):(mode==="target"?(n.target||n.func||""):(n.current||n.func||""));
|
|
454
635
|
// 26 of a real repo's 53 boxes printed "1 file: advisor." under a box titled "internal/advisor".
|
|
455
636
|
// A description that only restates the title is ink, not information: drop it and let the box
|
|
456
637
|
// breathe. The measured fallback text is the generator's honest last resort, not a lie — it just
|
|
457
638
|
// does not belong on screen when it says nothing the header has not already said.
|
|
458
639
|
if(!isCat&&echoesName(desc,n.name))desc="";
|
|
459
|
-
|
|
640
|
+
// The coverage rides WITH the mean, never without it: `100%` on a domain where 14 of 22 packages
|
|
641
|
+
// were ruled is the invented green, and this is the badge a stakeholder reads first.
|
|
642
|
+
var badge=isCat?String(n.count):(mode==="target"?STR.detTarget:badgeOf(n,roll));
|
|
460
643
|
// the title runs until the badge, so reserve the badge's actual width (9px text ≈ 5.4px/char)
|
|
461
644
|
// instead of a flat 70px — a fixed reserve clipped names that had all the room they needed
|
|
462
645
|
var tmax=Math.floor((p.w-34-String(badge).length*5.4)/6.7),tt=n.name;
|
|
463
646
|
if(String(tt).length>tmax)tt=String(tt).slice(0,Math.max(1,tmax-1))+"…";
|
|
464
647
|
s+='<g class="'+cls+'" data-id="'+esc(n.id)+'">'
|
|
465
648
|
+'<rect x="'+p.x+'" y="'+p.y+'" width="'+p.w+'" height="'+p.h+'" rx="10"/>'
|
|
466
|
-
+'<text class="kk" x="'+(p.x+13)+'" y="'+(p.y+16)+'">'+esc(n.category||n.kind||"")+'</text>'
|
|
649
|
+
+'<text class="kk" x="'+(p.x+13)+'" y="'+(p.y+16)+'">'+esc((n.category||n.kind||"")+(change?" · "+change.type:""))+'</text>'
|
|
467
650
|
+'<text class="tt" x="'+(p.x+13)+'" y="'+(p.y+33)+'">'+esc(tt)+'</text>'
|
|
468
651
|
+'<text class="pp" x="'+(p.x+p.w-13)+'" y="'+(p.y+16)+'" text-anchor="end">'+esc(badge)+'</text>';
|
|
469
652
|
// Description lines: how many fit comes from the geometry (text starts at y+51, 12.5px apart,
|
|
@@ -479,8 +662,8 @@ function draw(animate){
|
|
|
479
662
|
}
|
|
480
663
|
s+='</svg>';
|
|
481
664
|
var st2=$("stage");
|
|
482
|
-
var etip=$("etip"); // preserve
|
|
483
|
-
st2.innerHTML=s;st2.appendChild(etip);
|
|
665
|
+
var etip=$("etip"),err=$("err"); // preserve the live overlays across redraw/re-verify
|
|
666
|
+
st2.innerHTML=s;if(etip)st2.appendChild(etip);if(err)st2.appendChild(err);
|
|
484
667
|
st2.classList.toggle("anim",animate!==false);
|
|
485
668
|
// breadcrumb
|
|
486
669
|
var chain=[],c=pid;while(c){var nn=node(c);chain.unshift(nn);c=nn?nn.parent:null;}
|
|
@@ -499,9 +682,11 @@ function draw(animate){
|
|
|
499
682
|
var DOT={done:"#2fe08a",prog:"#ffc74d",next:"#3ee7ff",plan:"#2d5a72",prob:"#ff5d5d",unk:"#7d8f99"},ord=["done","prog","next","plan","prob","unk"],agg="";
|
|
500
683
|
for(j=0;j<ord.length;j++)if(T.cnt[ord[j]])agg+='<i style="background:'+DOT[ord[j]]+'"></i>'+T.cnt[ord[j]];
|
|
501
684
|
// The coverage rides WITH the percentage, never without it. See tallyOf.
|
|
502
|
-
if(T.ruled)agg+=' '+esc(STR.aggProgress)+' <b>'+T.mean+'%</b> '+T.ruled+'/'+T.tot;
|
|
685
|
+
if(T.ruled)agg+=' '+(T.mean!=null?esc(STR.aggProgress)+' <b>'+T.mean+'%</b> ':'')+T.ruled+'/'+T.tot;
|
|
503
686
|
cb='<span class="agg">'+agg+'</span>'+cb;
|
|
504
687
|
}
|
|
688
|
+
if(BASE&&BASE.timeline){var cpLabel=CPID==="as-is"?STR.asIs:CPID;for(j=0;j<TSTATES.length;j++)if(TSTATES[j].id===CPID)cpLabel=TSTATES[j].label;
|
|
689
|
+
cb='<b>'+esc(cpLabel)+'</b> · '+cb;}
|
|
505
690
|
$("crumb").innerHTML=cb;
|
|
506
691
|
$("back").disabled=stack.length<2;
|
|
507
692
|
$("detail").style.display="none";
|
|
@@ -563,8 +748,8 @@ function rollEdges(edges,vis,parent){
|
|
|
563
748
|
if(!f||!t||f===t)continue;
|
|
564
749
|
c=parseInt(String(e.label),10);if(!(c>0))c=1;
|
|
565
750
|
k=f+"|"+t;
|
|
566
|
-
if(ix[k]==null){ix[k]=out.length;out.push({from:f,to:t,label:e.label,estatus:e.estatus,n:c});}
|
|
567
|
-
else{g=out[ix[k]];g.n+=c;g.label=g.n+"×";if(e.estatus!=="to-build")g.estatus=e.estatus;}
|
|
751
|
+
if(ix[k]==null){ix[k]=out.length;out.push({from:f,to:t,label:e.label,estatus:e.estatus,n:c,delta:typeof edgeChange==="function"?edgeChange(e):null});}
|
|
752
|
+
else{g=out[ix[k]];g.n+=c;g.label=g.n+"×";if(e.estatus!=="to-build")g.estatus=e.estatus;if(!g.delta&&typeof edgeChange==="function")g.delta=edgeChange(e);}
|
|
568
753
|
}
|
|
569
754
|
return out;
|
|
570
755
|
}
|
|
@@ -587,16 +772,20 @@ function cssv(v){try{return getComputedStyle(document.documentElement).getProper
|
|
|
587
772
|
|
|
588
773
|
function showDetail(n){
|
|
589
774
|
if(n&&n.__catalog)return showRoster(n);
|
|
590
|
-
var src=srcOf(n),cur=n.current||n.func||"",showSrc=src&&String(cur).indexOf(src)<0;
|
|
775
|
+
var src=srcOf(n),cur=n.current||n.func||n.description||"",showSrc=src&&String(cur).indexOf(src)<0;
|
|
776
|
+
var timeline=!!(BASE&&BASE.timeline),change=timeline?nodeChange(n.id):null;
|
|
591
777
|
$("detail").innerHTML='<span class="cl" id="dc">'+esc(STR.detClose)+'</span><h3>'+esc(n.name)+' <span class="cat">· '+esc(n.category||n.kind||"")+(n.tech?" · "+esc(n.tech):"")+'</span></h3>'
|
|
592
778
|
+(n.func?'<div class="row"><span class="lbl">'+esc(STR.detFunc)+'</span><span class="func">'+esc(n.func)+'</span></div>':'')
|
|
593
|
-
+(cur&&cur!==n.func?'<div class="row"><span class="lbl">'+esc(STR.detNow)+'</span><span class="now">'+esc(cur)+'</span></div>':'')
|
|
594
|
-
+((n.target&&n.target!=="—")?'<div class="row"><span class="lbl">'+esc(STR.detTgt)+'</span><span class="tgt">'+esc(n.target)+'</span></div>':'')
|
|
779
|
+
+(cur&&(timeline||cur!==n.func)?'<div class="row"><span class="lbl">'+esc(timeline?STR.detCheckpoint:STR.detNow)+'</span><span class="now">'+esc(cur)+'</span></div>':'')
|
|
780
|
+
+(!timeline&&(n.target&&n.target!=="—")?'<div class="row"><span class="lbl">'+esc(STR.detTgt)+'</span><span class="tgt">'+esc(n.target)+'</span></div>':'')
|
|
781
|
+
+(change?'<div class="row"><span class="lbl">'+esc(STR.detChange)+'</span><span class="tgt">'+esc(change.type+" · "+change.change)+'</span></div>':'')
|
|
595
782
|
// A number DERIVED from a repo document is a declaration, not a verification: the label says which
|
|
596
783
|
// one, and the coverage says how much of this module the citation actually reaches. "3 of 3 rows
|
|
597
784
|
// declared done" and "this module is done" are not the same sentence when the module holds 22
|
|
598
785
|
// files and the document names 3 of them.
|
|
599
786
|
+(showSrc?'<div class="row"><span class="lbl">'+esc((n.verify&&n.verify.derived)?STR.detDecl:STR.detSrc)+'</span><span class="src">'+esc(src)+'</span></div>':'')
|
|
787
|
+
// A badge the viewer rolled up must say so, or it reads as a verdict somebody wrote about the box.
|
|
788
|
+
+((function(){var r=rollStatus(n,childrenOf);return r?'<div class="row"><span class="lbl">'+esc(STR.detRoll)+'</span><span class="now">'+esc(STR.rollPart.replace("{n}",r.ruled).replace("{t}",r.total))+'</span></div>':'';})())
|
|
600
789
|
+(coverText(n)?'<div class="row"><span class="lbl">'+esc(STR.detCover)+'</span><span class="now">'+esc(coverText(n))+'</span></div>':'')
|
|
601
790
|
+((n.issues&&n.issues.length)?'<div class="row"><span class="lbl">'+esc(STR.detIssue)+'</span><span class="now">'+esc(n.issues.join(", "))+'</span></div>':'');
|
|
602
791
|
$("detail").style.display="block";
|
|
@@ -695,12 +884,14 @@ function wire(){
|
|
|
695
884
|
document.addEventListener("keydown",function(e){if(e.key==="Escape"&&stack.length>1){stack.pop();draw(true);}});
|
|
696
885
|
$("bnow").addEventListener("click",function(){mode="now";$("bnow").className="on";$("btgt").className="";draw(false);});
|
|
697
886
|
$("btgt").addEventListener("click",function(){mode="target";$("btgt").className="on";$("bnow").className="";draw(false);});
|
|
887
|
+
$("timeline").addEventListener("click",function(ev){var b=ev.target;while(b&&b!==$("timeline")&&!(b.getAttribute&&b.getAttribute("data-checkpoint")))b=b.parentNode;
|
|
888
|
+
if(b&&b!==$("timeline"))setCheckpoint(b.getAttribute("data-checkpoint"),true,true);});
|
|
698
889
|
$("brf").addEventListener("click",function(){
|
|
699
890
|
// Served model ⇒ actually re-read it (a `forma verify` run in the terminal shows up here without
|
|
700
891
|
// losing the level, the layout or the mode). Injected/offline ⇒ re-stamp, and say why.
|
|
701
892
|
if(!FETCHED){stamp(STR.staticSrc);return;}
|
|
702
893
|
fetch("./c4-model.json?ts="+(new Date()).getTime()).then(function(r){return r.json();})
|
|
703
|
-
.then(function(m){if(m&&m.nodes&&m.nodes.length)
|
|
894
|
+
.then(function(m){if(m&&m.nodes&&m.nodes.length)loadModel(m,true);else stamp(STR.staticSrc);})
|
|
704
895
|
.catch(function(){stamp(STR.staticSrc);});
|
|
705
896
|
});
|
|
706
897
|
$("blbl").addEventListener("click",function(){LABELS=!($("blbl").className==="on");draw(false);});
|
|
@@ -718,14 +909,14 @@ function applyStrings(){
|
|
|
718
909
|
set("pexp",STR.printExport);set("pexpSvg",STR.exportSvg);set("pexpPng",STR.exportPng);
|
|
719
910
|
set("pexpLayout",STR.exportLayout);set("pexpPrint",STR.print);
|
|
720
911
|
set("lg-done",STR.legDone);set("lg-decl",STR.legDecl);set("lg-prog",STR.legProg);set("lg-next",STR.legNext);
|
|
721
|
-
set("lg-plan",STR.legPlan);set("lg-prob",STR.legProb);set("lg-unk",STR.legUnk);set("lg-hint",STR.legHint);
|
|
912
|
+
set("lg-plan",STR.legPlan);set("lg-prob",STR.legProb);set("lg-unk",STR.legUnk);set("lg-checkpoint",STR.legCheckpoint);set("lg-hint",STR.legHint);
|
|
722
913
|
}
|
|
723
914
|
// ---- FEATURE #2: PRINT / EXPORT (all client-side, offline) ----
|
|
724
915
|
function exportBaseName(){
|
|
725
916
|
var repo=(M&&M.source&&M.source.repo)||"forma";
|
|
726
917
|
var pid=stack[stack.length-1];
|
|
727
918
|
var lvl=pid?(node(pid)&&node(pid).name)||"level":"context";
|
|
728
|
-
return String(repo+"-"+lvl).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"forma";
|
|
919
|
+
return String(repo+"-"+(BASE&&BASE.timeline?CPID+"-":"")+lvl).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"forma";
|
|
729
920
|
}
|
|
730
921
|
// Collect the CSS rules the SVG needs so a standalone serialize renders styled.
|
|
731
922
|
function collectSvgCss(){
|
|
@@ -835,24 +1026,29 @@ function moveTip(ev){var st=$("stage").getBoundingClientRect(),t=$("etip");
|
|
|
835
1026
|
function stamp(suffix){
|
|
836
1027
|
var m=(M&&M.meta)||{};
|
|
837
1028
|
$("stamp").textContent=STR.stampBase+" "+(m.verifiedAt||(M&&M.generatedAt)||(M&&M.source&&M.source.commit)||"—")
|
|
838
|
-
+" · "+STR.stampMethod+" "+(m.verifyMethod||STR.stampVerify)
|
|
1029
|
+
+" · "+STR.stampMethod+" "+(m.verifyMethod||STR.stampVerify)
|
|
1030
|
+
+(BASE&&BASE.timeline?" · "+STR.stampProjection+" "+BASE.timeline.source:"")+(suffix?" "+suffix:"");
|
|
839
1031
|
}
|
|
840
|
-
function
|
|
841
|
-
|
|
842
|
-
|
|
1032
|
+
function loadModel(model,preserveCheckpoint){
|
|
1033
|
+
var wanted=preserveCheckpoint?CPID:null,candidate=null;
|
|
1034
|
+
try{candidate=jsonCopy(model);}catch(e){}
|
|
1035
|
+
LANG=pickLang(candidate);STR=STRINGS[LANG];
|
|
843
1036
|
try{document.documentElement.setAttribute("lang",LANG);}catch(e){}
|
|
844
1037
|
applyStrings();
|
|
845
|
-
if(!
|
|
1038
|
+
if(!candidate||!candidate.nodes||!candidate.nodes.length){$("err").style.display="block";$("err").textContent=STR.errEmpty;return;}
|
|
1039
|
+
var built=timelineStates(candidate);if(built.errors.length){$("err").style.display="block";$("err").textContent=STR.errTimeline+" "+built.errors.join(", ");return;}
|
|
1040
|
+
BASE=candidate;M=jsonCopy(candidate);
|
|
1041
|
+
$("err").style.display="none";TSTATES=built.states;
|
|
846
1042
|
var ttl=(M.meta&&M.meta.title)||((M.source&&M.source.repo)?"Forma · "+M.source.repo:null);
|
|
847
1043
|
if(ttl){$("title").textContent=ttl;try{document.title=ttl;}catch(e){}}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
draw(true);
|
|
1044
|
+
if(!preserveCheckpoint)setSkin((new URLSearchParams(location.search).get("skin"))||window.__C4_SKIN__||(M.meta&&M.meta.skin)||"holo");
|
|
1045
|
+
if(!preserveCheckpoint){stack=[null];POSALL={};wanted=null;try{wanted=new URLSearchParams(location.search).get("checkpoint");}catch(e){}}
|
|
1046
|
+
setCheckpoint(wanted||"as-is",false,!preserveCheckpoint,preserveCheckpoint);
|
|
852
1047
|
// Minimal hook for embedders (the Cowork artifact re-verifies through its own MCP bridge and
|
|
853
1048
|
// needs to push the result back in without forking the viewer).
|
|
854
|
-
window.__C4_API__={model:function(){return M;},redraw:function(){draw(false);},stamp:function(t){$("stamp").textContent=t;}};
|
|
1049
|
+
window.__C4_API__={model:function(){return M;},checkpoint:function(id){setCheckpoint(id,true,true);},redraw:function(){draw(false);},stamp:function(t){$("stamp").textContent=t;}};
|
|
855
1050
|
}
|
|
1051
|
+
function boot(model){loadModel(model,false);}
|
|
856
1052
|
function start(){
|
|
857
1053
|
// Pick language from URL/global up-front so header + errors render even if the model never loads.
|
|
858
1054
|
LANG=pickLang(null);STR=STRINGS[LANG];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forma-arch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Forma — present your architecture instead of slides. An interactive, stack-agnostic C4 explorer (context → container → component → leaf) generated from your code and kept true to it by a deterministic drift check.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"architecture",
|