@diagc/core 0.1.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.
Files changed (59) hide show
  1. package/LICENSE +709 -0
  2. package/README.md +27 -0
  3. package/dist/builder.d.ts +381 -0
  4. package/dist/builder.js +590 -0
  5. package/dist/children.d.ts +21 -0
  6. package/dist/children.js +45 -0
  7. package/dist/commands.d.ts +219 -0
  8. package/dist/commands.js +474 -0
  9. package/dist/compose.d.ts +19 -0
  10. package/dist/compose.js +246 -0
  11. package/dist/drawings.d.ts +13 -0
  12. package/dist/drawings.js +36 -0
  13. package/dist/eject.d.ts +20 -0
  14. package/dist/eject.js +260 -0
  15. package/dist/fishbone.d.ts +66 -0
  16. package/dist/fishbone.js +95 -0
  17. package/dist/git.d.ts +65 -0
  18. package/dist/git.js +159 -0
  19. package/dist/guards.d.ts +10 -0
  20. package/dist/guards.js +98 -0
  21. package/dist/index.d.ts +25 -0
  22. package/dist/index.js +45 -0
  23. package/dist/labels.d.ts +5 -0
  24. package/dist/labels.js +10 -0
  25. package/dist/layout-defaults.d.ts +20 -0
  26. package/dist/layout-defaults.js +20 -0
  27. package/dist/mutate.d.ts +106 -0
  28. package/dist/mutate.js +547 -0
  29. package/dist/second-order.d.ts +39 -0
  30. package/dist/second-order.js +86 -0
  31. package/dist/text.d.ts +5 -0
  32. package/dist/text.js +25 -0
  33. package/dist/threat-model.d.ts +88 -0
  34. package/dist/threat-model.js +188 -0
  35. package/dist/types.d.ts +395 -0
  36. package/dist/types.js +27 -0
  37. package/dist/util.d.ts +11 -0
  38. package/dist/util.js +13 -0
  39. package/dist/validate.d.ts +19 -0
  40. package/dist/validate.js +736 -0
  41. package/dist/view/compile.d.ts +29 -0
  42. package/dist/view/compile.js +78 -0
  43. package/dist/view/edges.d.ts +4 -0
  44. package/dist/view/edges.js +118 -0
  45. package/dist/view/hierarchy.d.ts +41 -0
  46. package/dist/view/hierarchy.js +103 -0
  47. package/dist/view/layers.d.ts +7 -0
  48. package/dist/view/layers.js +17 -0
  49. package/dist/view/lod.d.ts +15 -0
  50. package/dist/view/lod.js +17 -0
  51. package/dist/view/scope.d.ts +23 -0
  52. package/dist/view/scope.js +106 -0
  53. package/dist/view/size.d.ts +8 -0
  54. package/dist/view/size.js +34 -0
  55. package/dist/view/tree.d.ts +12 -0
  56. package/dist/view/tree.js +147 -0
  57. package/dist/view/types.d.ts +68 -0
  58. package/dist/view/types.js +1 -0
  59. package/package.json +38 -0
@@ -0,0 +1,147 @@
1
+ export function buildViewTree(m, h, lod) {
2
+ const stateOf = (id) => (h.childrenOf.get(id) ?? []).length === 0 ? 'leaf' : (lod[id] ?? 'collapsed');
3
+ const visible = new Set();
4
+ const hostOf = new Map();
5
+ const promoted = new Set();
6
+ for (const r of h.roots) {
7
+ visible.add(r);
8
+ hostOf.set(r, null);
9
+ }
10
+ const absorbersOf = (id, memo) => {
11
+ const hit = memo.get(id);
12
+ if (hit)
13
+ return hit;
14
+ const acc = new Set();
15
+ memo.set(id, acc); // pre-set guards diamond re-entry (containment is a DAG)
16
+ for (const p of h.parentsOf.get(id) ?? []) {
17
+ if (visible.has(p))
18
+ acc.add(p);
19
+ else
20
+ for (const a of absorbersOf(p, memo))
21
+ acc.add(a);
22
+ }
23
+ return acc;
24
+ };
25
+ const commonHost = (absorbers) => {
26
+ const chains = [...absorbers].map((a) => {
27
+ const chain = [];
28
+ let cur = hostOf.get(a) ?? null;
29
+ while (cur !== null) {
30
+ chain.push(cur);
31
+ cur = hostOf.get(cur) ?? null;
32
+ }
33
+ return chain;
34
+ });
35
+ const [first, ...rest] = chains;
36
+ for (const cand of first ?? []) {
37
+ if (rest.every((c) => c.includes(cand)))
38
+ return cand;
39
+ }
40
+ return null;
41
+ };
42
+ // Reveal children of visible expanded nodes; then promote hidden shared
43
+ // nodes spanning >=2 visible groups; repeat until stable. Within a round,
44
+ // only "membership-maximal" candidates promote: a candidate with a hidden
45
+ // ancestor that is itself a candidate waits — once the ancestor is visible,
46
+ // the descendant usually resolves to a single absorber (it lives inside the
47
+ // promoted container) instead of being wrongly promoted next to it.
48
+ for (;;) {
49
+ let placed = true;
50
+ while (placed) {
51
+ placed = false;
52
+ for (const n of m.nodes) {
53
+ if (visible.has(n.id))
54
+ continue;
55
+ const host = (h.parentsOf.get(n.id) ?? []).find((p) => visible.has(p) && stateOf(p) === 'expanded');
56
+ if (host !== undefined) {
57
+ visible.add(n.id);
58
+ hostOf.set(n.id, host);
59
+ placed = true;
60
+ }
61
+ }
62
+ }
63
+ const memo = new Map();
64
+ const candidates = m.nodes.filter((n) => !visible.has(n.id) && absorbersOf(n.id, memo).size >= 2);
65
+ if (candidates.length === 0)
66
+ break;
67
+ const candidateIds = new Set(candidates.map((n) => n.id));
68
+ const hasCandidateAncestor = (id) => {
69
+ const seen = new Set();
70
+ const stack = [...(h.parentsOf.get(id) ?? [])];
71
+ while (stack.length > 0) {
72
+ const p = stack.pop();
73
+ if (p === undefined || seen.has(p))
74
+ continue;
75
+ seen.add(p);
76
+ if (candidateIds.has(p))
77
+ return true;
78
+ if (!visible.has(p))
79
+ stack.push(...(h.parentsOf.get(p) ?? []));
80
+ }
81
+ return false;
82
+ };
83
+ // A DAG always has a maximal candidate, so this round promotes >=1 node.
84
+ for (const n of candidates.filter((c) => !hasCandidateAncestor(c.id))) {
85
+ promoted.add(n.id);
86
+ visible.add(n.id);
87
+ hostOf.set(n.id, commonHost(absorbersOf(n.id, memo)));
88
+ }
89
+ }
90
+ // Anchors for hidden nodes (unique by construction: >=2 would have promoted).
91
+ const anchorOf = new Map();
92
+ const finalMemo = new Map();
93
+ for (const n of m.nodes) {
94
+ if (visible.has(n.id))
95
+ continue;
96
+ const [anchor] = absorbersOf(n.id, finalMemo);
97
+ if (anchor !== undefined)
98
+ anchorOf.set(n.id, anchor);
99
+ }
100
+ // Assemble ViewNodes in model order.
101
+ const byId = new Map();
102
+ for (const n of m.nodes) {
103
+ if (!visible.has(n.id))
104
+ continue;
105
+ byId.set(n.id, {
106
+ id: n.id,
107
+ node: n,
108
+ state: stateOf(n.id),
109
+ children: [],
110
+ promoted: promoted.has(n.id),
111
+ sharedMembers: [],
112
+ });
113
+ }
114
+ // Two passes so promoted nodes land AFTER normally-placed siblings within
115
+ // each roots/children list, each group internally in model order.
116
+ const roots = [];
117
+ const placeInto = (vn) => {
118
+ const host = hostOf.get(vn.id) ?? null;
119
+ if (host === null)
120
+ roots.push(vn);
121
+ else
122
+ byId.get(host)?.children.push(vn);
123
+ };
124
+ for (const n of m.nodes) {
125
+ const vn = byId.get(n.id);
126
+ if (vn && !promoted.has(n.id))
127
+ placeInto(vn);
128
+ }
129
+ for (const n of m.nodes) {
130
+ const vn = byId.get(n.id);
131
+ if (vn && promoted.has(n.id))
132
+ placeInto(vn);
133
+ }
134
+ // Shared markers: visible parents that are not the placement host.
135
+ for (const n of m.nodes) {
136
+ if (!byId.has(n.id))
137
+ continue;
138
+ const parents = h.parentsOf.get(n.id) ?? [];
139
+ if (parents.length < 2)
140
+ continue;
141
+ for (const p of parents) {
142
+ if (p !== hostOf.get(n.id) && byId.has(p))
143
+ byId.get(p)?.sharedMembers.push(n.id);
144
+ }
145
+ }
146
+ return { roots, byId, anchorOf, hostOf };
147
+ }
@@ -0,0 +1,68 @@
1
+ import type { DiagramNode, DiagramRelation, EdgeLabel, Polarity, RelationStyle } from '../types.js';
2
+ export type NodeViewState = 'leaf' | 'expanded' | 'collapsed';
3
+ /** Collapse decisions for container nodes only; leaves never appear. */
4
+ export type LodState = Record<string, 'expanded' | 'collapsed'>;
5
+ export interface ViewportState {
6
+ /** active plane id; absent = the model's first-declared plane (or the implicit one) */
7
+ plane?: string;
8
+ /** container ids the viewer is zoomed into (renderer-computed focus chain) */
9
+ focus?: string[];
10
+ pins?: Record<string, 'expanded' | 'collapsed'>;
11
+ /** layer ids to draw. ABSENT = the host has no opinion, so the active plane's
12
+ * `layers` presets apply; PRESENT = the host's own choice, which replaces the
13
+ * presets entirely — an empty array therefore means "no layers at all", not
14
+ * "presets only". A host with a layer switch seeds its state from the plane's
15
+ * `layers` (on load and on every plane change) and owns it from then on. */
16
+ activeLayers?: string[];
17
+ /** nested-zoom drill root: when set, the view is scoped to this node's INTERIOR
18
+ * — its children become the top-level content (the node itself isn't drawn) and
19
+ * everything outside its subtree disappears. Relations crossing the boundary are
20
+ * re-pointed to an external stub node (see ViewNode.external). */
21
+ root?: string;
22
+ }
23
+ export interface Size {
24
+ width: number;
25
+ height: number;
26
+ }
27
+ export interface ViewNode {
28
+ id: string;
29
+ node: DiagramNode;
30
+ state: NodeViewState;
31
+ children: ViewNode[];
32
+ promoted: boolean;
33
+ /** Ids of shared nodes contained here but rendered elsewhere. */
34
+ sharedMembers: string[];
35
+ /** Set on a drill-view stub node: the id of the real (off-frame) node it stands
36
+ * in for — an edge to something outside the current drill root. Clicking it
37
+ * navigates into that node. */
38
+ external?: string;
39
+ }
40
+ export interface ViewEdge {
41
+ id: string;
42
+ from: string;
43
+ to: string;
44
+ kind: string;
45
+ label?: string;
46
+ /** the sole constituent's positioned labels; aggregated edges have none */
47
+ labels?: EdgeLabel[];
48
+ layer?: string;
49
+ tint?: string;
50
+ /** the sole constituent's per-relation style; aggregated edges have none */
51
+ style?: RelationStyle;
52
+ /** CLD polarity: a single constituent's sign, or an aggregate's shared sign when every constituent agrees; absent when constituents disagree or any lacks a sign */
53
+ polarity?: Polarity;
54
+ /** the sole constituent's CLD delay marker; aggregated edges have none */
55
+ delay?: boolean;
56
+ constituents: DiagramRelation[];
57
+ }
58
+ export interface CompiledView {
59
+ roots: ViewNode[];
60
+ /** the arrows to DRAW (filtered by active layers / plane) */
61
+ edges: ViewEdge[];
62
+ /** the arrows to LAYOUT BY (all layers + base) — stable across overlay
63
+ * toggles and structure-sharing planes, so boxes never jump */
64
+ layoutEdges: ViewEdge[];
65
+ lod: LodState;
66
+ /** drill view only: stub node id -> the real off-frame node it represents. */
67
+ externals?: Map<string, string>;
68
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@diagc/core",
3
+ "version": "0.1.0",
4
+ "description": "Diagram model, validator, and builder DSL for diagc — no React, no filesystem.",
5
+ "license": "AGPL-3.0-only",
6
+ "author": "Bogdan Frankovskyi",
7
+ "homepage": "https://github.com/Ferroman/diagc#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Ferroman/diagc.git",
11
+ "directory": "packages/core"
12
+ },
13
+ "keywords": [
14
+ "diagram",
15
+ "architecture",
16
+ "c4",
17
+ "causal-loop",
18
+ "dsl"
19
+ ],
20
+ "type": "module",
21
+ "engines": {
22
+ "node": ">=22"
23
+ },
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {},
37
+ "types": "./dist/index.d.ts"
38
+ }