forma-arch 0.11.2 → 0.12.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 +4 -0
- package/lib/gen.mjs +20 -1
- package/lib/lang.mjs +1 -1
- package/lib/schema/c4-model.schema.json +8 -2
- package/lib/viewer/c4-hologram.html +12 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Forma
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/forma-arch)
|
|
4
|
+
[](https://www.npmjs.com/package/forma-arch)
|
|
5
|
+
[](https://www.npmjs.com/package/forma-arch)
|
|
6
|
+
|
|
3
7
|
**Present your architecture instead of slides.** Forma turns any codebase into an interactive,
|
|
4
8
|
stack-agnostic C4 explorer — big-picture → drill-down to the leaf — generated *from the code* and
|
|
5
9
|
kept true to it by a deterministic drift check. No more slide decks that lie the day after you draw
|
package/lib/gen.mjs
CHANGED
|
@@ -338,6 +338,19 @@ if (!process.argv.includes('--no-auto-edges') && isGo(topo.meta && topo.meta.sta
|
|
|
338
338
|
try { for (const f of readdirSync(dir)) { const fp = join(dir, f); if (statSync(fp).isFile()) t += '\n' + readFileSync(fp, 'utf-8') } } catch {}
|
|
339
339
|
text.set(s.parent, t)
|
|
340
340
|
}
|
|
341
|
+
// ponytail: per-line kind is a heuristic (import > drives > reads > references); the count `c` below
|
|
342
|
+
// is untouched, and curated edges always win where precision matters. The verb is what an executive
|
|
343
|
+
// reads; the count lives in `weight` so the viewer can roll it up without parsing the label.
|
|
344
|
+
const VERB = [[/^\s*(?:import|export)\b[\s\S]*?\bfrom\b|^\s*const\s+\S+\s*=\s*require\s*\(/, 'imports'],
|
|
345
|
+
[/\b(?:spawn|exec)(?:Sync|File)?\s*\(/, 'drives'],
|
|
346
|
+
[/\breadFileSync\s*\(|\breaddirSync\s*\(|\breadJson\b/, 'reads']]
|
|
347
|
+
const RANK = { imports: 3, drives: 2, reads: 1, references: 0 }
|
|
348
|
+
const classify = (line) => { for (const [re, v] of VERB) if (re.test(line)) return v; return 'references' }
|
|
349
|
+
const exposesRe = new Map()
|
|
350
|
+
for (const to of exposes.keys()) {
|
|
351
|
+
const alts = exposes.get(to).map((nm) => nm.replace(/[.*+?^${}()|[\]\\\/-]/g, '\\$&')).join('|')
|
|
352
|
+
exposesRe.set(to, alts ? new RegExp('(^|[^\\w-])(?:' + alts + ')([^\\w-]|$)') : null)
|
|
353
|
+
}
|
|
341
354
|
const have = new Set((topo.edges || []).flatMap((e) => [e.from + '|' + e.to, e.to + '|' + e.from]))
|
|
342
355
|
const derived = []
|
|
343
356
|
for (const from of exposes.keys()) {
|
|
@@ -350,7 +363,13 @@ if (!process.argv.includes('--no-auto-edges') && isGo(topo.meta && topo.meta.sta
|
|
|
350
363
|
// repo, i.e. most JS/TS ones, rendered edges=0). `-` and `.` are word separators here, so
|
|
351
364
|
// the boundary is spelled out instead of using \b (which sits INSIDE "session-store").
|
|
352
365
|
for (const nm of exposes.get(to)) { if (new RegExp('(^|[^\\w-])' + nm.replace(/[.*+?^${}()|[\]\\\/-]/g, '\\$&') + '([^\\w-]|$)').test(t)) c++ }
|
|
353
|
-
if (c > 0) {
|
|
366
|
+
if (c > 0) {
|
|
367
|
+
let verb = 'references'
|
|
368
|
+
const re = exposesRe.get(to)
|
|
369
|
+
if (re) for (const line of t.split('\n')) { if (re.test(line)) { const v = classify(line); if (RANK[v] > RANK[verb]) verb = v } }
|
|
370
|
+
derived.push({ from, to, label: verb, weight: c, kind: 'import', estatus: 'inferred' })
|
|
371
|
+
have.add(from + '|' + to); have.add(to + '|' + from)
|
|
372
|
+
}
|
|
354
373
|
}
|
|
355
374
|
}
|
|
356
375
|
topo.edges = [...(topo.edges || []), ...derived]
|
package/lib/lang.mjs
CHANGED
|
@@ -96,7 +96,7 @@ export function goEdges({ repo, nodes, edges }) {
|
|
|
96
96
|
const k = p.container + '|' + to
|
|
97
97
|
if (have.has(k)) continue
|
|
98
98
|
have.add(k)
|
|
99
|
-
out.push({ from: p.container, to, label:
|
|
99
|
+
out.push({ from: p.container, to, label: 'imports', weight: c, kind: 'import', estatus: 'inferred' })
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
return out
|
|
@@ -571,6 +571,11 @@
|
|
|
571
571
|
"level": {
|
|
572
572
|
"type": "string"
|
|
573
573
|
},
|
|
574
|
+
"weight": {
|
|
575
|
+
"type": "integer",
|
|
576
|
+
"minimum": 0,
|
|
577
|
+
"description": "Reference count for a derived edge; the viewer sums it when rolling up. Optional; falls back to parsing the label."
|
|
578
|
+
},
|
|
574
579
|
"kind": {
|
|
575
580
|
"type": "string",
|
|
576
581
|
"description": "import | runtime | ci \u2014 provenance of the relationship."
|
|
@@ -605,9 +610,10 @@
|
|
|
605
610
|
"type": "string",
|
|
606
611
|
"enum": [
|
|
607
612
|
"active",
|
|
608
|
-
"to-build"
|
|
613
|
+
"to-build",
|
|
614
|
+
"inferred"
|
|
609
615
|
],
|
|
610
|
-
"description": "active = animated flow; to-build = dashed."
|
|
616
|
+
"description": "active = animated flow (curated); to-build = dashed; inferred = derived from code references (heuristic, drawn softer)."
|
|
611
617
|
}
|
|
612
618
|
}
|
|
613
619
|
}
|
|
@@ -90,6 +90,7 @@ svg{width:100%;height:100%;display:block}
|
|
|
90
90
|
.mode-t .nd .tt,.mode-t .nd .pp{fill:var(--next)!important}
|
|
91
91
|
.edge{stroke:var(--edge);stroke-width:1.3;fill:none;marker-end:url(#ar);cursor:help}
|
|
92
92
|
.edge.flow{stroke-dasharray:5 7;animation:dash 1.1s linear infinite}
|
|
93
|
+
.edge.inferred{stroke-dasharray:1 3;opacity:.5}
|
|
93
94
|
.edge.plan{stroke:var(--edgeplan);stroke-dasharray:3 6;opacity:.6;marker-end:url(#arp)}
|
|
94
95
|
.edge.hot{stroke:var(--cyan);stroke-width:2.6;opacity:1}
|
|
95
96
|
.edge.delta{stroke:var(--cyan);stroke-width:2.5;opacity:1;filter:drop-shadow(0 0 4px rgba(62,231,255,.7))}
|
|
@@ -224,7 +225,7 @@ var STRINGS={
|
|
|
224
225
|
legUnk:"UNKNOWN (no curated state)", stUnk:"?",
|
|
225
226
|
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",
|
|
226
227
|
coverPart:"{n} of {t} units named by the document",
|
|
227
|
-
legHint:"— animated arrow = active flow · dashed = to build", legCheckpoint:"CHANGE IN THIS CHECKPOINT",
|
|
228
|
+
legHint:"— animated arrow = active flow · dotted = inferred · dashed = to build", legCheckpoint:"CHANGE IN THIS CHECKPOINT",
|
|
228
229
|
timelineLabel:"Architecture checkpoints", asIs:"AS-IS", asIsBadge:"verified code", cumulativeHint:"the graph is always cumulative", noArchChanges:"no architecture changes",
|
|
229
230
|
deltaAdd:"ADD", deltaUpdate:"UPDATE", deltaRewire:"REWIRE", deltaRemove:"REMOVE",
|
|
230
231
|
crumbRoot:"Context", crumbLevel:"C4", lvlContext:"CONTEXT",
|
|
@@ -248,7 +249,7 @@ var STRINGS={
|
|
|
248
249
|
legUnk:"IGNOTO (stato non curato)", stUnk:"?",
|
|
249
250
|
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",
|
|
250
251
|
coverPart:"{n} unità su {t} nominate dal documento",
|
|
251
|
-
legHint:"— freccia animata = flusso attivo · tratteggio = da costruire", legCheckpoint:"CAMBIO IN QUESTO CHECKPOINT",
|
|
252
|
+
legHint:"— freccia animata = flusso attivo · puntini = derivato · tratteggio = da costruire", legCheckpoint:"CAMBIO IN QUESTO CHECKPOINT",
|
|
252
253
|
timelineLabel:"Checkpoint architetturali", asIs:"AS-IS", asIsBadge:"codice verificato", cumulativeHint:"il grafo è sempre cumulativo", noArchChanges:"nessun cambio architetturale",
|
|
253
254
|
deltaAdd:"ADD", deltaUpdate:"UPDATE", deltaRewire:"REWIRE", deltaRemove:"REMOVE",
|
|
254
255
|
crumbRoot:"Contesto", crumbLevel:"C4", lvlContext:"CONTESTO",
|
|
@@ -608,7 +609,7 @@ function draw(animate){
|
|
|
608
609
|
+'<defs><marker id="ar" markerWidth="9" markerHeight="7" refX="9" refY="3.5" orient="auto"><polygon points="0 0,9 3.5,0 7" fill="'+cssv("--edge")+'"/></marker>'
|
|
609
610
|
+'<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>';
|
|
610
611
|
for(j=0;j<geo.length;j++){
|
|
611
|
-
var e=geo[j].g,ed=geo[j].e,cl=(ed.estatus==="to-build"?"plan":"flow");
|
|
612
|
+
var e=geo[j].g,ed=geo[j].e,cl=(ed.estatus==="to-build"?"plan":ed.estatus==="inferred"?"inferred":"flow");
|
|
612
613
|
s+='<path class="edge '+cl+(ed.delta?" delta":"")+'" data-e="'+geo[j].i+'" d="'+e.d+'"/>';
|
|
613
614
|
s+='<path class="edgehit" data-e="'+geo[j].i+'" data-label="'+esc(ed.label||"")+'" d="'+e.d+'"/>';
|
|
614
615
|
if(showLbl&&ed.label){var lb=String(ed.label);if(lb.length>28)lb=lb.slice(0,27)+"…";
|
|
@@ -740,7 +741,7 @@ function rollEdges(edges,vis,parent){
|
|
|
740
741
|
f=null;cur=e.from;guard=0;while(cur!=null&&guard++<64){if(vis[cur]){f=vis[cur];break;}cur=parent[cur];}
|
|
741
742
|
t=null;cur=e.to;guard=0;while(cur!=null&&guard++<64){if(vis[cur]){t=vis[cur];break;}cur=parent[cur];}
|
|
742
743
|
if(!f||!t||f===t)continue;
|
|
743
|
-
c=parseInt(String(e.label),10);if(!(c>0))c=1;
|
|
744
|
+
c=(e.weight>0)?e.weight:parseInt(String(e.label),10);if(!(c>0))c=1;
|
|
744
745
|
k=f+"|"+t;
|
|
745
746
|
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
747
|
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);}
|
|
@@ -763,11 +764,16 @@ function edgePath(a,b){
|
|
|
763
764
|
}
|
|
764
765
|
function levelName(l){return (STR.lvlNames&&STR.lvlNames[l])||l||"—";}
|
|
765
766
|
function cssv(v){try{return getComputedStyle(document.documentElement).getPropertyValue(v).trim()||"#2b6e8a";}catch(e){return "#2b6e8a";}}
|
|
767
|
+
function detailState(n,timeline){
|
|
768
|
+
// Function prose says what a box is. It is not also a checkpoint state: unchanged timeline nodes
|
|
769
|
+
// without curated `current` used to repeat the same sentence under both labels.
|
|
770
|
+
return timeline?(n.current||""):(n.current||n.func||n.description||"");
|
|
771
|
+
}
|
|
766
772
|
|
|
767
773
|
function showDetail(n){
|
|
768
774
|
if(n&&n.__catalog)return showRoster(n);
|
|
769
|
-
var src=srcOf(n),cur=n
|
|
770
|
-
var
|
|
775
|
+
var timeline=!!(BASE&&BASE.timeline),src=srcOf(n),cur=detailState(n,timeline),showSrc=src&&String(cur).indexOf(src)<0;
|
|
776
|
+
var change=timeline?nodeChange(n.id):null;
|
|
771
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>'
|
|
772
778
|
+(n.func?'<div class="row"><span class="lbl">'+esc(STR.detFunc)+'</span><span class="func">'+esc(n.func)+'</span></div>':'')
|
|
773
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>':'')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forma-arch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|