forma-arch 0.10.1 → 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.
@@ -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
- .s-unk rect{stroke:var(--unk);stroke-dasharray:1 3}
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>
@@ -193,17 +228,19 @@ var STRINGS={
193
228
  legUnk:"UNKNOWN (no curated state)", stUnk:"?",
194
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",
@@ -215,17 +252,19 @@ var STRINGS={
215
252
  legUnk:"IGNOTO (stato non curato)", stUnk:"?",
216
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;
@@ -270,22 +309,40 @@ var STMAP={done:"done","in-progress":"prog",next:"next",planned:"plan",problem:"
270
309
  // decoration, it is half the claim. Same shape as the level tally and the docmap coverage.
271
310
  // Returns null when the box has its own verdict (it speaks for itself) or when nothing below it
272
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.
273
316
  function rollStatus(node,kidsOf){
274
317
  if(node.completion!=null||node.statusWord)return null;
275
318
  var lv=[],q=[node],n,k,i,guard=0;
276
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);}
277
320
  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;
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;
279
322
  for(i=0;i<lv.length;i++){
280
323
  if(lv[i].completion!=null){sum+=(+lv[i].completion||0);nc++;}
281
324
  s2=lv[i].status2||(lv[i].status==="planned"?"planned":"unknown");
325
+ if(s2!=="unknown")nr++;
282
326
  if((rank[s2]||0)>best){best=rank[s2]||0;key=s2;}
283
327
  }
284
- if(!nc)return null;
285
- return {status2:key,mean:Math.round(sum/nc),ruled:nc,total:lv.length};
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))
286
343
  }
287
344
  function tallyOf(kids,kidsOf){
288
- 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;
289
346
  for(i=0;i<kids.length;i++){
290
347
  lv=[];q=[kids[i]];
291
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);}
@@ -297,15 +354,65 @@ function tallyOf(kids,kidsOf){
297
354
  st3=STMAP[n.status2]||(n.status==="planned"?"plan":"unk");
298
355
  cnt[st3]=(cnt[st3]||0)+1;tot++;
299
356
  if(n.completion!=null){sum+=(+n.completion||0);nc++;}
357
+ if(st3!=="unk")nr++;
300
358
  }
301
359
  }
302
- return {cnt:cnt,tot:tot,ruled:nc,mean:nc?Math.round(sum/nc):null};
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;
370
+ }
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};
303
398
  }
304
- var M=null, stack=[null], mode="now", POS={}, drag=null, justDragged=false, CATNODES={}, FETCHED=false
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
305
401
  var VBOX={}; // last fitted viewBox per level — frozen while dragging so the canvas does not rescale
306
402
  function $(i){return document.getElementById(i)}
307
403
  function esc(s){return String(s==null?"":s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/"/g,"&quot;")}
308
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
+ }
309
416
  // Layout key = the id of the level's parent ("root" at the top). Stable and documentable: it is
310
417
  // exactly the key used by meta.layout hints and by the exported layout JSON.
311
418
  function keyOf(){return stack[stack.length-1]||"root";}
@@ -356,8 +463,52 @@ function makeCatalogNode(pid,g){
356
463
  function hasKids(id){for(var i=0;i<M.nodes.length;i++)if(M.nodes[i].parent===id)return true;return false;}
357
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;}
358
465
  function nameOf(id){var n=node(id);return n?n.name:id;}
359
- function srcOf(n){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;}
360
- var ev=(n.evidence||[]).filter(function(e){return e.type==="doc"||e.type==="path";})[0];return ev?ev.ref:((M.meta&&M.meta.verifyMethod)||"");}
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
+ }
361
512
 
362
513
  var NW=228,NH=118; // NH fits 3 description lines without hitting the [+] DRILL
363
514
  // Past DENSE_MIN siblings a full-size card cannot stay legible: the fit-to-content viewBox scales
@@ -464,7 +615,7 @@ function draw(animate){
464
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>';
465
616
  for(j=0;j<geo.length;j++){
466
617
  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+'"/>';
618
+ s+='<path class="edge '+cl+(ed.delta?" delta":"")+'" data-e="'+geo[j].i+'" d="'+e.d+'"/>';
468
619
  s+='<path class="edgehit" data-e="'+geo[j].i+'" data-label="'+esc(ed.label||"")+'" d="'+e.d+'"/>';
469
620
  if(showLbl&&ed.label){var lb=String(ed.label);if(lb.length>28)lb=lb.slice(0,27)+"…";
470
621
  s+='<text class="elbl" x="'+e.mx+'" y="'+(e.my-4)+'" text-anchor="middle">'+esc(lb)+'</text>';}
@@ -476,7 +627,10 @@ function draw(animate){
476
627
  var roll=(n.__catalog||mode==="target")?null:rollStatus(n,childrenOf);
477
628
  var st=STMAP[(roll?roll.status2:n.status2)]||(n.status==="planned"?"plan":"unk");
478
629
  var pulse=(st==="next"||st==="prog")&&mode!=="target"&&!n.__catalog;
479
- var isCat=!!n.__catalog,dr=!isCat&&hasKids(n.id),cls="nd s-"+st+(dr?" drill":"")+(isCat?" cat":"")+(pulse?" pulse":"");
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":"");
480
634
  var desc=isCat?(n.count+" "+STR.rosterCount):(mode==="target"?(n.target||n.func||""):(n.current||n.func||""));
481
635
  // 26 of a real repo's 53 boxes printed "1 file: advisor." under a box titled "internal/advisor".
482
636
  // A description that only restates the title is ink, not information: drop it and let the box
@@ -485,14 +639,14 @@ function draw(animate){
485
639
  if(!isCat&&echoesName(desc,n.name))desc="";
486
640
  // The coverage rides WITH the mean, never without it: `100%` on a domain where 14 of 22 packages
487
641
  // 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.statusWord||(n.completion!=null?n.completion+"%":(roll?roll.mean+"% "+roll.ruled+"/"+roll.total:STR.stUnk))));
642
+ var badge=isCat?String(n.count):(mode==="target"?STR.detTarget:badgeOf(n,roll));
489
643
  // the title runs until the badge, so reserve the badge's actual width (9px text ≈ 5.4px/char)
490
644
  // instead of a flat 70px — a fixed reserve clipped names that had all the room they needed
491
645
  var tmax=Math.floor((p.w-34-String(badge).length*5.4)/6.7),tt=n.name;
492
646
  if(String(tt).length>tmax)tt=String(tt).slice(0,Math.max(1,tmax-1))+"…";
493
647
  s+='<g class="'+cls+'" data-id="'+esc(n.id)+'">'
494
648
  +'<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>'
649
+ +'<text class="kk" x="'+(p.x+13)+'" y="'+(p.y+16)+'">'+esc((n.category||n.kind||"")+(change?" · "+change.type:""))+'</text>'
496
650
  +'<text class="tt" x="'+(p.x+13)+'" y="'+(p.y+33)+'">'+esc(tt)+'</text>'
497
651
  +'<text class="pp" x="'+(p.x+p.w-13)+'" y="'+(p.y+16)+'" text-anchor="end">'+esc(badge)+'</text>';
498
652
  // Description lines: how many fit comes from the geometry (text starts at y+51, 12.5px apart,
@@ -508,8 +662,8 @@ function draw(animate){
508
662
  }
509
663
  s+='</svg>';
510
664
  var st2=$("stage");
511
- var etip=$("etip"); // preserve tooltip node
512
- 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);
513
667
  st2.classList.toggle("anim",animate!==false);
514
668
  // breadcrumb
515
669
  var chain=[],c=pid;while(c){var nn=node(c);chain.unshift(nn);c=nn?nn.parent:null;}
@@ -528,9 +682,11 @@ function draw(animate){
528
682
  var DOT={done:"#2fe08a",prog:"#ffc74d",next:"#3ee7ff",plan:"#2d5a72",prob:"#ff5d5d",unk:"#7d8f99"},ord=["done","prog","next","plan","prob","unk"],agg="";
529
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]];
530
684
  // The coverage rides WITH the percentage, never without it. See tallyOf.
531
- if(T.ruled)agg+=' &nbsp;'+esc(STR.aggProgress)+' <b>'+T.mean+'%</b> '+T.ruled+'/'+T.tot;
685
+ if(T.ruled)agg+=' &nbsp;'+(T.mean!=null?esc(STR.aggProgress)+' <b>'+T.mean+'%</b> ':'')+T.ruled+'/'+T.tot;
532
686
  cb='<span class="agg">'+agg+'</span>'+cb;
533
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> &nbsp;·&nbsp; '+cb;}
534
690
  $("crumb").innerHTML=cb;
535
691
  $("back").disabled=stack.length<2;
536
692
  $("detail").style.display="none";
@@ -592,8 +748,8 @@ function rollEdges(edges,vis,parent){
592
748
  if(!f||!t||f===t)continue;
593
749
  c=parseInt(String(e.label),10);if(!(c>0))c=1;
594
750
  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;}
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);}
597
753
  }
598
754
  return out;
599
755
  }
@@ -616,11 +772,13 @@ function cssv(v){try{return getComputedStyle(document.documentElement).getProper
616
772
 
617
773
  function showDetail(n){
618
774
  if(n&&n.__catalog)return showRoster(n);
619
- 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;
620
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>'
621
778
  +(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>':'')
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>':'')
624
782
  // A number DERIVED from a repo document is a declaration, not a verification: the label says which
625
783
  // one, and the coverage says how much of this module the citation actually reaches. "3 of 3 rows
626
784
  // declared done" and "this module is done" are not the same sentence when the module holds 22
@@ -726,12 +884,14 @@ function wire(){
726
884
  document.addEventListener("keydown",function(e){if(e.key==="Escape"&&stack.length>1){stack.pop();draw(true);}});
727
885
  $("bnow").addEventListener("click",function(){mode="now";$("bnow").className="on";$("btgt").className="";draw(false);});
728
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);});
729
889
  $("brf").addEventListener("click",function(){
730
890
  // Served model ⇒ actually re-read it (a `forma verify` run in the terminal shows up here without
731
891
  // losing the level, the layout or the mode). Injected/offline ⇒ re-stamp, and say why.
732
892
  if(!FETCHED){stamp(STR.staticSrc);return;}
733
893
  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){M=m;stamp("");draw(false);}else stamp(STR.staticSrc);})
894
+ .then(function(m){if(m&&m.nodes&&m.nodes.length)loadModel(m,true);else stamp(STR.staticSrc);})
735
895
  .catch(function(){stamp(STR.staticSrc);});
736
896
  });
737
897
  $("blbl").addEventListener("click",function(){LABELS=!($("blbl").className==="on");draw(false);});
@@ -749,14 +909,14 @@ function applyStrings(){
749
909
  set("pexp",STR.printExport);set("pexpSvg",STR.exportSvg);set("pexpPng",STR.exportPng);
750
910
  set("pexpLayout",STR.exportLayout);set("pexpPrint",STR.print);
751
911
  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);
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);
753
913
  }
754
914
  // ---- FEATURE #2: PRINT / EXPORT (all client-side, offline) ----
755
915
  function exportBaseName(){
756
916
  var repo=(M&&M.source&&M.source.repo)||"forma";
757
917
  var pid=stack[stack.length-1];
758
918
  var lvl=pid?(node(pid)&&node(pid).name)||"level":"context";
759
- 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";
760
920
  }
761
921
  // Collect the CSS rules the SVG needs so a standalone serialize renders styled.
762
922
  function collectSvgCss(){
@@ -866,24 +1026,29 @@ function moveTip(ev){var st=$("stage").getBoundingClientRect(),t=$("etip");
866
1026
  function stamp(suffix){
867
1027
  var m=(M&&M.meta)||{};
868
1028
  $("stamp").textContent=STR.stampBase+" "+(m.verifiedAt||(M&&M.generatedAt)||(M&&M.source&&M.source.commit)||"—")
869
- +" · "+STR.stampMethod+" "+(m.verifyMethod||STR.stampVerify)+(suffix?" "+suffix:"");
1029
+ +" · "+STR.stampMethod+" "+(m.verifyMethod||STR.stampVerify)
1030
+ +(BASE&&BASE.timeline?" · "+STR.stampProjection+" "+BASE.timeline.source:"")+(suffix?" "+suffix:"");
870
1031
  }
871
- function boot(model){
872
- M=model;
873
- LANG=pickLang(M);STR=STRINGS[LANG];
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];
874
1036
  try{document.documentElement.setAttribute("lang",LANG);}catch(e){}
875
1037
  applyStrings();
876
- if(!M||!M.nodes||!M.nodes.length){$("err").style.display="block";$("err").textContent=STR.errEmpty;return;}
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;
877
1042
  var ttl=(M.meta&&M.meta.title)||((M.source&&M.source.repo)?"Forma · "+M.source.repo:null);
878
1043
  if(ttl){$("title").textContent=ttl;try{document.title=ttl;}catch(e){}}
879
- stamp("");
880
- setSkin((new URLSearchParams(location.search).get("skin"))||window.__C4_SKIN__||(M.meta&&M.meta.skin)||"holo");
881
- stack=[null];mode="now";POS={};
882
- 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);
883
1047
  // Minimal hook for embedders (the Cowork artifact re-verifies through its own MCP bridge and
884
1048
  // 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;}};
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;}};
886
1050
  }
1051
+ function boot(model){loadModel(model,false);}
887
1052
  function start(){
888
1053
  // Pick language from URL/global up-front so header + errors render even if the model never loads.
889
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.10.1",
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",