forma-arch 0.11.3 → 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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Forma
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/forma-arch?label=npm)](https://www.npmjs.com/package/forma-arch)
4
+ [![npm downloads](https://img.shields.io/npm/dm/forma-arch)](https://www.npmjs.com/package/forma-arch)
5
+ [![license](https://img.shields.io/npm/l/forma-arch)](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) { derived.push({ from, to, label: c + '×', kind: 'import', estatus: 'active' }); have.add(from + '|' + to); have.add(to + '|' + from) }
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: c + '×', kind: 'import', estatus: 'active' })
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);}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forma-arch",
3
- "version": "0.11.3",
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",