forma-arch 0.5.0 → 0.6.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 +6 -1
- package/lib/cluster.mjs +5 -2
- package/lib/describe.mjs +21 -2
- package/lib/enrich.mjs +13 -4
- package/lib/gen.mjs +20 -6
- package/lib/schema/c4-model.schema.json +3 -2
- package/lib/viewer/c4-hologram.html +20 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,10 +52,15 @@ npx forma-arch <command> # or: npm i -D forma-arch
|
|
|
52
52
|
| `--enricher` | Use it when | Network |
|
|
53
53
|
|---|---|---|
|
|
54
54
|
| `agent` | **An agent is driving forma.** Writes `enrich-plan.json` with the holes; the agent writes the sentences (reading the sources if it wants) and `gen --enrich-apply <file>` applies them with the same cache and provenance. | none |
|
|
55
|
-
| `anthropic`
|
|
55
|
+
| `anthropic` | Headless / CI, with `ANTHROPIC_API_KEY`. | REST |
|
|
56
56
|
| `openai` | Same, with `OPENAI_API_KEY`. | REST |
|
|
57
57
|
| `ollama` | Sensitive repos: a local model, nothing leaves the machine. | localhost |
|
|
58
58
|
|
|
59
|
+
`--enricher` has **no default**: `forma gen --enrich` on its own fails loud and lists the four. A
|
|
60
|
+
default provider is a silent choice about your network and your API keys — and the old default
|
|
61
|
+
(`anthropic`) meant that anyone without `ANTHROPIC_API_KEY` exported got a skip line, exit 0 and the
|
|
62
|
+
same empty boxes they ran `--enrich` to fill.
|
|
63
|
+
|
|
59
64
|
## How it fits together
|
|
60
65
|
|
|
61
66
|
```
|
package/lib/cluster.mjs
CHANGED
|
@@ -13,14 +13,17 @@ export function containerOf(node, byId) {
|
|
|
13
13
|
return n ? n.id : null
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
// Group a flat container's leaves by common `
|
|
16
|
+
// Group a flat container's leaves by common `foo<sep>*` prefix, where sep is any of - _ . — the
|
|
17
|
+
// three conventions real repos use (kebab dominates JS/TS, snake Python, dot some Java/config
|
|
18
|
+
// layouts). Splitting on `_` alone gave kebab and dot repos no component level at all.
|
|
19
|
+
// Deterministic: sorted group keys.
|
|
17
20
|
// Returns { components: Node[], reparent: Map(leafId → componentId) }. Leftovers stay under the container.
|
|
18
21
|
export function componentsFor(container, leaves, opts = {}) {
|
|
19
22
|
const groupMin = opts.groupMin || 3
|
|
20
23
|
const groups = new Map()
|
|
21
24
|
for (const l of leaves) {
|
|
22
25
|
const base = String(l.name)
|
|
23
|
-
const i = base.
|
|
26
|
+
const i = base.search(/[-_.]/)
|
|
24
27
|
if (i <= 0) continue // no prefix (e.g. "health", "version") → stays flat
|
|
25
28
|
const prefix = base.slice(0, i)
|
|
26
29
|
if (!groups.has(prefix)) groups.set(prefix, [])
|
package/lib/describe.mjs
CHANGED
|
@@ -60,6 +60,19 @@ function buildArc42Index(repo, docPath) {
|
|
|
60
60
|
return idx
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// Last resort for a node that HAS children: say what it measurably holds. Never the language —
|
|
64
|
+
// a box reading "TypeScript" states the one thing the reader can already see in the header.
|
|
65
|
+
function measuredFunc(node, ctx) {
|
|
66
|
+
const kids = [...ctx.byId.values()].filter((k) => k.parent === node.id)
|
|
67
|
+
.sort((a, b) => (String(a.name) < String(b.name) ? -1 : String(a.name) > String(b.name) ? 1 : 0))
|
|
68
|
+
if (!kids.length) return null
|
|
69
|
+
const count = (k) => kids.filter((x) => x.kind === k).length
|
|
70
|
+
const parts = [['container', count('container')], ['component', count('component')], ['file', count('leaf')]]
|
|
71
|
+
.filter((p) => p[1]).map((p) => `${p[1]} ${p[0]}${p[1] === 1 ? '' : 's'}`)
|
|
72
|
+
const names = kids.slice(0, 3).map((k) => k.name).join(', ')
|
|
73
|
+
return `${parts.join(' + ')}: ${names}${kids.length > 3 ? ', …' : '.'}`.slice(0, 240)
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
export function makeDescribeCtx({ repo, byId, descriptions, docPath, containerOf }) {
|
|
64
77
|
return { repo, byId, D: descriptions || {}, containerOf, readmeCache: new Map(), arc42: buildArc42Index(repo, docPath) }
|
|
65
78
|
}
|
|
@@ -79,12 +92,18 @@ export function resolveDescription(node, ctx) {
|
|
|
79
92
|
const rd = readmeFirstPara(dirname(abs), ctx.readmeCache)
|
|
80
93
|
if (rd) return { func: rd, descSource: 'readme' }
|
|
81
94
|
}
|
|
95
|
+
// A container's evidence is a GLOB over its own directory, never a `path` — which is why the
|
|
96
|
+
// README sitting in that very directory never reached it, and the box fell through to `tech`.
|
|
97
|
+
const glb = (node.evidence || []).find((e) => e.type === 'glob')
|
|
98
|
+
if (!pth && glb) {
|
|
99
|
+
const rd = readmeFirstPara(join(ctx.repo, glb.ref), ctx.readmeCache)
|
|
100
|
+
if (rd) return { func: rd, descSource: 'readme' }
|
|
101
|
+
}
|
|
82
102
|
const a = ctx.arc42.get(norm(node.name)) || ctx.arc42.get(norm(node.id))
|
|
83
103
|
if (a) return { func: a, descSource: 'arc42' }
|
|
84
104
|
if (node.kind === 'leaf') {
|
|
85
105
|
const cn = (ctx.byId.get(cont) || {}).name || cont || node.parent
|
|
86
106
|
return { func: `Component of module ${cn}.`, descSource: 'fallback' }
|
|
87
107
|
}
|
|
88
|
-
|
|
89
|
-
return { func: node.description || '', descSource: 'fallback' }
|
|
108
|
+
return { func: node.description || measuredFunc(node, ctx) || '', descSource: 'fallback' }
|
|
90
109
|
}
|
package/lib/enrich.mjs
CHANGED
|
@@ -36,15 +36,21 @@ export function descInputHash(node, ctx) {
|
|
|
36
36
|
|
|
37
37
|
function promptFor(node, ctx, opts = {}) {
|
|
38
38
|
const { cont, siblings } = siblingsOf(node, ctx)
|
|
39
|
+
// A container is its OWN container: unguarded, the prompt tells the model that "auth" belongs to
|
|
40
|
+
// the container "auth" and calls its own children its siblings. Its filesystem pointer is a glob
|
|
41
|
+
// over its directory, not a path, so it also had nothing to read.
|
|
42
|
+
const self = cont === node.id
|
|
39
43
|
const contName = (ctx.byId && ctx.byId.get(cont) || {}).name || cont || 'the system'
|
|
40
44
|
const path = (node.evidence || []).find((e) => e.type === 'path')
|
|
45
|
+
const glob = !path && (node.evidence || []).find((e) => e.type === 'glob')
|
|
41
46
|
return [
|
|
42
|
-
`In ONE plain sentence (max 18 words), say what the software module "${node.name}" does.`,
|
|
43
|
-
`It belongs to the container "${contName}".`,
|
|
44
|
-
siblings.length ?
|
|
47
|
+
`In ONE plain sentence (max 18 words), say what the software ${self ? 'module group' : 'module'} "${node.name}" does.`,
|
|
48
|
+
self ? '' : `It belongs to the container "${contName}".`,
|
|
49
|
+
siblings.length ? `${self ? 'The modules it holds' : 'Sibling modules'}: ${siblings.slice(0, 40).join(', ')}.` : '',
|
|
45
50
|
// Only the agent enricher can act on this: a REST provider has no filesystem. That asymmetry
|
|
46
51
|
// IS the point of the agent mode — the model driving forma can read the source it describes.
|
|
47
52
|
(opts.canRead && path) ? `Read the file at ${path.ref} if you need certainty.` : '',
|
|
53
|
+
(opts.canRead && glob) ? `Read the sources under ${glob.ref}/ if you need certainty.` : '',
|
|
48
54
|
'Answer with the sentence only — no preamble, no quotes.',
|
|
49
55
|
].filter(Boolean).join('\n')
|
|
50
56
|
}
|
|
@@ -103,8 +109,11 @@ export function mergeCache(nodes, cache) {
|
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
// The holes an enricher would fill: never-described nodes, plus llm prose whose inputs moved.
|
|
112
|
+
// Containers count. They were excluded on the theory that the topology describes them — but on a
|
|
113
|
+
// repo with no curated topology (i.e. anything `forma init` seeded) they are exactly the boxes
|
|
114
|
+
// left on a measured fallback, and the biggest ones on screen.
|
|
106
115
|
export function holesIn(nodes, ctx) {
|
|
107
|
-
return nodes.filter((n) => (n.kind === 'leaf' || n.kind === 'component') &&
|
|
116
|
+
return nodes.filter((n) => (n.kind === 'leaf' || n.kind === 'component' || n.kind === 'container') &&
|
|
108
117
|
(n.descSource === 'fallback' || (n.descSource === 'llm' && n.descInputHash !== descInputHash(n, ctx))))
|
|
109
118
|
}
|
|
110
119
|
|
package/lib/gen.mjs
CHANGED
|
@@ -15,10 +15,10 @@ const arg = (f, d) => { const i = process.argv.indexOf(f); return i > -1 ? proce
|
|
|
15
15
|
const REPO = arg('--repo', process.cwd())
|
|
16
16
|
const TOPO = arg('--topology', join(REPO, 'docs/architecture/c4-topology.json'))
|
|
17
17
|
const OUT = arg('--out', join(REPO, 'docs/architecture/c4-model.json'))
|
|
18
|
-
const SCHEMA_VERSION = '1.
|
|
18
|
+
const SCHEMA_VERSION = '1.4.0' // +status2:"unknown" (additive enum value → MINOR)
|
|
19
19
|
const CLUSTER = !process.argv.includes('--no-cluster') // §2: auto-cluster flat containers; --no-cluster to disable
|
|
20
20
|
const ENRICH = process.argv.includes('--enrich') // §7: opt-in LLM prose for description holes
|
|
21
|
-
const ENRICHER = arg('--enricher',
|
|
21
|
+
const ENRICHER = arg('--enricher', null)
|
|
22
22
|
const ENRICH_MODEL = arg('--enrich-model', null)
|
|
23
23
|
const STATUS = arg('--status', join(REPO, 'docs/architecture/c4-status.json')) // curated programme state, optional
|
|
24
24
|
const STATUS_SET = process.argv.includes('--status')
|
|
@@ -31,6 +31,12 @@ const fail = (m) => { console.error('[gen-c4] FAIL: ' + m); process.exit(1) }
|
|
|
31
31
|
// (`length >= NaN` is false for every group), so a bad value fails loud instead.
|
|
32
32
|
const posInt = (flag, dflt) => { const v = Number(arg(flag, dflt)); if (!Number.isInteger(v) || v < 1) fail(`${flag}: expected a positive integer, got "${arg(flag, dflt)}"`); return v }
|
|
33
33
|
const CLUSTER_MIN = posInt('--cluster-min', 8), GROUP_MIN = posInt('--group-min', 3)
|
|
34
|
+
// §7 has no default provider ON PURPOSE. It used to be `anthropic`, i.e. the one path that needs
|
|
35
|
+
// an API key most people have not exported — so `--enrich` "worked", printed a skip line, exited 0
|
|
36
|
+
// and left every box empty: the defect this command exists to fix, reintroduced by its own default.
|
|
37
|
+
// Flipping the default to `agent` would be just as silent the other way (a CI job with a key would
|
|
38
|
+
// quietly stop filling boxes and start writing a plan file). So: choose, explicitly.
|
|
39
|
+
if (ENRICH && !ENRICHER) fail('--enrich needs an explicit --enricher: `agent` (no key — an agent writes the prose, see --enrich-apply), `anthropic`/`openai` (REST, needs the API key in the environment), `ollama` (localhost), or `echo` (offline, testing).')
|
|
34
40
|
|
|
35
41
|
function walk(spec) {
|
|
36
42
|
const dir = rp(spec.dir)
|
|
@@ -114,12 +120,16 @@ if (CLUSTER) for (const cid of [...new Set(topo.leafSources.map((s) => s.parent)
|
|
|
114
120
|
console.log(`[gen-c4] container ${cid}: clustered ${leaves.length} leaves into ${components.length} component(s)`)
|
|
115
121
|
}
|
|
116
122
|
|
|
117
|
-
// enrich: fill hologram defaults (category,
|
|
123
|
+
// enrich: fill hologram defaults (category, 6-status, completion, current/target) where absent
|
|
118
124
|
for (const n of nodes) {
|
|
119
125
|
const par = n.parent ? byId.get(n.parent) : null
|
|
120
126
|
if (!n.category) n.category = n.kind === 'leaf' ? (par && par.category) || 'leaf' : n.kind
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
// Code can prove a file EXISTS; it cannot prove the work behind it is finished. Marking every
|
|
128
|
+
// undecorated node done/100 turned a virgin repo into a board reading "10/10 complete" — the
|
|
129
|
+
// exact false green this tool exists to kill. No overlay (§WP-A1), no verdict: `unknown`, and
|
|
130
|
+
// no completion at all (a percentage nobody curated is a made-up number, including 0).
|
|
131
|
+
if (!n.status2) n.status2 = n.status === 'planned' ? 'planned' : 'unknown'
|
|
132
|
+
if (n.completion == null && n.status === 'planned') n.completion = 0
|
|
123
133
|
// `current` is left EMPTY unless curated (topology or status overlay): the viewer falls back to
|
|
124
134
|
// `func`, which since §1a carries the module's real documentation. The old "Exists: <path>"
|
|
125
135
|
// filler restated the evidence path in the one field meant for programme facts.
|
|
@@ -226,7 +236,11 @@ if (!process.argv.includes('--no-auto-edges')) {
|
|
|
226
236
|
for (const to of exposes.keys()) {
|
|
227
237
|
if (to === from || have.has(from + '|' + to)) continue
|
|
228
238
|
let c = 0
|
|
229
|
-
|
|
239
|
+
// A module name is matched VERBATIM (metacharacters escaped, never deleted: stripping them
|
|
240
|
+
// turned "session-store" into /\bsessionstore\b/, which matches nothing — every kebab-case
|
|
241
|
+
// repo, i.e. most JS/TS ones, rendered edges=0). `-` and `.` are word separators here, so
|
|
242
|
+
// the boundary is spelled out instead of using \b (which sits INSIDE "session-store").
|
|
243
|
+
for (const nm of exposes.get(to)) { if (new RegExp('(^|[^\\w-])' + nm.replace(/[.*+?^${}()|[\]\\\/-]/g, '\\$&') + '([^\\w-]|$)').test(t)) c++ }
|
|
230
244
|
if (c > 0) { derived.push({ from, to, label: c + '×', kind: 'import', estatus: 'active' }); have.add(from + '|' + to); have.add(to + '|' + from) }
|
|
231
245
|
}
|
|
232
246
|
}
|
|
@@ -179,9 +179,10 @@
|
|
|
179
179
|
"in-progress",
|
|
180
180
|
"next",
|
|
181
181
|
"planned",
|
|
182
|
-
"problem"
|
|
182
|
+
"problem",
|
|
183
|
+
"unknown"
|
|
183
184
|
],
|
|
184
|
-
"description": "Hologram completion status (colour)."
|
|
185
|
+
"description": "Hologram completion status (colour). `unknown` is the default for a node no curated overlay has ruled on — code proves existence, never completion."
|
|
185
186
|
},
|
|
186
187
|
"completion": {
|
|
187
188
|
"type": "integer",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
--bg:#050d14; --grid:rgba(62,231,255,.04); --halo:rgba(62,231,255,.10);
|
|
10
10
|
--cyan:#3ee7ff; --sec:#6fa8bc; --ter:#4c7f94; --ter2:#41738a; --nm:#bfe9f7;
|
|
11
11
|
--stagebg:rgba(4,12,19,.72); --stageborder:#143547; --nodebg:rgba(8,26,38,.92); --detailbg:rgba(6,20,30,.97);
|
|
12
|
-
--border:#1e4a5e; --done:#2fe08a; --prog:#ffc74d; --next:#3ee7ff; --plan:#2d5a72; --planTxt:#7fb3c6; --prob:#ff5d5d;
|
|
12
|
+
--border:#1e4a5e; --done:#2fe08a; --prog:#ffc74d; --next:#3ee7ff; --plan:#2d5a72; --planTxt:#7fb3c6; --prob:#ff5d5d; --unk:#7d8f99;
|
|
13
13
|
--edge:#2b6e8a; --edgeplan:#264d61; --elbl:#3d7f9c; --ddtxt:#7fb3c6; --now:#cfeefb; --tgt:#8fd8ec; --src:#4fbf8d;}
|
|
14
14
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
15
15
|
body{font-family:"SF Mono",Menlo,Consolas,ui-monospace,monospace;background:var(--bg);color:var(--nm);overflow-x:hidden}
|
|
@@ -60,6 +60,8 @@ svg{width:100%;height:100%;display:block}
|
|
|
60
60
|
.s-plan .tt,.s-plan .pp{fill:var(--planTxt)}
|
|
61
61
|
.s-prob rect{stroke:var(--prob);filter:drop-shadow(0 0 8px rgba(255,93,93,.6))}
|
|
62
62
|
.s-prob .tt,.s-prob .pp{fill:var(--prob)}
|
|
63
|
+
.s-unk rect{stroke:var(--unk);stroke-dasharray:1 3}
|
|
64
|
+
.s-unk .tt,.s-unk .pp{fill:var(--unk)}
|
|
63
65
|
.mode-t .nd rect{stroke:var(--next)!important;stroke-dasharray:none!important;filter:drop-shadow(0 0 8px rgba(62,231,255,.5))!important}
|
|
64
66
|
.mode-t .nd .tt,.mode-t .nd .pp{fill:var(--next)!important}
|
|
65
67
|
.edge{stroke:var(--edge);stroke-width:1.3;fill:none;marker-end:url(#ar);cursor:help}
|
|
@@ -119,7 +121,7 @@ svg{width:100%;height:100%;display:block}
|
|
|
119
121
|
html[data-skin="blueprint"]{--bg:#eef3f9;--grid:rgba(31,119,180,.10);--halo:rgba(31,119,180,.08);
|
|
120
122
|
--cyan:#1f77b4;--sec:#5a6b7d;--ter:#6b7b8c;--ter2:#5a6b7d;--nm:#16273a;
|
|
121
123
|
--stagebg:#ffffff;--stageborder:rgba(31,119,180,.25);--nodebg:#ffffff;--detailbg:#ffffff;--border:rgba(31,119,180,.35);
|
|
122
|
-
--done:#1f9d57;--prog:#c07a12;--next:#1f77b4;--plan:#8592a3;--planTxt:#5a6b7d;--prob:#d1453b;
|
|
124
|
+
--done:#1f9d57;--prog:#c07a12;--next:#1f77b4;--plan:#8592a3;--planTxt:#5a6b7d;--prob:#d1453b;--unk:#8a97a3;
|
|
123
125
|
--edge:#8aa0b4;--edgeplan:#b9c6d3;--elbl:#5a6b7d;--ddtxt:#40536a;--now:#16273a;--tgt:#1f5f8a;--src:#1f9d57}
|
|
124
126
|
html[data-skin="blueprint"] h1{text-shadow:none}
|
|
125
127
|
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}
|
|
@@ -160,6 +162,7 @@ html[data-skin="blueprint"] button.on{text-shadow:none}
|
|
|
160
162
|
<span><i style="background:#3ee7ff"></i><span id="lg-next"></span></span>
|
|
161
163
|
<span><i style="background:#2d5a72"></i><span id="lg-plan"></span></span>
|
|
162
164
|
<span><i style="background:#ff5d5d"></i><span id="lg-prob"></span></span>
|
|
165
|
+
<span><i style="background:#7d8f99"></i><span id="lg-unk"></span></span>
|
|
163
166
|
<span class="hint" id="lg-hint"></span>
|
|
164
167
|
</div>
|
|
165
168
|
<div id="detail"></div>
|
|
@@ -176,6 +179,7 @@ var STRINGS={
|
|
|
176
179
|
resetLayout:"RESET LAYOUT", resetLayoutTitle:"re-arrange",
|
|
177
180
|
printExport:"PRINT / EXPORT", exportSvg:"Export SVG", exportPng:"Export PNG", exportLayout:"Export layout JSON", print:"Print",
|
|
178
181
|
legDone:"DONE (proven)", legProg:"IN PROGRESS", legNext:"NEXT", legPlan:"PLAN ONLY", legProb:"PROBLEM",
|
|
182
|
+
legUnk:"UNKNOWN (no curated state)", stUnk:"?",
|
|
179
183
|
legHint:"— animated arrow = active flow · dashed = to build",
|
|
180
184
|
crumbRoot:"Context", crumbLevel:"C4", lvlContext:"CONTEXT",
|
|
181
185
|
lvlNames:{context:"Context",container:"Container",component:"Components",leaf:"Leaves"},
|
|
@@ -195,6 +199,7 @@ var STRINGS={
|
|
|
195
199
|
resetLayout:"RESET LAYOUT", resetLayoutTitle:"ridisponi",
|
|
196
200
|
printExport:"STAMPA / ESPORTA", exportSvg:"Esporta SVG", exportPng:"Esporta PNG", exportLayout:"Esporta layout JSON", print:"Stampa",
|
|
197
201
|
legDone:"FATTO (provato)", legProg:"IN CORSO", legNext:"PROSSIMO", legPlan:"SOLO PROGETTO", legProb:"PROBLEMA",
|
|
202
|
+
legUnk:"IGNOTO (stato non curato)", stUnk:"?",
|
|
198
203
|
legHint:"— freccia animata = flusso attivo · tratteggio = da costruire",
|
|
199
204
|
crumbRoot:"Contesto", crumbLevel:"C4", lvlContext:"CONTESTO",
|
|
200
205
|
lvlNames:{context:"Contesto",container:"Container",component:"Componenti",leaf:"Foglie"},
|
|
@@ -219,7 +224,7 @@ var CATALOG_THRESHOLD=24; // group of same-category siblings above this collapse
|
|
|
219
224
|
var LABEL_AUTO_MAX=14; // above this many arrows in the current level, fixed labels would knot the view
|
|
220
225
|
var LABELS=null; // null = auto (by arrow count); true/false = user override for this session
|
|
221
226
|
var SKINS=[["holo","◆ holo"],["blueprint","▭ blueprint"]], SKIN_IDS=["holo","blueprint"];
|
|
222
|
-
var STMAP={done:"done","in-progress":"prog",next:"next",planned:"plan",problem:"prob"};
|
|
227
|
+
var STMAP={done:"done","in-progress":"prog",next:"next",planned:"plan",problem:"prob",unknown:"unk"};
|
|
223
228
|
var M=null, stack=[null], mode="now", POS={}, drag=null, justDragged=false, CATNODES={}, FETCHED=false
|
|
224
229
|
var VBOX={}; // last fitted viewBox per level — frozen while dragging so the canvas does not rescale
|
|
225
230
|
function $(i){return document.getElementById(i)}
|
|
@@ -258,9 +263,9 @@ function collapseCatalogs(pid,kids){
|
|
|
258
263
|
}
|
|
259
264
|
function makeCatalogNode(pid,g){
|
|
260
265
|
// Aggregate status: worst-of-a-few heuristic — problem > next > in-progress > planned > done.
|
|
261
|
-
var rank={problem:5,next:4,"in-progress":3,planned:2,done:1},best=0,bestKey="
|
|
266
|
+
var rank={problem:5,next:4,"in-progress":3,planned:2,unknown:1.5,done:1},best=0,bestKey="unknown",done=0,tot=g.members.length;
|
|
262
267
|
for(var i=0;i<g.members.length;i++){
|
|
263
|
-
var s2=g.members[i].status2||(g.members[i].status==="planned"?"planned":"
|
|
268
|
+
var s2=g.members[i].status2||(g.members[i].status==="planned"?"planned":"unknown");
|
|
264
269
|
if((rank[s2]||0)>best){best=rank[s2]||0;bestKey=s2;}
|
|
265
270
|
if(s2==="done")done++;
|
|
266
271
|
}
|
|
@@ -371,11 +376,11 @@ function draw(animate){
|
|
|
371
376
|
}
|
|
372
377
|
for(var j=0;j<kids.length;j++){
|
|
373
378
|
var n=kids[j],p=pos[n.id];
|
|
374
|
-
var st=STMAP[n.status2]||(n.status==="planned"?"plan":"
|
|
379
|
+
var st=STMAP[n.status2]||(n.status==="planned"?"plan":"unk");
|
|
375
380
|
var pulse=(st==="next"||st==="prog")&&mode!=="target"&&!n.__catalog;
|
|
376
381
|
var isCat=!!n.__catalog,dr=!isCat&&hasKids(n.id),cls="nd s-"+st+(dr?" drill":"")+(isCat?" cat":"")+(pulse?" pulse":"");
|
|
377
|
-
var desc=isCat?(n.count+" "+STR.rosterCount):(mode==="target"?(n.target||n.func||""):(n.current||n.func||
|
|
378
|
-
var badge=isCat?String(n.count):(mode==="target"?STR.detTarget:(n.statusWord||(n.completion!=null?n.completion+"%":
|
|
382
|
+
var desc=isCat?(n.count+" "+STR.rosterCount):(mode==="target"?(n.target||n.func||""):(n.current||n.func||""));
|
|
383
|
+
var badge=isCat?String(n.count):(mode==="target"?STR.detTarget:(n.statusWord||(n.completion!=null?n.completion+"%":STR.stUnk)));
|
|
379
384
|
// the title runs until the badge, so reserve the badge's actual width (9px text ≈ 5.4px/char)
|
|
380
385
|
// instead of a flat 70px — a fixed reserve clipped names that had all the room they needed
|
|
381
386
|
var tmax=Math.floor((p.w-34-String(badge).length*5.4)/6.7),tt=n.name;
|
|
@@ -414,12 +419,12 @@ function draw(animate){
|
|
|
414
419
|
// its members), so the tally never changes when the catalogue threshold does.
|
|
415
420
|
var cnt={},tot=0,sum=0,nc=0,st3;
|
|
416
421
|
for(j=0;j<realKids.length;j++){
|
|
417
|
-
st3=STMAP[realKids[j].status2]||(realKids[j].status==="planned"?"plan":"
|
|
422
|
+
st3=STMAP[realKids[j].status2]||(realKids[j].status==="planned"?"plan":"unk");
|
|
418
423
|
cnt[st3]=(cnt[st3]||0)+1;tot++;
|
|
419
424
|
if(realKids[j].completion!=null){sum+=(+realKids[j].completion||0);nc++;}
|
|
420
425
|
}
|
|
421
426
|
if(tot){
|
|
422
|
-
var DOT={done:"#2fe08a",prog:"#ffc74d",next:"#3ee7ff",plan:"#2d5a72",prob:"#ff5d5d"},ord=["done","prog","next","plan","prob"],agg="";
|
|
427
|
+
var DOT={done:"#2fe08a",prog:"#ffc74d",next:"#3ee7ff",plan:"#2d5a72",prob:"#ff5d5d",unk:"#7d8f99"},ord=["done","prog","next","plan","prob","unk"],agg="";
|
|
423
428
|
for(j=0;j<ord.length;j++)if(cnt[ord[j]])agg+='<i style="background:'+DOT[ord[j]]+'"></i>'+cnt[ord[j]];
|
|
424
429
|
if(nc)agg+=' '+esc(STR.aggProgress)+' <b>'+Math.round(sum/nc)+'%</b>';
|
|
425
430
|
cb='<span class="agg">'+agg+'</span>'+cb;
|
|
@@ -461,7 +466,7 @@ function cssv(v){try{return getComputedStyle(document.documentElement).getProper
|
|
|
461
466
|
|
|
462
467
|
function showDetail(n){
|
|
463
468
|
if(n&&n.__catalog)return showRoster(n);
|
|
464
|
-
var src=srcOf(n),cur=n.current||n.func||
|
|
469
|
+
var src=srcOf(n),cur=n.current||n.func||"",showSrc=src&&String(cur).indexOf(src)<0;
|
|
465
470
|
$("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>'
|
|
466
471
|
+(n.func?'<div class="row"><span class="lbl">'+esc(STR.detFunc)+'</span><span class="func">'+esc(n.func)+'</span></div>':'')
|
|
467
472
|
+(cur&&cur!==n.func?'<div class="row"><span class="lbl">'+esc(STR.detNow)+'</span><span class="now">'+esc(cur)+'</span></div>':'')
|
|
@@ -482,7 +487,7 @@ function showRoster(cat){
|
|
|
482
487
|
$("detail").innerHTML=head;$("detail").style.display="block";
|
|
483
488
|
var dc=$("dc");if(dc)dc.addEventListener("click",function(){$("detail").style.display="none";});
|
|
484
489
|
function rowHtml(m){
|
|
485
|
-
var d=m.current||m.func||
|
|
490
|
+
var d=m.current||m.func||"";
|
|
486
491
|
var pct=m.completion!=null?(m.completion+"%"):(m.statusWord||"");
|
|
487
492
|
return '<span class="ritem">'+(pct?'<span class="rp">'+esc(pct)+'</span>':'')
|
|
488
493
|
+'<span class="rn">'+esc(m.name)+'</span>'
|
|
@@ -587,7 +592,7 @@ function applyStrings(){
|
|
|
587
592
|
set("pexp",STR.printExport);set("pexpSvg",STR.exportSvg);set("pexpPng",STR.exportPng);
|
|
588
593
|
set("pexpLayout",STR.exportLayout);set("pexpPrint",STR.print);
|
|
589
594
|
set("lg-done",STR.legDone);set("lg-prog",STR.legProg);set("lg-next",STR.legNext);
|
|
590
|
-
set("lg-plan",STR.legPlan);set("lg-prob",STR.legProb);set("lg-hint",STR.legHint);
|
|
595
|
+
set("lg-plan",STR.legPlan);set("lg-prob",STR.legProb);set("lg-unk",STR.legUnk);set("lg-hint",STR.legHint);
|
|
591
596
|
}
|
|
592
597
|
// ---- FEATURE #2: PRINT / EXPORT (all client-side, offline) ----
|
|
593
598
|
function exportBaseName(){
|
|
@@ -604,7 +609,7 @@ function collectSvgCss(){
|
|
|
604
609
|
if(!rules)continue;
|
|
605
610
|
for(j=0;j<rules.length;j++){var r=rules[j],t=r.cssText||"";
|
|
606
611
|
// keep rules that target svg-internal classes/elements + the color vars on :root/html
|
|
607
|
-
if(/\.(nd|edge|edgehit|elbl|s-done|s-prog|s-next|s-plan|s-prob|mode-t|tt|kk|dd|pp|plus|cat)\b/.test(t)
|
|
612
|
+
if(/\.(nd|edge|edgehit|elbl|s-done|s-prog|s-next|s-plan|s-prob|s-unk|mode-t|tt|kk|dd|pp|plus|cat)\b/.test(t)
|
|
608
613
|
||/(^|[\s,])svg\b/.test(t)||/:root|html\[data-skin/.test(t)){
|
|
609
614
|
// drop animations for a static export (keepdefs stable)
|
|
610
615
|
out.push(t);
|
|
@@ -617,7 +622,7 @@ function collectSvgCss(){
|
|
|
617
622
|
// with the on-screen palette. Returns a "--k:v;--k:v" inline-style string.
|
|
618
623
|
var SVG_VARS=["--bg","--grid","--halo","--cyan","--sec","--ter","--ter2","--nm",
|
|
619
624
|
"--stagebg","--stageborder","--nodebg","--detailbg","--border","--done","--prog",
|
|
620
|
-
"--next","--plan","--planTxt","--prob","--edge","--edgeplan","--elbl","--ddtxt",
|
|
625
|
+
"--next","--plan","--planTxt","--prob","--unk","--edge","--edgeplan","--elbl","--ddtxt",
|
|
621
626
|
"--now","--tgt","--src"];
|
|
622
627
|
function resolvedVars(){
|
|
623
628
|
var cs;try{cs=getComputedStyle(document.documentElement);}catch(e){return "";}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forma-arch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|