@arcote.tech/arc-process 0.7.28

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/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@arcote.tech/arc-process",
3
+ "type": "module",
4
+ "version": "0.7.28",
5
+ "private": false,
6
+ "description": "Process description & diagram fragment for Arc framework — generic graph model with profiles, MDX flow components and a deterministic 8-direction layout",
7
+ "main": "./src/index.ts",
8
+ "types": "./src/index.ts",
9
+ "exports": {
10
+ ".": "./src/index.ts",
11
+ "./react": "./src/react/index.ts"
12
+ },
13
+ "scripts": {
14
+ "type-check": "tsc --noEmit",
15
+ "test": "bun test"
16
+ },
17
+ "dependencies": {
18
+ "@mdx-js/mdx": "^3.1.0",
19
+ "@xyflow/react": "^12.0.0",
20
+ "remark-frontmatter": "^5.0.0",
21
+ "remark-gfm": "^4.0.0"
22
+ },
23
+ "peerDependencies": {
24
+ "@arcote.tech/arc": "^0.7.28",
25
+ "@arcote.tech/platform": "^0.7.28",
26
+ "react": ">=18.0.0",
27
+ "react-dom": ">=18.0.0",
28
+ "typescript": "^5.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/bun": "latest",
32
+ "@types/react": "^19.0.0"
33
+ }
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @arcote.tech/arc-process — fragment opisu i wizualizacji procesów.
3
+ *
4
+ * Entry React-free: generyczny model grafu, profile, budowa modelu,
5
+ * layout i stan widoku. Komponenty React/MDX: `@arcote.tech/arc-process/react`.
6
+ */
7
+
8
+ export * from "./model/types";
9
+ export * from "./model/profile";
10
+ export * from "./model/build";
11
+ export * from "./model/collapse";
12
+ export * from "./layout/measure";
13
+ export * from "./layout/layout";
14
+ export * from "./state/view-state";
15
+ export * from "./state/store";
16
+ export * from "./state/aggregate";
17
+ export * from "./profiles/arc";
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Pass 2 layoutu — deterministyczne rozmieszczenie 2D.
3
+ *
4
+ * x = depth (czas / kolejność zależności) — kolumny (tracki poziome)
5
+ * y = lane (tor: oś główna 0, wejścia Entry ujemne/za-maks, branch/parallel)
6
+ *
7
+ * Tracki wymiarowane są pasmami z pass 1 (measure) jak w CSS grid: kolumna
8
+ * rezerwuje najszersze pasma W/E swoich kroków, wiersz — najwyższe N/S.
9
+ * Dzięki temu rozwinięcia w 8 kierunkach (wraz z rekurencją) nigdy nie
10
+ * nachodzą na sąsiednie tory — bez solvera, w pełni deterministycznie.
11
+ *
12
+ * Zwinięte konteksty przychodzą już jako pseudo-kroki (model/collapse);
13
+ * relacje do ich wnętrza re-targetowane są na box (dedup krawędzi).
14
+ */
15
+
16
+ import type { CollapseResult } from "../model/collapse";
17
+ import type { ProcessProfile } from "../model/profile";
18
+ import {
19
+ CONTEXT_REF_KIND,
20
+ directionDx,
21
+ directionDy,
22
+ type ProcessGraph,
23
+ type ProcessModel,
24
+ } from "../model/types";
25
+ import type { ProcessViewState } from "../state/view-state";
26
+ import { buildRelationIndex } from "../model/build";
27
+ import {
28
+ CARD_GAP,
29
+ CARD_H,
30
+ CARD_W,
31
+ COL_GAP_X,
32
+ DIAG_GAP_Y,
33
+ LANE_MARGIN,
34
+ MARGIN,
35
+ measureSteps,
36
+ PAD,
37
+ STEP_H,
38
+ STEP_W,
39
+ type ExpansionGroup,
40
+ type ExpansionItem,
41
+ type MeasuredStep,
42
+ } from "./measure";
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Wynik
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export interface DiagramNode {
49
+ id: string;
50
+ type: "step" | "attachment" | "connector" | "note" | "context-group";
51
+ x: number;
52
+ y: number;
53
+ w: number;
54
+ h: number;
55
+ data: Record<string, unknown>;
56
+ }
57
+
58
+ export interface DiagramEdge {
59
+ id: string;
60
+ source: string;
61
+ target: string;
62
+ label?: string;
63
+ color: string;
64
+ dashed?: boolean;
65
+ opacity?: number;
66
+ /** Krawędź szkieletu narracji (kolejność dokumentu). */
67
+ backbone?: boolean;
68
+ }
69
+
70
+ export interface ProcessLayout {
71
+ nodes: DiagramNode[];
72
+ edges: DiagramEdge[];
73
+ }
74
+
75
+ export interface LayoutInput {
76
+ model: ProcessModel;
77
+ collapse: CollapseResult;
78
+ graph: ProcessGraph;
79
+ profile: ProcessProfile;
80
+ state: ProcessViewState;
81
+ }
82
+
83
+ const BACKBONE_COLOR = "#c4b5fd";
84
+ const CONTINUE_COLOR = "#7c3aed";
85
+ const NOTE_COLOR = "#eab308";
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Layout
89
+ // ---------------------------------------------------------------------------
90
+
91
+ export function layoutProcess(input: LayoutInput): ProcessLayout {
92
+ const { model, collapse, graph, profile, state } = input;
93
+ const steps = collapse.steps;
94
+ const nodes: DiagramNode[] = [];
95
+ const edges: DiagramEdge[] = [];
96
+ if (steps.length === 0) return { nodes, edges };
97
+
98
+ const index = buildRelationIndex(graph);
99
+ const measured = measureSteps({
100
+ steps,
101
+ index,
102
+ profile,
103
+ state,
104
+ visible: model.visible,
105
+ });
106
+ const byOcc = new Map(measured.map((m) => [m.step.occId, m]));
107
+
108
+ // --- Track sizing ----------------------------------------------------------
109
+ const maxDepth = Math.max(...steps.map((s) => s.depth));
110
+ const lanes = [...new Set(steps.map((s) => s.lane))].sort((a, b) => a - b);
111
+
112
+ const westAt: number[] = new Array(maxDepth + 1).fill(0);
113
+ const eastAt: number[] = new Array(maxDepth + 1).fill(0);
114
+ const northAt = new Map<number, number>(lanes.map((l) => [l, 0]));
115
+ const southAt = new Map<number, number>(lanes.map((l) => [l, 0]));
116
+ for (const m of measured) {
117
+ const { depth, lane } = m.step;
118
+ westAt[depth] = Math.max(westAt[depth], m.bands.W);
119
+ eastAt[depth] = Math.max(eastAt[depth], m.bands.E);
120
+ northAt.set(lane, Math.max(northAt.get(lane) ?? 0, m.bands.N));
121
+ southAt.set(lane, Math.max(southAt.get(lane) ?? 0, m.bands.S));
122
+ }
123
+
124
+ const xByDepth: number[] = [];
125
+ for (let d = 0; d <= maxDepth; d++) {
126
+ xByDepth[d] =
127
+ d === 0
128
+ ? PAD + westAt[0] + STEP_W / 2
129
+ : xByDepth[d - 1] + STEP_W / 2 + eastAt[d - 1] + MARGIN + westAt[d] + STEP_W / 2;
130
+ }
131
+
132
+ const yByLane = new Map<number, number>();
133
+ let cursorY = PAD;
134
+ for (let i = 0; i < lanes.length; i++) {
135
+ const lane = lanes[i];
136
+ cursorY += (northAt.get(lane) ?? 0) + STEP_H / 2;
137
+ yByLane.set(lane, cursorY);
138
+ cursorY += STEP_H / 2 + (southAt.get(lane) ?? 0) + LANE_MARGIN;
139
+ }
140
+ const bottomY = cursorY;
141
+
142
+ // --- Kroki + rozwinięcia ----------------------------------------------------
143
+ for (const m of measured) {
144
+ const cx = xByDepth[m.step.depth];
145
+ const cy = yByLane.get(m.step.lane)!;
146
+ const s = m.step;
147
+ const isContextBox = s.node?.kind === CONTEXT_REF_KIND;
148
+ nodes.push({
149
+ id: s.occId,
150
+ type: "step",
151
+ x: cx - STEP_W / 2,
152
+ y: cy - STEP_H / 2,
153
+ w: STEP_W,
154
+ h: STEP_H,
155
+ data: {
156
+ step: s,
157
+ expandKey: m.key,
158
+ expanded: state.expanded[m.key] ?? [],
159
+ isContext: isContextBox,
160
+ memberCount: isContextBox ? (s.node?.meta?.memberCount ?? 0) : undefined,
161
+ },
162
+ });
163
+ placeGroups(m.groups, s.occId, cx, cy, STEP_W, STEP_H, nodes, edges);
164
+ }
165
+
166
+ // --- Szkielet narracji -------------------------------------------------------
167
+ for (let i = 1; i < steps.length; i++) {
168
+ const a = steps[i - 1];
169
+ const b = steps[i];
170
+ // Równoległe rodzeństwo (ten sam blok, inne lane) i różne tory Entry
171
+ // nie są sekwencją.
172
+ if (a.blockId && a.blockId === b.blockId && a.lane !== b.lane) continue;
173
+ if (a.entryId !== b.entryId) continue;
174
+ edges.push({
175
+ id: `bb:${i}`,
176
+ source: a.occId,
177
+ target: b.occId,
178
+ color: BACKBONE_COLOR,
179
+ dashed: true,
180
+ opacity: 0.6,
181
+ backbone: true,
182
+ });
183
+ }
184
+
185
+ // --- Krawędzie continues (Entry → faza, skosem) ------------------------------
186
+ const occOf = (occId: string) => collapse.groupByOcc.get(occId) ?? occId;
187
+ const seenContinue = new Set<string>();
188
+ for (const c of model.continueEdges) {
189
+ const from = occOf(c.fromOcc);
190
+ const to = occOf(c.toOcc);
191
+ if (from === to) continue;
192
+ const key = `${from}->${to}`;
193
+ if (seenContinue.has(key)) continue;
194
+ seenContinue.add(key);
195
+ edges.push({
196
+ id: `cont:${key}`,
197
+ source: from,
198
+ target: to,
199
+ label: c.phaseId ? `→ ${c.phaseId}` : undefined,
200
+ color: CONTINUE_COLOR,
201
+ dashed: true,
202
+ opacity: 0.85,
203
+ });
204
+ }
205
+
206
+ // --- Connectory ---------------------------------------------------------------
207
+ const stepDepthByOcc = new Map(steps.map((s) => [s.occId, s.depth]));
208
+ const firstOccByNode = new Map<string, string>();
209
+ for (const s of steps) {
210
+ if (s.exists && s.node && !firstOccByNode.has(s.node.id)) {
211
+ firstOccByNode.set(s.node.id, s.occId);
212
+ }
213
+ }
214
+ const seqToNode = new Map<number, string>();
215
+ for (const s of model.steps) {
216
+ if (s.exists && !seqToNode.has(s.seq)) seqToNode.set(s.seq, s.node!.id);
217
+ }
218
+ const nodeIdFor = (id: string): string | undefined =>
219
+ collapse.groupByNodeId.get(id) ??
220
+ firstOccByNode.get(id) ??
221
+ (model.visible.has(id) ? `conn:${id}` : undefined);
222
+
223
+ const depthByNode = new Map<string, number>();
224
+ for (const s of steps) {
225
+ if (s.exists && s.node && !depthByNode.has(s.node.id)) {
226
+ depthByNode.set(s.node.id, s.depth);
227
+ }
228
+ }
229
+ for (const [nodeId, groupOcc] of collapse.groupByNodeId) {
230
+ const d = stepDepthByOcc.get(groupOcc);
231
+ if (d !== undefined && !depthByNode.has(nodeId)) depthByNode.set(nodeId, d);
232
+ }
233
+
234
+ const connDepth = new Map<string, number>();
235
+ const connY = bottomY + CARD_H / 2;
236
+ const xStack = new Map<number, number>();
237
+ for (const c of model.connectors) {
238
+ // Connector wewnątrz zwiniętego boxa nie istnieje na diagramie.
239
+ if (collapse.groupByNodeId.has(c.id)) continue;
240
+ const aNode = seqToNode.get(c.between[0]) ?? "";
241
+ const bNode = seqToNode.get(c.between[1]) ?? "";
242
+ const da = depthByNode.get(aNode) ?? 0;
243
+ const db = depthByNode.get(bNode) ?? da;
244
+ const frac = c.order / (c.count + 1);
245
+ const x = (xByDepth[da] ?? PAD) + ((xByDepth[db] ?? PAD) - (xByDepth[da] ?? PAD)) * frac;
246
+ const key = Math.round(x);
247
+ const idx = xStack.get(key) ?? 0;
248
+ xStack.set(key, idx + 1);
249
+ const y = connY + idx * (CARD_H + CARD_GAP);
250
+ connDepth.set(`conn:${c.id}`, da + (db - da) * frac);
251
+ nodes.push({
252
+ id: `conn:${c.id}`,
253
+ type: "connector",
254
+ x: x - CARD_W / 2,
255
+ y,
256
+ w: CARD_W,
257
+ h: CARD_H,
258
+ data: { node: c.node },
259
+ });
260
+ }
261
+
262
+ const depthOf = (occId: string): number =>
263
+ connDepth.get(occId) ?? stepDepthByOcc.get(occId) ?? 0;
264
+
265
+ // --- Krawędzie relacji (orientacja wg semantyki profilu) ----------------------
266
+ const seenEdge = new Set<string>();
267
+ for (const r of graph.relations) {
268
+ const sId = nodeIdFor(r.from);
269
+ const tId = nodeIdFor(r.to);
270
+ if (!sId || !tId || sId === tId) continue;
271
+ const spec = profile.relations[r.kind];
272
+ const semantics = spec?.semantics ?? "in";
273
+ const from = semantics === "in" ? tId : sId;
274
+ const to = semantics === "in" ? sId : tId;
275
+ if (semantics === "out" && depthOf(to) < depthOf(from)) continue; // wsteczne wyjście → szum
276
+ const key = `${from}->${to}:${r.kind}`;
277
+ if (seenEdge.has(key)) continue;
278
+ seenEdge.add(key);
279
+ edges.push({
280
+ id: `rel:${key}`,
281
+ source: from,
282
+ target: to,
283
+ label: spec?.label ?? r.kind,
284
+ color: spec?.color ?? "#868e96",
285
+ dashed: spec?.dashed,
286
+ opacity: 0.75,
287
+ });
288
+ }
289
+
290
+ // --- Otwarte boxy kontekstów ---------------------------------------------------
291
+ // Kontekst jawnie otwarty przez czytelnika → obrys wokół jego kroków.
292
+ const openContexts = Object.entries(state.collapsedContexts)
293
+ .filter(([, collapsed]) => !collapsed)
294
+ .map(([id]) => id);
295
+ for (const ctxId of openContexts) {
296
+ const ctx = (graph.contexts ?? []).find((c) => c.id === ctxId);
297
+ if (!ctx) continue;
298
+ const members = nodes.filter((n) => {
299
+ if (n.type !== "step") return false;
300
+ const path = (n.data.step as { node?: { contextPath?: string[] } }).node
301
+ ?.contextPath;
302
+ return !!path?.includes(ctxId);
303
+ });
304
+ if (!members.length) continue;
305
+ const minX = Math.min(...members.map((n) => n.x)) - 28;
306
+ const minY = Math.min(...members.map((n) => n.y)) - 46;
307
+ const maxX = Math.max(...members.map((n) => n.x + n.w)) + 28;
308
+ const maxY = Math.max(...members.map((n) => n.y + n.h)) + 28;
309
+ nodes.unshift({
310
+ id: `ctxbox:${ctxId}`,
311
+ type: "context-group",
312
+ x: minX,
313
+ y: minY,
314
+ w: maxX - minX,
315
+ h: maxY - minY,
316
+ data: { context: ctx, open: true },
317
+ });
318
+ }
319
+
320
+ return { nodes, edges };
321
+ }
322
+
323
+ // ---------------------------------------------------------------------------
324
+ // Rozmieszczenie rozwinięć wokół węzła (rekurencyjne)
325
+ // ---------------------------------------------------------------------------
326
+
327
+ function placeGroups(
328
+ groups: ExpansionGroup[],
329
+ parentId: string,
330
+ cx: number,
331
+ cy: number,
332
+ parentW: number,
333
+ parentH: number,
334
+ nodes: DiagramNode[],
335
+ edges: DiagramEdge[],
336
+ ): void {
337
+ for (const g of groups) {
338
+ const dx = directionDx(g.side);
339
+ const dy = directionDy(g.side);
340
+ if (dx !== 0) {
341
+ // Kolumna przy W/E (skos: w całości nad/pod nodem).
342
+ const colX = cx + dx * COL_GAP_X;
343
+ let y: number;
344
+ if (dy < 0) y = cy - parentH / 2 - DIAG_GAP_Y - g.slotH;
345
+ else if (dy > 0) y = cy + parentH / 2 + DIAG_GAP_Y;
346
+ else y = cy - g.slotH / 2;
347
+ placeColumn(g, colX, y, parentId, nodes, edges);
348
+ } else {
349
+ // Wiersz N/S.
350
+ const rowY =
351
+ dy < 0 ? cy - parentH / 2 - DIAG_GAP_Y - g.slotH : cy + parentH / 2 + DIAG_GAP_Y;
352
+ let x = cx - g.slotW / 2;
353
+ for (const it of g.items) {
354
+ x += it.bands.W;
355
+ emitItem(it, g, x + it.w / 2, rowY + it.bands.N + it.h / 2, parentId, nodes, edges);
356
+ x += it.w + it.bands.E + CARD_GAP;
357
+ }
358
+ }
359
+ }
360
+ }
361
+
362
+ function placeColumn(
363
+ g: ExpansionGroup,
364
+ colX: number,
365
+ topY: number,
366
+ parentId: string,
367
+ nodes: DiagramNode[],
368
+ edges: DiagramEdge[],
369
+ ): void {
370
+ let y = topY;
371
+ for (const it of g.items) {
372
+ y += it.bands.N;
373
+ emitItem(it, g, colX, y + it.h / 2, parentId, nodes, edges);
374
+ y += it.h + it.bands.S + CARD_GAP;
375
+ }
376
+ }
377
+
378
+ function emitItem(
379
+ it: ExpansionItem,
380
+ g: ExpansionGroup,
381
+ cx: number,
382
+ cy: number,
383
+ parentId: string,
384
+ nodes: DiagramNode[],
385
+ edges: DiagramEdge[],
386
+ ): void {
387
+ const isNote = it.node.kind === "note";
388
+ const nodeId = `att:${it.key}`;
389
+ nodes.push({
390
+ id: nodeId,
391
+ type: isNote ? "note" : "attachment",
392
+ x: cx - it.w / 2,
393
+ y: cy - it.h / 2,
394
+ w: it.w,
395
+ h: it.h,
396
+ data: {
397
+ node: it.node,
398
+ group: g.group,
399
+ expandKey: it.key,
400
+ expanded: it.groups.map((cg) => cg.group),
401
+ attachments: it.attachments,
402
+ },
403
+ });
404
+ edges.push({
405
+ id: `att:${nodeId}`,
406
+ source: g.spec.intoNode ? nodeId : parentId,
407
+ target: g.spec.intoNode ? parentId : nodeId,
408
+ color: isNote ? NOTE_COLOR : g.spec.color,
409
+ dashed: isNote,
410
+ opacity: 0.9,
411
+ });
412
+ // Rekurencja: rozwinięcia karteczki względem niej samej.
413
+ placeGroups(it.groups, nodeId, cx, cy, it.w, it.h, nodes, edges);
414
+ }