forma-arch 0.10.1 → 0.11.1
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 +50 -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 +205 -46
- 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,18 @@ 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}
|
|
31
44
|
#crumb{font-size:11px;color:var(--sec);margin:8px 0}
|
|
32
45
|
#crumb .agg{float:right;color:var(--ter2);letter-spacing:.05em}
|
|
33
46
|
#crumb .agg i{display:inline-block;width:7px;height:7px;border-radius:50%;margin:0 3px 0 8px;vertical-align:baseline}
|
|
@@ -52,6 +65,12 @@ svg{width:100%;height:100%;display:block}
|
|
|
52
65
|
.nd .drillhit:hover{fill:rgba(62,231,255,.12)}
|
|
53
66
|
.s-done rect{stroke:var(--done);filter:drop-shadow(0 0 7px rgba(47,224,138,.55))}
|
|
54
67
|
.s-done .tt,.s-done .pp{fill:var(--done)}
|
|
68
|
+
/* The hollow green the legend has promised since #38: a done a repo document DECLARES is not a
|
|
69
|
+
done a closed issue PROVES, and until now the canvas drew them identically. Hollow = the glow
|
|
70
|
+
comes off, which is exactly what the legend swatch says (filled pill vs outline).
|
|
71
|
+
ponytail: the blueprint skin zeroes every filter, so there the two greens still read alike —
|
|
72
|
+
upgrade path is a second, skin-independent mark if anyone actually presents in blueprint. */
|
|
73
|
+
.s-done.decl rect{filter:none}
|
|
55
74
|
.s-prog rect{stroke:var(--prog);filter:drop-shadow(0 0 7px rgba(255,199,77,.5))}
|
|
56
75
|
.s-prog .tt,.s-prog .pp{fill:var(--prog)}
|
|
57
76
|
.s-next rect{stroke:var(--next);stroke-width:2.2;filter:drop-shadow(0 0 11px rgba(62,231,255,.75))}
|
|
@@ -60,14 +79,20 @@ svg{width:100%;height:100%;display:block}
|
|
|
60
79
|
.s-plan .tt,.s-plan .pp{fill:var(--planTxt)}
|
|
61
80
|
.s-prob rect{stroke:var(--prob);filter:drop-shadow(0 0 8px rgba(255,93,93,.6))}
|
|
62
81
|
.s-prob .tt,.s-prob .pp{fill:var(--prob)}
|
|
63
|
-
.
|
|
82
|
+
/* NO dash. legHint teaches the reader that a dashed box is `da costruire`, and wearing that dash
|
|
83
|
+
made "nobody has ruled on this" read as "this does not exist" — on a repo where the module was
|
|
84
|
+
right there. Not-ruled is now a plain grey outline: no dash (planned), no glow (done). */
|
|
85
|
+
.s-unk rect{stroke:var(--unk)}
|
|
64
86
|
.s-unk .tt,.s-unk .pp{fill:var(--unk)}
|
|
87
|
+
.nd.delta rect{stroke:var(--cyan)!important;stroke-width:2.3!important;filter:drop-shadow(0 0 12px rgba(62,231,255,.72))!important}
|
|
88
|
+
.nd.delta .tt{fill:var(--cyan)!important}
|
|
65
89
|
.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
90
|
.mode-t .nd .tt,.mode-t .nd .pp{fill:var(--next)!important}
|
|
67
91
|
.edge{stroke:var(--edge);stroke-width:1.3;fill:none;marker-end:url(#ar);cursor:help}
|
|
68
92
|
.edge.flow{stroke-dasharray:5 7;animation:dash 1.1s linear infinite}
|
|
69
93
|
.edge.plan{stroke:var(--edgeplan);stroke-dasharray:3 6;opacity:.6;marker-end:url(#arp)}
|
|
70
94
|
.edge.hot{stroke:var(--cyan);stroke-width:2.6;opacity:1}
|
|
95
|
+
.edge.delta{stroke:var(--cyan);stroke-width:2.5;opacity:1;filter:drop-shadow(0 0 4px rgba(62,231,255,.7))}
|
|
71
96
|
/* A level past the label threshold is a hairball, and no layout saves it: on a real Go repo 172 of
|
|
72
97
|
189 arrows cross a box that is not one of their endpoints, and that stays 85-91% at EVERY grid
|
|
73
98
|
shape tried. Only drawing less helps. They stop competing with the names — structure survives as
|
|
@@ -121,7 +146,7 @@ svg.dense .edge.hot{opacity:1;stroke-width:2.6}
|
|
|
121
146
|
@media print{
|
|
122
147
|
body{background:#fff}
|
|
123
148
|
#wrap{background:none;padding:0}
|
|
124
|
-
.ctrls,#legend,.sub,#etip,#pmenu{display:none!important}
|
|
149
|
+
.ctrls,#timeline,#legend,.sub,#etip,#pmenu{display:none!important}
|
|
125
150
|
#stage{border:1px solid #999;height:auto;min-height:0}
|
|
126
151
|
#stage svg{height:70vh}
|
|
127
152
|
}
|
|
@@ -134,6 +159,7 @@ html[data-skin="blueprint"] h1{text-shadow:none}
|
|
|
134
159
|
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
160
|
html[data-skin="blueprint"] .nd.pulse rect{animation:none}
|
|
136
161
|
html[data-skin="blueprint"] button.on{text-shadow:none}
|
|
162
|
+
html[data-skin="blueprint"] .nd.delta rect,html[data-skin="blueprint"] .edge.delta{filter:none}
|
|
137
163
|
</style>
|
|
138
164
|
</head>
|
|
139
165
|
<body>
|
|
@@ -143,8 +169,8 @@ html[data-skin="blueprint"] button.on{text-shadow:none}
|
|
|
143
169
|
<div class="sub" id="subhelp"></div></div>
|
|
144
170
|
<div class="ctrls">
|
|
145
171
|
<button id="back" type="button" disabled></button>
|
|
146
|
-
<button id="bnow" class="on" type="button"></button>
|
|
147
|
-
<button id="btgt" type="button"></button>
|
|
172
|
+
<span id="legacytime"><button id="bnow" class="on" type="button"></button>
|
|
173
|
+
<button id="btgt" type="button"></button></span>
|
|
148
174
|
<button id="brf" type="button"></button>
|
|
149
175
|
<button id="blbl" type="button"></button>
|
|
150
176
|
<button id="brl" type="button"></button>
|
|
@@ -160,6 +186,10 @@ html[data-skin="blueprint"] button.on{text-shadow:none}
|
|
|
160
186
|
<select id="skin" title="skin"></select>
|
|
161
187
|
</div>
|
|
162
188
|
</div>
|
|
189
|
+
<nav id="timeline">
|
|
190
|
+
<div id="timeline-track"></div>
|
|
191
|
+
<div id="checkpoint-summary"></div>
|
|
192
|
+
</nav>
|
|
163
193
|
<div id="crumb"></div>
|
|
164
194
|
<div id="stamp"></div>
|
|
165
195
|
<div id="stage"><div id="etip"></div><div id="err" style="display:none"></div></div>
|
|
@@ -174,6 +204,7 @@ html[data-skin="blueprint"] button.on{text-shadow:none}
|
|
|
174
204
|
<span><i style="background:#2d5a72"></i><span id="lg-plan"></span></span>
|
|
175
205
|
<span><i style="background:#ff5d5d"></i><span id="lg-prob"></span></span>
|
|
176
206
|
<span><i style="background:#7d8f99"></i><span id="lg-unk"></span></span>
|
|
207
|
+
<span id="lg-checkpoint-wrap" style="display:none"><i style="border:1.5px solid #3ee7ff"></i><span id="lg-checkpoint"></span></span>
|
|
177
208
|
<span class="hint" id="lg-hint"></span>
|
|
178
209
|
</div>
|
|
179
210
|
<div id="detail"></div>
|
|
@@ -193,17 +224,19 @@ var STRINGS={
|
|
|
193
224
|
legUnk:"UNKNOWN (no curated state)", stUnk:"?",
|
|
194
225
|
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
226
|
coverPart:"{n} of {t} units named by the document",
|
|
196
|
-
legHint:"— animated arrow = active flow · dashed = to build",
|
|
227
|
+
legHint:"— animated arrow = active flow · dashed = to build", legCheckpoint:"CHANGE IN THIS CHECKPOINT",
|
|
228
|
+
timelineLabel:"Architecture checkpoints", asIs:"AS-IS", asIsBadge:"verified code", cumulativeHint:"the graph is always cumulative", noArchChanges:"no architecture changes",
|
|
229
|
+
deltaAdd:"ADD", deltaUpdate:"UPDATE", deltaRewire:"REWIRE", deltaRemove:"REMOVE",
|
|
197
230
|
crumbRoot:"Context", crumbLevel:"C4", lvlContext:"CONTEXT",
|
|
198
231
|
lvlNames:{context:"Context",container:"Container",component:"Components",leaf:"Leaves"},
|
|
199
|
-
detClose:"[x] close", detFunc:"What it does", detNow:"Current state (facts)",
|
|
232
|
+
detClose:"[x] close", detFunc:"What it does", detNow:"Current state (facts)", detCheckpoint:"At this checkpoint", detChange:"Change from previous",
|
|
200
233
|
detTgt:"Target state (project)", detSrc:"Verification source", detIssue:"Issue",
|
|
201
234
|
detTarget:"TARGET",
|
|
202
235
|
rosterSearch:"filter…", rosterCount:"members", rosterEmpty:"no match", aggProgress:"progress",
|
|
203
|
-
stampBase:"Fact base:", stampMethod:"method:", stampVerify:"code+topology", staticSrc:"(static source)",
|
|
236
|
+
stampBase:"Fact base:", stampMethod:"method:", stampVerify:"code+topology", stampProjection:"projection:", staticSrc:"(static source)",
|
|
204
237
|
errEmpty:"Model unreachable or empty.",
|
|
205
238
|
errNotFound:"Model not found (./c4-model.json). Open via `forma serve` or inject window.__C4_MODEL__.",
|
|
206
|
-
errUnavail:"Model unavailable."
|
|
239
|
+
errUnavail:"Model unavailable.", errTimeline:"Invalid architecture timeline."
|
|
207
240
|
},
|
|
208
241
|
it:{
|
|
209
242
|
subhelp:"click = spiega (ogni livello) · doppio-click o [+] = entra · trascina per spostare · hover su una freccia = etichetta · ESC/BACK",
|
|
@@ -215,17 +248,19 @@ var STRINGS={
|
|
|
215
248
|
legUnk:"IGNOTO (stato non curato)", stUnk:"?",
|
|
216
249
|
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
250
|
coverPart:"{n} unità su {t} nominate dal documento",
|
|
218
|
-
legHint:"— freccia animata = flusso attivo · tratteggio = da costruire",
|
|
251
|
+
legHint:"— freccia animata = flusso attivo · tratteggio = da costruire", legCheckpoint:"CAMBIO IN QUESTO CHECKPOINT",
|
|
252
|
+
timelineLabel:"Checkpoint architetturali", asIs:"AS-IS", asIsBadge:"codice verificato", cumulativeHint:"il grafo è sempre cumulativo", noArchChanges:"nessun cambio architetturale",
|
|
253
|
+
deltaAdd:"ADD", deltaUpdate:"UPDATE", deltaRewire:"REWIRE", deltaRemove:"REMOVE",
|
|
219
254
|
crumbRoot:"Contesto", crumbLevel:"C4", lvlContext:"CONTESTO",
|
|
220
255
|
lvlNames:{context:"Contesto",container:"Container",component:"Componenti",leaf:"Foglie"},
|
|
221
|
-
detClose:"[x] chiudi", detFunc:"Cosa fa", detNow:"Stato attuale (fatti)",
|
|
256
|
+
detClose:"[x] chiudi", detFunc:"Cosa fa", detNow:"Stato attuale (fatti)", detCheckpoint:"A questo checkpoint", detChange:"Modifica dal precedente",
|
|
222
257
|
detTgt:"Stato finito (progetto)", detSrc:"Fonte verifica", detIssue:"Issue",
|
|
223
258
|
detTarget:"TARGET",
|
|
224
259
|
rosterSearch:"filtra…", rosterCount:"elementi", rosterEmpty:"nessun risultato", aggProgress:"avanzamento",
|
|
225
|
-
stampBase:"Base fatti:", stampMethod:"metodo:", stampVerify:"code+topology", staticSrc:"(fonte statica)",
|
|
260
|
+
stampBase:"Base fatti:", stampMethod:"metodo:", stampVerify:"code+topology", stampProjection:"proiezione:", staticSrc:"(fonte statica)",
|
|
226
261
|
errEmpty:"Modello non raggiungibile o vuoto.",
|
|
227
262
|
errNotFound:"Modello non trovato (./c4-model.json). Apri via `forma serve` o inietta window.__C4_MODEL__.",
|
|
228
|
-
errUnavail:"Modello non disponibile."
|
|
263
|
+
errUnavail:"Modello non disponibile.", errTimeline:"Timeline architetturale non valida."
|
|
229
264
|
}
|
|
230
265
|
};
|
|
231
266
|
var LANG="en", STR=STRINGS.en;
|
|
@@ -270,22 +305,40 @@ var STMAP={done:"done","in-progress":"prog",next:"next",planned:"plan",problem:"
|
|
|
270
305
|
// decoration, it is half the claim. Same shape as the level tally and the docmap coverage.
|
|
271
306
|
// Returns null when the box has its own verdict (it speaks for itself) or when nothing below it
|
|
272
307
|
// was ruled (there is no fact to report).
|
|
308
|
+
// A VERDICT is what counts as ruled, not a percentage: a document declares a capability finished,
|
|
309
|
+
// it does not measure it, so those nodes carry `done` with no completion at all. Counting only the
|
|
310
|
+
// numbers put `?` back on a domain holding nine packages a document calls finished — the same
|
|
311
|
+
// silence #42 closed. The mean is null when nobody below carries one; the coverage is not.
|
|
273
312
|
function rollStatus(node,kidsOf){
|
|
274
313
|
if(node.completion!=null||node.statusWord)return null;
|
|
275
314
|
var lv=[],q=[node],n,k,i,guard=0;
|
|
276
315
|
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);}
|
|
277
316
|
if(!lv.length)return null;
|
|
278
|
-
var sum=0,nc=0,rank={problem:5,next:4,"in-progress":3,planned:2,unknown:1.5,done:1},best=0,key="unknown",s2;
|
|
317
|
+
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;
|
|
279
318
|
for(i=0;i<lv.length;i++){
|
|
280
319
|
if(lv[i].completion!=null){sum+=(+lv[i].completion||0);nc++;}
|
|
281
320
|
s2=lv[i].status2||(lv[i].status==="planned"?"planned":"unknown");
|
|
321
|
+
if(s2!=="unknown")nr++;
|
|
282
322
|
if((rank[s2]||0)>best){best=rank[s2]||0;key=s2;}
|
|
283
323
|
}
|
|
284
|
-
if(!
|
|
285
|
-
|
|
324
|
+
if(!nr)return null;
|
|
325
|
+
// #43: the mean and the coverage must share a denominator or the badge lies twice in one string.
|
|
326
|
+
// Averaging over the children that carry a number while claiming coverage over the children that
|
|
327
|
+
// carry a verdict printed "100% 25/53" from a SINGLE measured child - the owner's complaint,
|
|
328
|
+
// verbatim. A mean is emitted only when every ruled child carries a number; otherwise the badge
|
|
329
|
+
// shows coverage alone, which is the thing actually known.
|
|
330
|
+
return {status2:key,mean:(nc&&nc===nr)?Math.round(sum/nc):null,ruled:nr,measured:nc,total:lv.length};
|
|
331
|
+
}
|
|
332
|
+
// The badge: the first number a stakeholder reads, so it is a function like everything else that
|
|
333
|
+
// makes a claim. Order of authority: a curated word, the node's own measurement, the roll-up, then
|
|
334
|
+
// nothing. `?` is reserved for a box NOBODY ruled on — printing it over a box a document declares
|
|
335
|
+
// finished made the badge contradict its own colour. A declaration has no number, and inventing
|
|
336
|
+
// one for it is the whole defect this file is being corrected for.
|
|
337
|
+
function badgeOf(n,roll){
|
|
338
|
+
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))
|
|
286
339
|
}
|
|
287
340
|
function tallyOf(kids,kidsOf){
|
|
288
|
-
var cnt={},tot=0,sum=0,nc=0,st3,i,j,k,q,lv,use,n;
|
|
341
|
+
var cnt={},tot=0,sum=0,nc=0,nr=0,st3,i,j,k,q,lv,use,n;
|
|
289
342
|
for(i=0;i<kids.length;i++){
|
|
290
343
|
lv=[];q=[kids[i]];
|
|
291
344
|
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);}
|
|
@@ -297,15 +350,73 @@ function tallyOf(kids,kidsOf){
|
|
|
297
350
|
st3=STMAP[n.status2]||(n.status==="planned"?"plan":"unk");
|
|
298
351
|
cnt[st3]=(cnt[st3]||0)+1;tot++;
|
|
299
352
|
if(n.completion!=null){sum+=(+n.completion||0);nc++;}
|
|
353
|
+
if(st3!=="unk")nr++;
|
|
300
354
|
}
|
|
301
355
|
}
|
|
302
|
-
|
|
356
|
+
// #43: `ruled` must mean the same thing here as in rollStatus - how many boxes somebody judged -
|
|
357
|
+
// or the breadcrumb and the badge contradict each other on the same screen. The mean stays
|
|
358
|
+
// subject to the same rule: it is emitted only when every judged box carries a number.
|
|
359
|
+
return {cnt:cnt,tot:tot,ruled:nr,measured:nc,mean:(nc&&nc===nr)?Math.round(sum/nc):null};
|
|
360
|
+
}
|
|
361
|
+
function jsonCopy(v){return JSON.parse(JSON.stringify(v));}
|
|
362
|
+
function timelineMatches(edges,match){
|
|
363
|
+
var out=[],i,e;for(i=0;i<edges.length;i++){e=edges[i];
|
|
364
|
+
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);
|
|
365
|
+
}return out;
|
|
366
|
+
}
|
|
367
|
+
// The browser materializer mirrors lib/validate.mjs after generation has rejected every ambiguous
|
|
368
|
+
// patch. It returns baseline+checkpoint states without storing a second graph in the model.
|
|
369
|
+
function timelineStates(base){
|
|
370
|
+
var tl=base&&base.timeline;if(!tl)return {errors:[],states:[]};
|
|
371
|
+
var errors=[],states=[],current=jsonCopy(base),c,cp,p,n,e,i,j,found,target,before;
|
|
372
|
+
if(!tl.checkpoints||!tl.checkpoints.length)return {errors:["timeline.checkpoints"],states:[]};
|
|
373
|
+
for(c=0;c<tl.checkpoints.length;c++){
|
|
374
|
+
cp=tl.checkpoints[c];p=cp.patch||{};n=p.nodes||{};e=p.edges||{};
|
|
375
|
+
var d={nodes:[],edges:[],removedNodes:[],removedEdges:[],counts:{add:0,update:0,rewire:0,remove:0}};
|
|
376
|
+
var adds=n.add||[],updates=n.update||[],nrem=n.remove||[],erem=e.remove||[],rew=e.rewire||[],eadd=e.add||[];
|
|
377
|
+
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++;}
|
|
378
|
+
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;}
|
|
379
|
+
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]);
|
|
380
|
+
d.nodes.push({id:updates[i].id,type:"UPDATE",change:updates[i].change});d.counts.update++;}
|
|
381
|
+
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;}
|
|
382
|
+
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++;}
|
|
383
|
+
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;}
|
|
384
|
+
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]);
|
|
385
|
+
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++;}
|
|
386
|
+
for(i=0;i<eadd.length;i++){current.edges.push(jsonCopy(eadd[i].edge));target=eadd[i].edge;
|
|
387
|
+
d.edges.push({from:target.from,to:target.to,label:target.label,kind:target.kind,type:"ADD",change:eadd[i].change});d.counts.add++;}
|
|
388
|
+
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;}
|
|
389
|
+
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;});
|
|
390
|
+
d.removedNodes.push({id:nrem[i].id,name:target.name,type:"REMOVE",change:nrem[i].change});d.counts.remove++;}
|
|
391
|
+
states.push({id:cp.id,label:cp.label,badge:cp.badge||"",model:jsonCopy(current),delta:d});
|
|
392
|
+
}
|
|
393
|
+
return {errors:errors,states:states};
|
|
394
|
+
}
|
|
395
|
+
function timelineSummary(id,delta){
|
|
396
|
+
var c=(delta&&delta.counts)||{},parts=[];
|
|
397
|
+
if(c.add)parts.push({key:"deltaAdd",count:c.add});
|
|
398
|
+
if(c.update)parts.push({key:"deltaUpdate",count:c.update});
|
|
399
|
+
if(c.rewire)parts.push({key:"deltaRewire",count:c.rewire});
|
|
400
|
+
if(c.remove)parts.push({key:"deltaRemove",count:c.remove});
|
|
401
|
+
return {parts:parts,empty:id!=="as-is"&&!parts.length};
|
|
303
402
|
}
|
|
304
|
-
var M=null,
|
|
403
|
+
var BASE=null,M=null,TSTATES=[],CPID="as-is",CPDELTA={nodes:[],edges:[],removedNodes:[],removedEdges:[],counts:{add:0,update:0,rewire:0,remove:0}}
|
|
404
|
+
var stack=[null],mode="now",POS={},POSALL={},drag=null,justDragged=false,CATNODES={},FETCHED=false
|
|
305
405
|
var VBOX={}; // last fitted viewBox per level — frozen while dragging so the canvas does not rescale
|
|
306
406
|
function $(i){return document.getElementById(i)}
|
|
307
407
|
function esc(s){return String(s==null?"":s).replace(/&/g,"&").replace(/</g,"<").replace(/"/g,""")}
|
|
308
408
|
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;}
|
|
409
|
+
function nodeChange(id){for(var i=0;i<CPDELTA.nodes.length;i++)if(CPDELTA.nodes[i].id===id)return CPDELTA.nodes[i];return null;}
|
|
410
|
+
function nodeProjected(id){
|
|
411
|
+
var projected=false;
|
|
412
|
+
for(var i=0;i<TSTATES.length;i++){
|
|
413
|
+
var d=TSTATES[i].delta,j;
|
|
414
|
+
for(j=0;j<d.nodes.length;j++)if(d.nodes[j].id===id)projected=true;
|
|
415
|
+
for(j=0;j<d.removedNodes.length;j++)if(d.removedNodes[j].id===id)projected=false;
|
|
416
|
+
if(TSTATES[i].id===CPID)break;
|
|
417
|
+
}
|
|
418
|
+
return projected;
|
|
419
|
+
}
|
|
309
420
|
// Layout key = the id of the level's parent ("root" at the top). Stable and documentable: it is
|
|
310
421
|
// exactly the key used by meta.layout hints and by the exported layout JSON.
|
|
311
422
|
function keyOf(){return stack[stack.length-1]||"root";}
|
|
@@ -355,9 +466,43 @@ function makeCatalogNode(pid,g){
|
|
|
355
466
|
}
|
|
356
467
|
function hasKids(id){for(var i=0;i<M.nodes.length;i++)if(M.nodes[i].parent===id)return true;return false;}
|
|
357
468
|
function node(id){for(var i=0;i<M.nodes.length;i++)if(M.nodes[i].id===id)return M.nodes[i];return null;}
|
|
358
|
-
function
|
|
359
|
-
|
|
360
|
-
var ev=(n.evidence||[]).filter(function(e){return e.type==="doc"||e.type==="path";})[0];
|
|
469
|
+
function srcOf(n){if(CPID!=="as-is"&&nodeProjected(n.id)&&BASE&&BASE.timeline)return BASE.timeline.source;
|
|
470
|
+
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;}
|
|
471
|
+
var ev=(n.evidence||[]).filter(function(e){return e.type==="doc"||e.type==="path";})[0];
|
|
472
|
+
return ev?ev.ref:((M.meta&&M.meta.verifyMethod)||"");}
|
|
473
|
+
function edgeChange(e){
|
|
474
|
+
for(var i=0;i<CPDELTA.edges.length;i++){var d=CPDELTA.edges[i];
|
|
475
|
+
if(d.from===e.from&&d.to===e.to&&(d.label||"")===(e.label||"")&&(d.kind||"")===(e.kind||""))return d;
|
|
476
|
+
}return null;
|
|
477
|
+
}
|
|
478
|
+
function emptyDelta(){return {nodes:[],edges:[],removedNodes:[],removedEdges:[],counts:{add:0,update:0,rewire:0,remove:0}};}
|
|
479
|
+
function renderTimeline(){
|
|
480
|
+
var nav=$("timeline"),legacy=$("legacytime"),legend=$("lg-checkpoint-wrap");
|
|
481
|
+
if(!BASE||!BASE.timeline){nav.style.display="none";legacy.style.display="inline-flex";legend.style.display="none";return;}
|
|
482
|
+
nav.style.display="block";legacy.style.display="none";legend.style.display="inline";
|
|
483
|
+
nav.setAttribute("aria-label",STR.timelineLabel);
|
|
484
|
+
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>';
|
|
485
|
+
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>';}
|
|
486
|
+
$("timeline-track").innerHTML=html;
|
|
487
|
+
var summary=timelineSummary(CPID,CPDELTA),parts=[];
|
|
488
|
+
for(i=0;i<summary.parts.length;i++)parts.push('<b>'+STR[summary.parts[i].key]+'</b> '+summary.parts[i].count);
|
|
489
|
+
$("checkpoint-summary").innerHTML=parts.length?parts.join(" · ")+" · "+esc(STR.cumulativeHint)
|
|
490
|
+
:(summary.empty?'<b>'+esc(STR.noArchChanges)+'</b> · ':'')+esc(STR.cumulativeHint);
|
|
491
|
+
}
|
|
492
|
+
function checkpointUrl(id){
|
|
493
|
+
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){}
|
|
494
|
+
}
|
|
495
|
+
function setCheckpoint(id,writeUrl,animate,preserveLegacyMode){
|
|
496
|
+
var state=null,i;for(i=0;i<TSTATES.length;i++)if(TSTATES[i].id===id){state=TSTATES[i];break;}
|
|
497
|
+
if(!state){id="as-is";M=jsonCopy(BASE);CPDELTA=emptyDelta();}else{M=jsonCopy(state.model);CPDELTA=jsonCopy(state.delta);}
|
|
498
|
+
CPID=id;mode=preserveLegacyMode&&(!BASE||!BASE.timeline)&&mode==="target"?"target":"now";
|
|
499
|
+
$("bnow").className=mode==="now"?"on":"";$("btgt").className=mode==="target"?"on":"";
|
|
500
|
+
var posKey="checkpoint:"+id;if(!POSALL[posKey])POSALL[posKey]={};POS=POSALL[posKey];VBOX={};
|
|
501
|
+
while(stack.length>1&&!node(stack[stack.length-1]))stack.pop();
|
|
502
|
+
if(!stack.length)stack=[null];
|
|
503
|
+
$("detail").style.display="none";
|
|
504
|
+
renderTimeline();stamp("");if(writeUrl)checkpointUrl(id);draw(animate!==false);
|
|
505
|
+
}
|
|
361
506
|
|
|
362
507
|
var NW=228,NH=118; // NH fits 3 description lines without hitting the [+] DRILL
|
|
363
508
|
// Past DENSE_MIN siblings a full-size card cannot stay legible: the fit-to-content viewBox scales
|
|
@@ -464,7 +609,7 @@ function draw(animate){
|
|
|
464
609
|
+'<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>';
|
|
465
610
|
for(j=0;j<geo.length;j++){
|
|
466
611
|
var e=geo[j].g,ed=geo[j].e,cl=(ed.estatus==="to-build"?"plan":"flow");
|
|
467
|
-
s+='<path class="edge '+cl+'" data-e="'+geo[j].i+'" d="'+e.d+'"/>';
|
|
612
|
+
s+='<path class="edge '+cl+(ed.delta?" delta":"")+'" data-e="'+geo[j].i+'" d="'+e.d+'"/>';
|
|
468
613
|
s+='<path class="edgehit" data-e="'+geo[j].i+'" data-label="'+esc(ed.label||"")+'" d="'+e.d+'"/>';
|
|
469
614
|
if(showLbl&&ed.label){var lb=String(ed.label);if(lb.length>28)lb=lb.slice(0,27)+"…";
|
|
470
615
|
s+='<text class="elbl" x="'+e.mx+'" y="'+(e.my-4)+'" text-anchor="middle">'+esc(lb)+'</text>';}
|
|
@@ -476,7 +621,10 @@ function draw(animate){
|
|
|
476
621
|
var roll=(n.__catalog||mode==="target")?null:rollStatus(n,childrenOf);
|
|
477
622
|
var st=STMAP[(roll?roll.status2:n.status2)]||(n.status==="planned"?"plan":"unk");
|
|
478
623
|
var pulse=(st==="next"||st==="prog")&&mode!=="target"&&!n.__catalog;
|
|
479
|
-
|
|
624
|
+
// `decl`: this verdict was DERIVED from a repo document, not proven. The legend has carried a
|
|
625
|
+
// hollow green for it since #38 and the canvas never drew one, so every declared done shipped
|
|
626
|
+
// dressed as proof.
|
|
627
|
+
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":"");
|
|
480
628
|
var desc=isCat?(n.count+" "+STR.rosterCount):(mode==="target"?(n.target||n.func||""):(n.current||n.func||""));
|
|
481
629
|
// 26 of a real repo's 53 boxes printed "1 file: advisor." under a box titled "internal/advisor".
|
|
482
630
|
// A description that only restates the title is ink, not information: drop it and let the box
|
|
@@ -485,14 +633,14 @@ function draw(animate){
|
|
|
485
633
|
if(!isCat&&echoesName(desc,n.name))desc="";
|
|
486
634
|
// The coverage rides WITH the mean, never without it: `100%` on a domain where 14 of 22 packages
|
|
487
635
|
// were ruled is the invented green, and this is the badge a stakeholder reads first.
|
|
488
|
-
var badge=isCat?String(n.count):(mode==="target"?STR.detTarget:(n
|
|
636
|
+
var badge=isCat?String(n.count):(mode==="target"?STR.detTarget:badgeOf(n,roll));
|
|
489
637
|
// the title runs until the badge, so reserve the badge's actual width (9px text ≈ 5.4px/char)
|
|
490
638
|
// instead of a flat 70px — a fixed reserve clipped names that had all the room they needed
|
|
491
639
|
var tmax=Math.floor((p.w-34-String(badge).length*5.4)/6.7),tt=n.name;
|
|
492
640
|
if(String(tt).length>tmax)tt=String(tt).slice(0,Math.max(1,tmax-1))+"…";
|
|
493
641
|
s+='<g class="'+cls+'" data-id="'+esc(n.id)+'">'
|
|
494
642
|
+'<rect x="'+p.x+'" y="'+p.y+'" width="'+p.w+'" height="'+p.h+'" rx="10"/>'
|
|
495
|
-
+'<text class="kk" x="'+(p.x+13)+'" y="'+(p.y+16)+'">'+esc(n.category||n.kind||"")+'</text>'
|
|
643
|
+
+'<text class="kk" x="'+(p.x+13)+'" y="'+(p.y+16)+'">'+esc((n.category||n.kind||"")+(change?" · "+change.type:""))+'</text>'
|
|
496
644
|
+'<text class="tt" x="'+(p.x+13)+'" y="'+(p.y+33)+'">'+esc(tt)+'</text>'
|
|
497
645
|
+'<text class="pp" x="'+(p.x+p.w-13)+'" y="'+(p.y+16)+'" text-anchor="end">'+esc(badge)+'</text>';
|
|
498
646
|
// Description lines: how many fit comes from the geometry (text starts at y+51, 12.5px apart,
|
|
@@ -508,8 +656,8 @@ function draw(animate){
|
|
|
508
656
|
}
|
|
509
657
|
s+='</svg>';
|
|
510
658
|
var st2=$("stage");
|
|
511
|
-
var etip=$("etip"); // preserve
|
|
512
|
-
st2.innerHTML=s;st2.appendChild(etip);
|
|
659
|
+
var etip=$("etip"),err=$("err"); // preserve the live overlays across redraw/re-verify
|
|
660
|
+
st2.innerHTML=s;if(etip)st2.appendChild(etip);if(err)st2.appendChild(err);
|
|
513
661
|
st2.classList.toggle("anim",animate!==false);
|
|
514
662
|
// breadcrumb
|
|
515
663
|
var chain=[],c=pid;while(c){var nn=node(c);chain.unshift(nn);c=nn?nn.parent:null;}
|
|
@@ -528,9 +676,11 @@ function draw(animate){
|
|
|
528
676
|
var DOT={done:"#2fe08a",prog:"#ffc74d",next:"#3ee7ff",plan:"#2d5a72",prob:"#ff5d5d",unk:"#7d8f99"},ord=["done","prog","next","plan","prob","unk"],agg="";
|
|
529
677
|
for(j=0;j<ord.length;j++)if(T.cnt[ord[j]])agg+='<i style="background:'+DOT[ord[j]]+'"></i>'+T.cnt[ord[j]];
|
|
530
678
|
// The coverage rides WITH the percentage, never without it. See tallyOf.
|
|
531
|
-
if(T.ruled)agg+=' '+esc(STR.aggProgress)+' <b>'+T.mean+'%</b> '+T.ruled+'/'+T.tot;
|
|
679
|
+
if(T.ruled)agg+=' '+(T.mean!=null?esc(STR.aggProgress)+' <b>'+T.mean+'%</b> ':'')+T.ruled+'/'+T.tot;
|
|
532
680
|
cb='<span class="agg">'+agg+'</span>'+cb;
|
|
533
681
|
}
|
|
682
|
+
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;
|
|
683
|
+
cb='<b>'+esc(cpLabel)+'</b> · '+cb;}
|
|
534
684
|
$("crumb").innerHTML=cb;
|
|
535
685
|
$("back").disabled=stack.length<2;
|
|
536
686
|
$("detail").style.display="none";
|
|
@@ -592,8 +742,8 @@ function rollEdges(edges,vis,parent){
|
|
|
592
742
|
if(!f||!t||f===t)continue;
|
|
593
743
|
c=parseInt(String(e.label),10);if(!(c>0))c=1;
|
|
594
744
|
k=f+"|"+t;
|
|
595
|
-
if(ix[k]==null){ix[k]=out.length;out.push({from:f,to:t,label:e.label,estatus:e.estatus,n:c});}
|
|
596
|
-
else{g=out[ix[k]];g.n+=c;g.label=g.n+"×";if(e.estatus!=="to-build")g.estatus=e.estatus;}
|
|
745
|
+
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});}
|
|
746
|
+
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);}
|
|
597
747
|
}
|
|
598
748
|
return out;
|
|
599
749
|
}
|
|
@@ -616,11 +766,13 @@ function cssv(v){try{return getComputedStyle(document.documentElement).getProper
|
|
|
616
766
|
|
|
617
767
|
function showDetail(n){
|
|
618
768
|
if(n&&n.__catalog)return showRoster(n);
|
|
619
|
-
var src=srcOf(n),cur=n.current||n.func||"",showSrc=src&&String(cur).indexOf(src)<0;
|
|
769
|
+
var src=srcOf(n),cur=n.current||n.func||n.description||"",showSrc=src&&String(cur).indexOf(src)<0;
|
|
770
|
+
var timeline=!!(BASE&&BASE.timeline),change=timeline?nodeChange(n.id):null;
|
|
620
771
|
$("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>'
|
|
621
772
|
+(n.func?'<div class="row"><span class="lbl">'+esc(STR.detFunc)+'</span><span class="func">'+esc(n.func)+'</span></div>':'')
|
|
622
|
-
+(cur&&cur!==n.func?'<div class="row"><span class="lbl">'+esc(STR.detNow)+'</span><span class="now">'+esc(cur)+'</span></div>':'')
|
|
623
|
-
+((n.target&&n.target!=="—")?'<div class="row"><span class="lbl">'+esc(STR.detTgt)+'</span><span class="tgt">'+esc(n.target)+'</span></div>':'')
|
|
773
|
+
+(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>':'')
|
|
774
|
+
+(!timeline&&(n.target&&n.target!=="—")?'<div class="row"><span class="lbl">'+esc(STR.detTgt)+'</span><span class="tgt">'+esc(n.target)+'</span></div>':'')
|
|
775
|
+
+(change?'<div class="row"><span class="lbl">'+esc(STR.detChange)+'</span><span class="tgt">'+esc(change.type+" · "+change.change)+'</span></div>':'')
|
|
624
776
|
// A number DERIVED from a repo document is a declaration, not a verification: the label says which
|
|
625
777
|
// one, and the coverage says how much of this module the citation actually reaches. "3 of 3 rows
|
|
626
778
|
// declared done" and "this module is done" are not the same sentence when the module holds 22
|
|
@@ -726,12 +878,14 @@ function wire(){
|
|
|
726
878
|
document.addEventListener("keydown",function(e){if(e.key==="Escape"&&stack.length>1){stack.pop();draw(true);}});
|
|
727
879
|
$("bnow").addEventListener("click",function(){mode="now";$("bnow").className="on";$("btgt").className="";draw(false);});
|
|
728
880
|
$("btgt").addEventListener("click",function(){mode="target";$("btgt").className="on";$("bnow").className="";draw(false);});
|
|
881
|
+
$("timeline").addEventListener("click",function(ev){var b=ev.target;while(b&&b!==$("timeline")&&!(b.getAttribute&&b.getAttribute("data-checkpoint")))b=b.parentNode;
|
|
882
|
+
if(b&&b!==$("timeline"))setCheckpoint(b.getAttribute("data-checkpoint"),true,true);});
|
|
729
883
|
$("brf").addEventListener("click",function(){
|
|
730
884
|
// Served model ⇒ actually re-read it (a `forma verify` run in the terminal shows up here without
|
|
731
885
|
// losing the level, the layout or the mode). Injected/offline ⇒ re-stamp, and say why.
|
|
732
886
|
if(!FETCHED){stamp(STR.staticSrc);return;}
|
|
733
887
|
fetch("./c4-model.json?ts="+(new Date()).getTime()).then(function(r){return r.json();})
|
|
734
|
-
.then(function(m){if(m&&m.nodes&&m.nodes.length)
|
|
888
|
+
.then(function(m){if(m&&m.nodes&&m.nodes.length)loadModel(m,true);else stamp(STR.staticSrc);})
|
|
735
889
|
.catch(function(){stamp(STR.staticSrc);});
|
|
736
890
|
});
|
|
737
891
|
$("blbl").addEventListener("click",function(){LABELS=!($("blbl").className==="on");draw(false);});
|
|
@@ -749,14 +903,14 @@ function applyStrings(){
|
|
|
749
903
|
set("pexp",STR.printExport);set("pexpSvg",STR.exportSvg);set("pexpPng",STR.exportPng);
|
|
750
904
|
set("pexpLayout",STR.exportLayout);set("pexpPrint",STR.print);
|
|
751
905
|
set("lg-done",STR.legDone);set("lg-decl",STR.legDecl);set("lg-prog",STR.legProg);set("lg-next",STR.legNext);
|
|
752
|
-
set("lg-plan",STR.legPlan);set("lg-prob",STR.legProb);set("lg-unk",STR.legUnk);set("lg-hint",STR.legHint);
|
|
906
|
+
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);
|
|
753
907
|
}
|
|
754
908
|
// ---- FEATURE #2: PRINT / EXPORT (all client-side, offline) ----
|
|
755
909
|
function exportBaseName(){
|
|
756
910
|
var repo=(M&&M.source&&M.source.repo)||"forma";
|
|
757
911
|
var pid=stack[stack.length-1];
|
|
758
912
|
var lvl=pid?(node(pid)&&node(pid).name)||"level":"context";
|
|
759
|
-
return String(repo+"-"+lvl).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"forma";
|
|
913
|
+
return String(repo+"-"+(BASE&&BASE.timeline?CPID+"-":"")+lvl).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"forma";
|
|
760
914
|
}
|
|
761
915
|
// Collect the CSS rules the SVG needs so a standalone serialize renders styled.
|
|
762
916
|
function collectSvgCss(){
|
|
@@ -866,24 +1020,29 @@ function moveTip(ev){var st=$("stage").getBoundingClientRect(),t=$("etip");
|
|
|
866
1020
|
function stamp(suffix){
|
|
867
1021
|
var m=(M&&M.meta)||{};
|
|
868
1022
|
$("stamp").textContent=STR.stampBase+" "+(m.verifiedAt||(M&&M.generatedAt)||(M&&M.source&&M.source.commit)||"—")
|
|
869
|
-
+" · "+STR.stampMethod+" "+(m.verifyMethod||STR.stampVerify)
|
|
1023
|
+
+" · "+STR.stampMethod+" "+(m.verifyMethod||STR.stampVerify)
|
|
1024
|
+
+(BASE&&BASE.timeline?" · "+STR.stampProjection+" "+BASE.timeline.source:"")+(suffix?" "+suffix:"");
|
|
870
1025
|
}
|
|
871
|
-
function
|
|
872
|
-
|
|
873
|
-
|
|
1026
|
+
function loadModel(model,preserveCheckpoint){
|
|
1027
|
+
var wanted=preserveCheckpoint?CPID:null,candidate=null;
|
|
1028
|
+
try{candidate=jsonCopy(model);}catch(e){}
|
|
1029
|
+
LANG=pickLang(candidate);STR=STRINGS[LANG];
|
|
874
1030
|
try{document.documentElement.setAttribute("lang",LANG);}catch(e){}
|
|
875
1031
|
applyStrings();
|
|
876
|
-
if(!
|
|
1032
|
+
if(!candidate||!candidate.nodes||!candidate.nodes.length){$("err").style.display="block";$("err").textContent=STR.errEmpty;return;}
|
|
1033
|
+
var built=timelineStates(candidate);if(built.errors.length){$("err").style.display="block";$("err").textContent=STR.errTimeline+" "+built.errors.join(", ");return;}
|
|
1034
|
+
BASE=candidate;M=jsonCopy(candidate);
|
|
1035
|
+
$("err").style.display="none";TSTATES=built.states;
|
|
877
1036
|
var ttl=(M.meta&&M.meta.title)||((M.source&&M.source.repo)?"Forma · "+M.source.repo:null);
|
|
878
1037
|
if(ttl){$("title").textContent=ttl;try{document.title=ttl;}catch(e){}}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
draw(true);
|
|
1038
|
+
if(!preserveCheckpoint)setSkin((new URLSearchParams(location.search).get("skin"))||window.__C4_SKIN__||(M.meta&&M.meta.skin)||"holo");
|
|
1039
|
+
if(!preserveCheckpoint){stack=[null];POSALL={};wanted=null;try{wanted=new URLSearchParams(location.search).get("checkpoint");}catch(e){}}
|
|
1040
|
+
setCheckpoint(wanted||"as-is",false,!preserveCheckpoint,preserveCheckpoint);
|
|
883
1041
|
// Minimal hook for embedders (the Cowork artifact re-verifies through its own MCP bridge and
|
|
884
1042
|
// needs to push the result back in without forking the viewer).
|
|
885
|
-
window.__C4_API__={model:function(){return M;},redraw:function(){draw(false);},stamp:function(t){$("stamp").textContent=t;}};
|
|
1043
|
+
window.__C4_API__={model:function(){return M;},checkpoint:function(id){setCheckpoint(id,true,true);},redraw:function(){draw(false);},stamp:function(t){$("stamp").textContent=t;}};
|
|
886
1044
|
}
|
|
1045
|
+
function boot(model){loadModel(model,false);}
|
|
887
1046
|
function start(){
|
|
888
1047
|
// Pick language from URL/global up-front so header + errors render even if the model never loads.
|
|
889
1048
|
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.1",
|
|
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",
|