@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,95 @@
1
+ /** The notation id a plane (or the model) declares to be drawn as a fish. */
2
+ export const FISHBONE_NOTATION = 'fishbone';
3
+ export const FB_EFFECT_TYPE = 'fb-effect';
4
+ export const FB_CATEGORY_TYPE = 'fb-category';
5
+ /** A cause and a sub-cause are ONE type: which it is comes from where it hangs
6
+ * (see fishboneTree), so promoting a sub-cause is a relation edit, not a type
7
+ * change, and nothing about a node can disagree with its place on the fish. */
8
+ export const FB_CAUSE_TYPE = 'fb-cause';
9
+ export const FISHBONE_TYPES = [FB_EFFECT_TYPE, FB_CATEGORY_TYPE, FB_CAUSE_TYPE];
10
+ /** The kind the builder and the studio create — from the cause TO what it
11
+ * explains. The derivation below does NOT filter by it (see fishboneTree). */
12
+ export const FB_CAUSE_OF_KIND = 'cause-of';
13
+ export const isFishboneNode = (n) => n.type !== undefined && FISHBONE_TYPES.includes(n.type);
14
+ /** Category sets an author can start from. Software first: it is what this
15
+ * tool is mostly used for; the classic manufacturing (6M) and service (4S) sets
16
+ * follow. The order of names is the order of bones. */
17
+ export const FISHBONE_PRESETS = {
18
+ Software: ['People', 'Process', 'Requirements', 'Code', 'Infrastructure', 'Dependencies'],
19
+ '6M': ['Man', 'Machine', 'Method', 'Material', 'Measurement', 'Environment'],
20
+ '4S': ['Surroundings', 'Suppliers', 'Systems', 'Skills'],
21
+ };
22
+ export const FISHBONE_PRESET_NAMES = ['Software', '6M', '4S'];
23
+ /** node id for a preset category name: 'Infrastructure' → 'infrastructure' */
24
+ export const presetId = (name) => name
25
+ .toLowerCase()
26
+ .replace(/[^a-z0-9]+/g, '-')
27
+ .replace(/^-+|-+$/g, '');
28
+ /**
29
+ * The one definition of "hangs on": a fishbone node's parent is the `to` of
30
+ * its FIRST relation (declaration order) whose other end is also a fishbone
31
+ * node — any kind, so restyling an arrow can never drop a cause off its bone.
32
+ * Self-loops are skipped, and a relation whose ends aren't both fishbone
33
+ * nodes doesn't count as a parent pick. Shared by fishboneTree and
34
+ * validateFishbone (validate.ts) so the rule is defined exactly once.
35
+ */
36
+ export function fishboneParents(model) {
37
+ const typeOf = new Map(model.nodes.filter(isFishboneNode).map((n) => [n.id, n.type]));
38
+ const parentOf = new Map();
39
+ for (const r of model.relations) {
40
+ if (r.from === r.to || !typeOf.has(r.from) || !typeOf.has(r.to) || parentOf.has(r.from))
41
+ continue;
42
+ parentOf.set(r.from, r.to);
43
+ }
44
+ return parentOf;
45
+ }
46
+ /**
47
+ * What hangs where. This is the ONE place that answers it, so the layout, the
48
+ * colour hooks, the studio panel and validation cannot disagree.
49
+ *
50
+ * Reads a node's parent via fishboneParents, then reads the fish top-down from
51
+ * the effect with the types checked at each level: only a category hangs on
52
+ * the effect, only a cause on a category, only a cause on a cause, and a
53
+ * sub-cause has no children.
54
+ *
55
+ * Reads the MODEL's relations, never drawn edges: toggling a layer must not
56
+ * move a box. Never throws.
57
+ */
58
+ export function fishboneTree(model) {
59
+ const nodes = model.nodes.filter(isFishboneNode);
60
+ const typeOf = new Map(nodes.map((n) => [n.id, n.type]));
61
+ const effect = nodes.find((n) => n.type === FB_EFFECT_TYPE)?.id;
62
+ const parentOf = fishboneParents(model);
63
+ // Map iteration is insertion order, so each child list is in relation order.
64
+ const childrenOf = new Map();
65
+ for (const [child, parent] of parentOf) {
66
+ const list = childrenOf.get(parent);
67
+ if (list === undefined)
68
+ childrenOf.set(parent, [child]);
69
+ else
70
+ list.push(child);
71
+ }
72
+ const kids = (id, type) => (childrenOf.get(id) ?? []).filter((c) => typeOf.get(c) === type);
73
+ const placed = new Set();
74
+ const categories = [];
75
+ if (effect !== undefined) {
76
+ placed.add(effect);
77
+ for (const c of kids(effect, FB_CATEGORY_TYPE)) {
78
+ placed.add(c);
79
+ const causes = [];
80
+ for (const cause of kids(c, FB_CAUSE_TYPE)) {
81
+ placed.add(cause);
82
+ const subs = kids(cause, FB_CAUSE_TYPE);
83
+ for (const s of subs)
84
+ placed.add(s);
85
+ causes.push({ id: cause, subs });
86
+ }
87
+ categories.push({ id: c, causes });
88
+ }
89
+ }
90
+ return {
91
+ ...(effect !== undefined ? { effect } : {}),
92
+ categories,
93
+ unattached: nodes.map((n) => n.id).filter((id) => !placed.has(id)),
94
+ };
95
+ }
package/dist/git.d.ts ADDED
@@ -0,0 +1,65 @@
1
+ import type { DiagramModel, DiagramNode } from './types.js';
2
+ /** The notation id a plane declares to be drawn as a git graph. */
3
+ export declare const GIT_NOTATION: "git-graph";
4
+ /** The three link kinds the layout understands. Any other kind between commits
5
+ * is drawn as a plain edge and never moves a circle. */
6
+ export declare const GIT_KINDS: readonly ["commit", "branch", "merge"];
7
+ export type GitKind = (typeof GIT_KINDS)[number];
8
+ export declare const isGitKind: (k: string) => k is GitKind;
9
+ /** A stage: a named frame drawn across EVERY lane, from one commit's column to
10
+ * another's — "Development", "Release candidates", "Hotfix". It is a node of
11
+ * this type whose span lives in its metadata (`from`, and optionally `to`, each
12
+ * a commit id) rather than in containment: a commit already belongs to its lane,
13
+ * and a stage cuts across lanes. */
14
+ export declare const GIT_STAGE_TYPE: "git-stage";
15
+ export interface GitStage {
16
+ id: string;
17
+ node: DiagramNode;
18
+ /** first and last column the frame covers, inclusive (from ≤ to) */
19
+ fromCol: number;
20
+ toCol: number;
21
+ }
22
+ /** the commit id a stage names under `key`, if it names one at all */
23
+ export declare function stageCommit(n: DiagramNode, key: 'from' | 'to'): string | undefined;
24
+ export interface GitLane {
25
+ id: string;
26
+ node: DiagramNode;
27
+ /** this lane's commits in declaration order */
28
+ commits: DiagramNode[];
29
+ }
30
+ export interface GitGraph {
31
+ /** `type: 'branch'` nodes in declaration order — top-to-bottom lane order */
32
+ lanes: GitLane[];
33
+ /** commit id → lane id */
34
+ laneOf: Map<string, string>;
35
+ /** commit id → column, every commit including strays */
36
+ columns: Map<string, number>;
37
+ /** `type: 'commit'` nodes no lane contains on this plane */
38
+ strays: DiagramNode[];
39
+ /** relation ids ignored to break cycles; empty for a valid graph */
40
+ cycleEdges: string[];
41
+ /** `type: 'git-stage'` nodes whose span resolves to columns, in declaration
42
+ * order. One that names no known commit is left out (validation reports it). */
43
+ stages: GitStage[];
44
+ }
45
+ /** `metadata.gap` as a count of empty columns: a non-negative integer, or a
46
+ * string of digits (the studio's generic metadata editor stores strings). */
47
+ export declare function gapOf(n: DiagramNode): number;
48
+ /** true when the commit's segment ended in a merge (it has an outgoing `merge`). */
49
+ export declare function mergedAway(model: DiagramModel, commitId: string): boolean;
50
+ /**
51
+ * Lanes, membership and columns for one plane. This is the ONE place that
52
+ * answers "which lane is this commit on" and "which column does it take", so
53
+ * the renderer's layout, the studio's panel and validation cannot disagree.
54
+ * Containment is read the way the view reads it: the plane's own edges, or its
55
+ * donor's when it borrows (`containmentOf`); untagged edges belong to the
56
+ * default (first-declared) plane. Never throws — a cycle is cut, not reported
57
+ * as an error here (validation does that from `cycleEdges`).
58
+ */
59
+ export declare function gitGraph(model: DiagramModel, plane?: string): GitGraph;
60
+ /** `${lane}-${n}` with the smallest n not yet taken — the DSL's naming, kept
61
+ * unique even after deletions. Shared by the Git panel and the canvas `+` so
62
+ * a commit is named the same whichever created it. */
63
+ export declare function nextCommitId(model: DiagramModel, laneId: string): string;
64
+ /** The lane's rightmost commit (max column; ties go to the later declared). */
65
+ export declare function latestCommit(g: GitGraph, laneId: string): DiagramNode | undefined;
package/dist/git.js ADDED
@@ -0,0 +1,159 @@
1
+ import { containmentPlaneOf } from './view/hierarchy.js';
2
+ /** The notation id a plane declares to be drawn as a git graph. */
3
+ export const GIT_NOTATION = 'git-graph';
4
+ /** The three link kinds the layout understands. Any other kind between commits
5
+ * is drawn as a plain edge and never moves a circle. */
6
+ export const GIT_KINDS = ['commit', 'branch', 'merge'];
7
+ export const isGitKind = (k) => GIT_KINDS.includes(k);
8
+ /** A stage: a named frame drawn across EVERY lane, from one commit's column to
9
+ * another's — "Development", "Release candidates", "Hotfix". It is a node of
10
+ * this type whose span lives in its metadata (`from`, and optionally `to`, each
11
+ * a commit id) rather than in containment: a commit already belongs to its lane,
12
+ * and a stage cuts across lanes. */
13
+ export const GIT_STAGE_TYPE = 'git-stage';
14
+ /** the commit id a stage names under `key`, if it names one at all */
15
+ export function stageCommit(n, key) {
16
+ const raw = n.metadata?.[key];
17
+ return typeof raw === 'string' && raw !== '' ? raw : undefined;
18
+ }
19
+ /** `metadata.gap` as a count of empty columns: a non-negative integer, or a
20
+ * string of digits (the studio's generic metadata editor stores strings). */
21
+ export function gapOf(n) {
22
+ const raw = n.metadata?.['gap'];
23
+ if (typeof raw === 'number')
24
+ return Number.isInteger(raw) && raw >= 0 ? raw : 0;
25
+ if (typeof raw === 'string' && /^\d+$/.test(raw))
26
+ return Number(raw);
27
+ return 0;
28
+ }
29
+ /** true when the commit's segment ended in a merge (it has an outgoing `merge`). */
30
+ export function mergedAway(model, commitId) {
31
+ return model.relations.some((r) => r.kind === 'merge' && r.from === commitId);
32
+ }
33
+ /**
34
+ * Lanes, membership and columns for one plane. This is the ONE place that
35
+ * answers "which lane is this commit on" and "which column does it take", so
36
+ * the renderer's layout, the studio's panel and validation cannot disagree.
37
+ * Containment is read the way the view reads it: the plane's own edges, or its
38
+ * donor's when it borrows (`containmentOf`); untagged edges belong to the
39
+ * default (first-declared) plane. Never throws — a cycle is cut, not reported
40
+ * as an error here (validation does that from `cycleEdges`).
41
+ */
42
+ export function gitGraph(model, plane) {
43
+ const planes = model.planes ?? [];
44
+ const defaultPlane = planes[0]?.id;
45
+ const active = containmentPlaneOf(model, plane) ?? plane ?? defaultPlane;
46
+ const edges = model.containment.filter((e) => (e.plane ?? defaultPlane) === active);
47
+ const byId = new Map(model.nodes.map((n) => [n.id, n]));
48
+ const index = new Map(model.nodes.map((n, i) => [n.id, i]));
49
+ const declared = (a, b) => index.get(a.id) - index.get(b.id);
50
+ const laneOf = new Map();
51
+ const lanes = [];
52
+ for (const n of model.nodes) {
53
+ if (n.type !== 'branch')
54
+ continue;
55
+ const members = edges
56
+ .filter((e) => e.parent === n.id)
57
+ .map((e) => byId.get(e.child))
58
+ .filter((c) => c !== undefined && c.type === 'commit')
59
+ .sort(declared);
60
+ // Containment is a DAG; a commit two lanes claim belongs to the first one
61
+ // declared, so every commit has exactly one row.
62
+ const own = members.filter((c) => !laneOf.has(c.id));
63
+ for (const c of own)
64
+ laneOf.set(c.id, n.id);
65
+ lanes.push({ id: n.id, node: n, commits: own });
66
+ }
67
+ const commits = model.nodes.filter((n) => n.type === 'commit');
68
+ const strays = commits.filter((c) => !laneOf.has(c.id));
69
+ const { columns, cycleEdges } = computeColumns(model.relations, commits, byId, index);
70
+ const stages = [];
71
+ for (const n of model.nodes) {
72
+ if (n.type !== GIT_STAGE_TYPE)
73
+ continue;
74
+ const from = stageCommit(n, 'from');
75
+ const a = from !== undefined ? columns.get(from) : undefined;
76
+ const b = columns.get(stageCommit(n, 'to') ?? from ?? '');
77
+ if (a === undefined || b === undefined)
78
+ continue;
79
+ stages.push({ id: n.id, node: n, fromCol: Math.min(a, b), toCol: Math.max(a, b) });
80
+ }
81
+ return { lanes, laneOf, columns, strays, cycleEdges, stages };
82
+ }
83
+ /** `${lane}-${n}` with the smallest n not yet taken — the DSL's naming, kept
84
+ * unique even after deletions. Shared by the Git panel and the canvas `+` so
85
+ * a commit is named the same whichever created it. */
86
+ export function nextCommitId(model, laneId) {
87
+ const taken = new Set(model.nodes.map((n) => n.id));
88
+ for (let n = 1;; n++) {
89
+ const id = `${laneId}-${n}`;
90
+ if (!taken.has(id))
91
+ return id;
92
+ }
93
+ }
94
+ /** The lane's rightmost commit (max column; ties go to the later declared). */
95
+ export function latestCommit(g, laneId) {
96
+ const lane = g.lanes.find((l) => l.id === laneId);
97
+ let best;
98
+ for (const c of lane?.commits ?? []) {
99
+ if (best === undefined || (g.columns.get(c.id) ?? 0) >= (g.columns.get(best.id) ?? 0))
100
+ best = c;
101
+ }
102
+ return best;
103
+ }
104
+ /**
105
+ * Longest-path columns over the git links: Kahn's algorithm, ready nodes taken
106
+ * in declaration order so the result is stable under re-serialisation. When
107
+ * nothing is ready but commits remain, the links form a cycle: the
108
+ * earliest-declared remaining commit gives up its incoming links from other
109
+ * remaining commits (recorded in `cycleEdges`) and the walk goes on.
110
+ */
111
+ function computeColumns(relations, commits, byId, index) {
112
+ const ids = new Set(commits.map((c) => c.id));
113
+ const links = relations.filter((r) => isGitKind(r.kind) && ids.has(r.from) && ids.has(r.to) && r.from !== r.to);
114
+ const incoming = new Map();
115
+ const outgoing = new Map();
116
+ const indeg = new Map(commits.map((c) => [c.id, 0]));
117
+ for (const l of links) {
118
+ incoming.set(l.to, [...(incoming.get(l.to) ?? []), l]);
119
+ outgoing.set(l.from, [...(outgoing.get(l.from) ?? []), l]);
120
+ indeg.set(l.to, indeg.get(l.to) + 1);
121
+ }
122
+ const order = [...commits].sort((a, b) => index.get(a.id) - index.get(b.id));
123
+ const remaining = new Set(order.map((c) => c.id));
124
+ const ready = order.filter((c) => indeg.get(c.id) === 0).map((c) => c.id);
125
+ const dropped = new Set();
126
+ const cycleEdges = [];
127
+ const columns = new Map();
128
+ while (remaining.size > 0) {
129
+ if (ready.length === 0) {
130
+ const victim = order.find((c) => remaining.has(c.id));
131
+ for (const l of incoming.get(victim.id) ?? []) {
132
+ if (!remaining.has(l.from) || dropped.has(l.id))
133
+ continue;
134
+ dropped.add(l.id);
135
+ cycleEdges.push(l.id);
136
+ indeg.set(victim.id, indeg.get(victim.id) - 1);
137
+ }
138
+ ready.push(victim.id);
139
+ }
140
+ ready.sort((a, b) => index.get(a) - index.get(b));
141
+ const id = ready.shift();
142
+ remaining.delete(id);
143
+ let col = 0;
144
+ for (const l of incoming.get(id) ?? []) {
145
+ if (!dropped.has(l.id))
146
+ col = Math.max(col, (columns.get(l.from) ?? 0) + 1);
147
+ }
148
+ columns.set(id, col + gapOf(byId.get(id)));
149
+ for (const l of outgoing.get(id) ?? []) {
150
+ if (dropped.has(l.id))
151
+ continue;
152
+ const d = indeg.get(l.to) - 1;
153
+ indeg.set(l.to, d);
154
+ if (d === 0)
155
+ ready.push(l.to);
156
+ }
157
+ }
158
+ return { columns, cycleEdges };
159
+ }
@@ -0,0 +1,10 @@
1
+ import { type Drawings, type LayoutOverlay } from './types.js';
2
+ /** Structural guard for a LayoutOverlay, shared by the studio server (before
3
+ * persisting a layout) and the client (before trusting a loaded one). Checks
4
+ * shape only — not referential integrity against a model. */
5
+ export declare function isLayoutOverlay(u: unknown): u is LayoutOverlay;
6
+ /** Structural guard for a drawings sidecar — the same contract as
7
+ * isLayoutOverlay: shape only, shared by the save route and the client loader.
8
+ * Every rule here is one the renderer relies on without re-checking (even point
9
+ * count, finite numbers, positive width). */
10
+ export declare function isDrawings(u: unknown): u is Drawings;
package/dist/guards.js ADDED
@@ -0,0 +1,98 @@
1
+ import { EDGE_LABEL_SIDES } from './types.js';
2
+ /** Structural guard for a LayoutOverlay, shared by the studio server (before
3
+ * persisting a layout) and the client (before trusting a loaded one). Checks
4
+ * shape only — not referential integrity against a model. */
5
+ export function isLayoutOverlay(u) {
6
+ if (typeof u !== 'object' || u === null)
7
+ return false;
8
+ const layout = u;
9
+ if (layout.version !== 1 || typeof layout.planes !== 'object' || layout.planes === null)
10
+ return false;
11
+ const planesOk = Object.values(layout.planes).every((plane) => typeof plane === 'object' &&
12
+ plane !== null &&
13
+ Object.values(plane).every((p) => typeof p === 'object' &&
14
+ p !== null &&
15
+ typeof p.x === 'number' &&
16
+ typeof p.y === 'number'));
17
+ if (!planesOk)
18
+ return false;
19
+ const dim = (v) => typeof v === 'number' && Number.isFinite(v) && v > 0;
20
+ const sizesOk = layout.sizes === undefined ||
21
+ (typeof layout.sizes === 'object' &&
22
+ layout.sizes !== null &&
23
+ Object.values(layout.sizes).every((s) => typeof s === 'object' && s !== null && dim(s.w) && dim(s.h)));
24
+ if (!sizesOk)
25
+ return false;
26
+ const unfolded = u.unfolded;
27
+ const unfoldedOk = unfolded === undefined ||
28
+ (typeof unfolded === 'object' &&
29
+ unfolded !== null &&
30
+ !Array.isArray(unfolded) &&
31
+ Object.values(unfolded).every((ids) => Array.isArray(ids) && ids.every((id) => typeof id === 'string')));
32
+ if (!unfoldedOk)
33
+ return false;
34
+ const edgeLabels = u.edgeLabels;
35
+ const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
36
+ const placementOk = (v) => isRecord(v) &&
37
+ typeof v['t'] === 'number' &&
38
+ Number.isFinite(v['t']) &&
39
+ (v['side'] === undefined || EDGE_LABEL_SIDES.includes(v['side']));
40
+ const edgeLabelsOk = edgeLabels === undefined ||
41
+ (isRecord(edgeLabels) &&
42
+ Object.values(edgeLabels).every((plane) => isRecord(plane) && Object.values(plane).every((rel) => isRecord(rel) && Object.values(rel).every(placementOk))));
43
+ if (!edgeLabelsOk)
44
+ return false;
45
+ // A note offset is drawn straight into a transform, so a NaN or an Infinity
46
+ // here is a note the reader can never find again — finiteness is checked, not
47
+ // just the type.
48
+ const finiteNum = (v) => typeof v === 'number' && Number.isFinite(v);
49
+ const notes = u.notes;
50
+ const notesOk = notes === undefined ||
51
+ (isRecord(notes) &&
52
+ Object.values(notes).every((plane) => isRecord(plane) &&
53
+ Object.values(plane).every((o) => isRecord(o) &&
54
+ finiteNum(o['dx']) &&
55
+ finiteNum(o['dy']) &&
56
+ // `true` or absent: a stored `false` would be a second spelling of "closed"
57
+ (o['open'] === undefined || o['open'] === true))));
58
+ if (!notesOk)
59
+ return false;
60
+ // `export` is export-only presentation (see LayoutOverlay): an object whose
61
+ // only field today is a list of node ids. Validate it structurally so a typo
62
+ // is a 400 from the studio's save endpoint rather than a silently ignored
63
+ // block — an unreadable PNG is a hard defect to trace back to a layout file.
64
+ const exp = u.export;
65
+ if (exp === undefined)
66
+ return true;
67
+ if (typeof exp !== 'object' || exp === null || Array.isArray(exp))
68
+ return false;
69
+ const collapsed = exp.collapsed;
70
+ return (collapsed === undefined || (Array.isArray(collapsed) && collapsed.every((id) => typeof id === 'string')));
71
+ }
72
+ /** Structural guard for a drawings sidecar — the same contract as
73
+ * isLayoutOverlay: shape only, shared by the save route and the client loader.
74
+ * Every rule here is one the renderer relies on without re-checking (even point
75
+ * count, finite numbers, positive width). */
76
+ export function isDrawings(u) {
77
+ if (typeof u !== 'object' || u === null)
78
+ return false;
79
+ const d = u;
80
+ if (d.version !== 1 || typeof d.planes !== 'object' || d.planes === null)
81
+ return false;
82
+ const finite = (n) => typeof n === 'number' && Number.isFinite(n);
83
+ return Object.values(d.planes).every((bucket) => Array.isArray(bucket) &&
84
+ bucket.every((s) => {
85
+ if (typeof s !== 'object' || s === null)
86
+ return false;
87
+ const st = s;
88
+ if (typeof st.id !== 'string')
89
+ return false;
90
+ if (!Array.isArray(st.points) || st.points.length < 2 || st.points.length % 2 !== 0)
91
+ return false;
92
+ if (!st.points.every(finite))
93
+ return false;
94
+ if (st.color !== undefined && typeof st.color !== 'string')
95
+ return false;
96
+ return st.width === undefined || (finite(st.width) && st.width > 0);
97
+ }));
98
+ }
@@ -0,0 +1,25 @@
1
+ export declare const CORE_VERSION = 1;
2
+ export { ejectSource } from './eject.js';
3
+ export * from './types.js';
4
+ export * from './mutate.js';
5
+ export { normalizeRuns, runsToPlainText } from './text.js';
6
+ export { relationLabels } from './labels.js';
7
+ export { defaultLayoutDirection, type LayoutDirection } from './layout-defaults.js';
8
+ export { model, ModelBuilder, NodeRef, BranchRef, CommitRef, GitGraphBuilder, ActivityBuilder, ActivityScope, LaneRef, RegionRef, ConsequenceRef, SecondOrderBuilder, FishboneBuilder, CategoryRef, CauseRef, ThreatModelBuilder, FlowRef, type NodeOpts, type RelateOpts, type CommitOpts, type StageOpts, type MergeOpts, type ActivityElementOpts, type ConsequenceOpts, type FishboneOpts, type ThreatOpts, type ElementOpts, } from './builder.js';
9
+ export { validate, DiagramValidationError, IMAGE_REF, LIBRARY_IMAGE_REF, type ValidationIssue } from './validate.js';
10
+ export { isDrawings, isLayoutOverlay } from './guards.js';
11
+ export { addStroke, deleteStroke, emptyDrawings, pruneDrawingsPlane, uniqueStrokeId } from './drawings.js';
12
+ export { errMessage, SOURCE_URL } from './util.js';
13
+ export { childrenOf, countAnchored } from './children.js';
14
+ export { activeNotation, compileView, presetLayers, resolveContainmentPlane } from './view/compile.js';
15
+ export { buildHierarchy, type HierarchyIndex } from './view/hierarchy.js';
16
+ export { relationLayer } from './view/layers.js';
17
+ export { scopeToRoot, EXTERNAL_STUB_PREFIX, type ScopedModel } from './view/scope.js';
18
+ export { estimateSizes, LEAF_SIZE, CONTAINER_PADDING, CONTAINER_HEADER } from './view/size.js';
19
+ export type { CompiledView, LodState, NodeViewState, Size, ViewEdge, ViewNode, ViewportState, } from './view/types.js';
20
+ export { applyCommand, applyCommandWithResult, emptyLayout, layoutPlaneKey, openingPins, withEdgeLabelPlacements, withUnfolded, type EditorCommand, type EditorState, } from './commands.js';
21
+ export { composeIncludes, IncludeError, MAX_INCLUDE_DEPTH, type IncludeResolver, type IncludeSource, } from './compose.js';
22
+ export { GIT_KINDS, GIT_NOTATION, GIT_STAGE_TYPE, gapOf, gitGraph, isGitKind, latestCommit, mergedAway, nextCommitId, stageCommit, type GitGraph, type GitKind, type GitLane, type GitStage, } from './git.js';
23
+ export { SECOND_ORDER_NOTATION, SO_CONSEQUENCE_TYPES, SO_DECISION_TYPE, SO_LEADS_TO_KIND, consequenceOrders, consequenceTypeOf, isSecondOrderNode, valenceOf, type ConsequenceOrders, type Valence, } from './second-order.js';
24
+ export { FB_CATEGORY_TYPE, FB_CAUSE_OF_KIND, FB_CAUSE_TYPE, FB_EFFECT_TYPE, FISHBONE_NOTATION, FISHBONE_PRESET_NAMES, FISHBONE_PRESETS, FISHBONE_TYPES, fishboneParents, fishboneTree, isFishboneNode, presetId, type FishboneCategory, type FishboneCause, type FishbonePreset, type FishboneTree, } from './fishbone.js';
25
+ export { TM_NOTATION, TM_ENTITY_TYPE, TM_PROCESS_TYPE, TM_STORE_TYPE, TM_BOUNDARY_TYPE, TM_FLOW_KIND, TM_TYPES, STRIDE_NAMES, NEW_THREAT_TITLE, isThreatModelNode, isOpen, strideFor, boundaryOf, boundaryName, crossings, crossingLabel, threatRegister, threatSummary, threatTargetKey, threatsOf, nextThreatId, nextThreatStatus, allNotesOpen, type ThreatTarget, type Crossing, type ThreatRow, } from './threat-model.js';
package/dist/index.js ADDED
@@ -0,0 +1,45 @@
1
+ /*
2
+ * @diagc/core — diagram model, validator, builder DSL, and view compiler.
3
+ * Copyright (C) 2026 Bogdan Frankovskyi
4
+ *
5
+ * This program is free software: you can redistribute it and/or modify it
6
+ * under the terms of the GNU Affero General Public License version 3 as
7
+ * published by the Free Software Foundation, with the additional permissions
8
+ * granted under section 7 that are set out in the LICENSE file alongside this
9
+ * package. Those permissions let you license diagram sources you author, and
10
+ * the output produced from them, under terms of your choosing.
11
+ *
12
+ * This program is distributed in the hope that it will be useful, but WITHOUT
13
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
15
+ * for more details.
16
+ *
17
+ * You should have received a copy of the GNU Affero General Public License
18
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
19
+ *
20
+ * SPDX-License-Identifier: AGPL-3.0-only
21
+ */
22
+ export const CORE_VERSION = 1;
23
+ export { ejectSource } from './eject.js';
24
+ export * from './types.js';
25
+ export * from './mutate.js';
26
+ export { normalizeRuns, runsToPlainText } from './text.js';
27
+ export { relationLabels } from './labels.js';
28
+ export { defaultLayoutDirection } from './layout-defaults.js';
29
+ export { model, ModelBuilder, NodeRef, BranchRef, CommitRef, GitGraphBuilder, ActivityBuilder, ActivityScope, LaneRef, RegionRef, ConsequenceRef, SecondOrderBuilder, FishboneBuilder, CategoryRef, CauseRef, ThreatModelBuilder, FlowRef, } from './builder.js';
30
+ export { validate, DiagramValidationError, IMAGE_REF, LIBRARY_IMAGE_REF } from './validate.js';
31
+ export { isDrawings, isLayoutOverlay } from './guards.js';
32
+ export { addStroke, deleteStroke, emptyDrawings, pruneDrawingsPlane, uniqueStrokeId } from './drawings.js';
33
+ export { errMessage, SOURCE_URL } from './util.js';
34
+ export { childrenOf, countAnchored } from './children.js';
35
+ export { activeNotation, compileView, presetLayers, resolveContainmentPlane } from './view/compile.js';
36
+ export { buildHierarchy } from './view/hierarchy.js';
37
+ export { relationLayer } from './view/layers.js';
38
+ export { scopeToRoot, EXTERNAL_STUB_PREFIX } from './view/scope.js';
39
+ export { estimateSizes, LEAF_SIZE, CONTAINER_PADDING, CONTAINER_HEADER } from './view/size.js';
40
+ export { applyCommand, applyCommandWithResult, emptyLayout, layoutPlaneKey, openingPins, withEdgeLabelPlacements, withUnfolded, } from './commands.js';
41
+ export { composeIncludes, IncludeError, MAX_INCLUDE_DEPTH, } from './compose.js';
42
+ export { GIT_KINDS, GIT_NOTATION, GIT_STAGE_TYPE, gapOf, gitGraph, isGitKind, latestCommit, mergedAway, nextCommitId, stageCommit, } from './git.js';
43
+ export { SECOND_ORDER_NOTATION, SO_CONSEQUENCE_TYPES, SO_DECISION_TYPE, SO_LEADS_TO_KIND, consequenceOrders, consequenceTypeOf, isSecondOrderNode, valenceOf, } from './second-order.js';
44
+ export { FB_CATEGORY_TYPE, FB_CAUSE_OF_KIND, FB_CAUSE_TYPE, FB_EFFECT_TYPE, FISHBONE_NOTATION, FISHBONE_PRESET_NAMES, FISHBONE_PRESETS, FISHBONE_TYPES, fishboneParents, fishboneTree, isFishboneNode, presetId, } from './fishbone.js';
45
+ export { TM_NOTATION, TM_ENTITY_TYPE, TM_PROCESS_TYPE, TM_STORE_TYPE, TM_BOUNDARY_TYPE, TM_FLOW_KIND, TM_TYPES, STRIDE_NAMES, NEW_THREAT_TITLE, isThreatModelNode, isOpen, strideFor, boundaryOf, boundaryName, crossings, crossingLabel, threatRegister, threatSummary, threatTargetKey, threatsOf, nextThreatId, nextThreatStatus, allNotesOpen, } from './threat-model.js';
@@ -0,0 +1,5 @@
1
+ import type { DiagramRelation, EdgeLabel } from './types.js';
2
+ /** Effective labels for a relation, bridging the legacy single `label` string.
3
+ * `labels` (when present) is authoritative; otherwise a non-empty legacy
4
+ * `label` becomes one centered label; otherwise none. */
5
+ export declare function relationLabels(r: DiagramRelation): EdgeLabel[];
package/dist/labels.js ADDED
@@ -0,0 +1,10 @@
1
+ /** Effective labels for a relation, bridging the legacy single `label` string.
2
+ * `labels` (when present) is authoritative; otherwise a non-empty legacy
3
+ * `label` becomes one centered label; otherwise none. */
4
+ export function relationLabels(r) {
5
+ if (r.labels !== undefined)
6
+ return r.labels;
7
+ if (r.label !== undefined && r.label !== '')
8
+ return [{ id: 'legacy', text: r.label, t: 0.5, side: 'center' }];
9
+ return [];
10
+ }
@@ -0,0 +1,20 @@
1
+ import type { DiagramModel } from './types.js';
2
+ export type LayoutDirection = 'DOWN' | 'RIGHT' | 'LEFT' | 'UP';
3
+ /**
4
+ * The flow direction an automatically laid-out plane takes when its settings
5
+ * name none.
6
+ *
7
+ * Down. A diagram is read on a page and embedded in documents, where height
8
+ * scrolls and width does not: a left-to-right chain of a dozen boxes becomes a
9
+ * ribbon that has to be shrunk until its labels are unreadable, the same chain
10
+ * top-to-bottom is a column read at full size. Boxes are also wide and short, so
11
+ * stacking them wastes far less room than queueing them.
12
+ *
13
+ * The exception is a model that draws activity frames: their lanes are
14
+ * horizontal bands (vertical lanes are a recorded deferral), and a flow that ran
15
+ * ACROSS the bands would make every lane as tall as the whole activity.
16
+ *
17
+ * In core so the renderer (which lays out) and the studio (whose direction
18
+ * picker shows the default as selected) cannot drift.
19
+ */
20
+ export declare function defaultLayoutDirection(model: Pick<DiagramModel, 'nodes'>): LayoutDirection;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The flow direction an automatically laid-out plane takes when its settings
3
+ * name none.
4
+ *
5
+ * Down. A diagram is read on a page and embedded in documents, where height
6
+ * scrolls and width does not: a left-to-right chain of a dozen boxes becomes a
7
+ * ribbon that has to be shrunk until its labels are unreadable, the same chain
8
+ * top-to-bottom is a column read at full size. Boxes are also wide and short, so
9
+ * stacking them wastes far less room than queueing them.
10
+ *
11
+ * The exception is a model that draws activity frames: their lanes are
12
+ * horizontal bands (vertical lanes are a recorded deferral), and a flow that ran
13
+ * ACROSS the bands would make every lane as tall as the whole activity.
14
+ *
15
+ * In core so the renderer (which lays out) and the studio (whose direction
16
+ * picker shows the default as selected) cannot drift.
17
+ */
18
+ export function defaultLayoutDirection(model) {
19
+ return model.nodes.some((n) => n.type === 'activity-frame') ? 'RIGHT' : 'DOWN';
20
+ }