@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 +34 -0
- package/src/index.ts +17 -0
- package/src/layout/layout.ts +414 -0
- package/src/layout/measure.ts +260 -0
- package/src/mdx/arc.tsx +139 -0
- package/src/mdx/contexts.tsx +57 -0
- package/src/mdx/mdx-view.tsx +131 -0
- package/src/mdx/ref-context.tsx +57 -0
- package/src/mdx/structure.tsx +206 -0
- package/src/model/build.ts +470 -0
- package/src/model/collapse.ts +205 -0
- package/src/model/profile.ts +95 -0
- package/src/model/types.ts +231 -0
- package/src/profiles/arc.ts +332 -0
- package/src/react/context-card.tsx +112 -0
- package/src/react/index.ts +42 -0
- package/src/react/nodes.tsx +311 -0
- package/src/react/process-diagram.tsx +295 -0
- package/src/react/theme.ts +36 -0
- package/src/state/aggregate.ts +54 -0
- package/src/state/store.ts +55 -0
- package/src/state/view-state.ts +88 -0
- package/tests/arc-profile.test.ts +56 -0
- package/tests/build.test.ts +345 -0
- package/tests/layout.test.ts +244 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strukturalne komponenty MDX przepływów. Renderują lekki znacznik w prozie
|
|
3
|
+
* ORAZ nadają zagnieżdżonym wystąpieniom ścieżkę strukturalną (przez context),
|
|
4
|
+
* którą model zamienia na depth (czas) × lane (tor).
|
|
5
|
+
*
|
|
6
|
+
* <Parallel> — dzieci biegną równolegle (AND); wspólny depth, lane'y
|
|
7
|
+
* <Branch><Lane>…</Lane> — ścieżki alternatywne (XOR); lane'y branchy
|
|
8
|
+
* <Step id title> — nazwana faza; `id` czyni ją celem dla <Continue>
|
|
9
|
+
* <Entry title> — punkt wejścia procesu (subproces poza osią)
|
|
10
|
+
* <Continue to="faza"/> — wpięcie bieżącego Entry w fazę osi głównej
|
|
11
|
+
* <Note side="N">…</Note> — notatka doczepiana do poprzedzającego kroku
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Children, isValidElement, useId, type ReactNode } from "react";
|
|
15
|
+
import type { Direction, PathSeg } from "../model/types";
|
|
16
|
+
import { StructureContext, useRegister, useStructure } from "./contexts";
|
|
17
|
+
import { GLASS } from "../react/theme";
|
|
18
|
+
|
|
19
|
+
/** Owiń każde dziecko segmentem strukturalnym + indeksem toru. */
|
|
20
|
+
function mapLanes(
|
|
21
|
+
children: ReactNode,
|
|
22
|
+
base: PathSeg[],
|
|
23
|
+
seg: (i: number) => PathSeg,
|
|
24
|
+
): ReactNode {
|
|
25
|
+
let i = 0;
|
|
26
|
+
return Children.map(children, (child) => {
|
|
27
|
+
if (!isValidElement(child)) return child;
|
|
28
|
+
const s = seg(i++);
|
|
29
|
+
return (
|
|
30
|
+
<StructureContext.Provider value={{ path: [...base, s] }}>
|
|
31
|
+
{child}
|
|
32
|
+
</StructureContext.Provider>
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const wrapStyle: React.CSSProperties = {
|
|
38
|
+
display: "block",
|
|
39
|
+
borderLeft: `3px solid ${GLASS.border}`,
|
|
40
|
+
paddingLeft: 12,
|
|
41
|
+
margin: "8px 0",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const tagStyle = (color: string): React.CSSProperties => ({
|
|
45
|
+
display: "inline-block",
|
|
46
|
+
fontSize: 10,
|
|
47
|
+
fontWeight: 700,
|
|
48
|
+
textTransform: "uppercase",
|
|
49
|
+
letterSpacing: 0.5,
|
|
50
|
+
color,
|
|
51
|
+
background: "rgba(124,58,237,0.08)",
|
|
52
|
+
borderRadius: 5,
|
|
53
|
+
padding: "1px 7px",
|
|
54
|
+
marginBottom: 4,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export function Parallel({ children }: { children?: ReactNode }) {
|
|
58
|
+
const id = useId();
|
|
59
|
+
const { path } = useStructure();
|
|
60
|
+
return (
|
|
61
|
+
<span style={{ ...wrapStyle, borderLeftColor: "#7c3aed" }}>
|
|
62
|
+
<span style={tagStyle("#7c3aed")}>∥ równolegle</span>
|
|
63
|
+
{mapLanes(children, path, (i) => ({ type: "parallel", id, laneIndex: i }))}
|
|
64
|
+
</span>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function Branch({ children }: { children?: ReactNode }) {
|
|
69
|
+
const id = useId();
|
|
70
|
+
const { path } = useStructure();
|
|
71
|
+
return (
|
|
72
|
+
<span style={{ ...wrapStyle, borderLeftColor: "#e64980" }}>
|
|
73
|
+
<span style={tagStyle("#e64980")}>⋔ alternatywnie</span>
|
|
74
|
+
{mapLanes(children, path, (i) => ({ type: "branch", id, laneIndex: i }))}
|
|
75
|
+
</span>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function Lane({ title, children }: { title?: string; children?: ReactNode }) {
|
|
80
|
+
const id = useId();
|
|
81
|
+
const { path } = useStructure();
|
|
82
|
+
return (
|
|
83
|
+
<StructureContext.Provider
|
|
84
|
+
value={{ path: [...path, { type: "lane", id, title, alt: true }] }}
|
|
85
|
+
>
|
|
86
|
+
<span style={{ display: "block", margin: "4px 0" }}>
|
|
87
|
+
{title ? (
|
|
88
|
+
<span style={{ fontSize: 11, fontWeight: 700, color: "#e64980", marginRight: 6 }}>
|
|
89
|
+
{title}:
|
|
90
|
+
</span>
|
|
91
|
+
) : null}
|
|
92
|
+
{children}
|
|
93
|
+
</span>
|
|
94
|
+
</StructureContext.Provider>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function Step({
|
|
99
|
+
id: phaseId,
|
|
100
|
+
title,
|
|
101
|
+
children,
|
|
102
|
+
}: {
|
|
103
|
+
/** Identyfikator fazy — cel dla `<Continue to>`. */
|
|
104
|
+
id?: string;
|
|
105
|
+
title?: string;
|
|
106
|
+
children?: ReactNode;
|
|
107
|
+
}) {
|
|
108
|
+
const id = useId();
|
|
109
|
+
const { path } = useStructure();
|
|
110
|
+
return (
|
|
111
|
+
<StructureContext.Provider
|
|
112
|
+
value={{ path: [...path, { type: "step", id, title, phaseId }] }}
|
|
113
|
+
>
|
|
114
|
+
<span style={{ ...wrapStyle, borderLeftColor: "#1c7ed6" }}>
|
|
115
|
+
{title ? <span style={tagStyle("#1c7ed6")}>▸ {title}</span> : null}
|
|
116
|
+
{children}
|
|
117
|
+
</span>
|
|
118
|
+
</StructureContext.Provider>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function Entry({ title, children }: { title?: string; children?: ReactNode }) {
|
|
123
|
+
const id = useId();
|
|
124
|
+
const { path } = useStructure();
|
|
125
|
+
return (
|
|
126
|
+
<StructureContext.Provider
|
|
127
|
+
value={{ path: [...path, { type: "entry", id, title }] }}
|
|
128
|
+
>
|
|
129
|
+
<span style={{ ...wrapStyle, borderLeftColor: "#0ca678" }}>
|
|
130
|
+
<span style={tagStyle("#0ca678")}>⇥ wejście{title ? `: ${title}` : ""}</span>
|
|
131
|
+
{children}
|
|
132
|
+
</span>
|
|
133
|
+
</StructureContext.Provider>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function Continue({ to }: { to: string }) {
|
|
138
|
+
const domId = useId();
|
|
139
|
+
const register = useRegister();
|
|
140
|
+
const { path } = useStructure();
|
|
141
|
+
register?.({ domId, occurrence: { type: "continue", to, path } });
|
|
142
|
+
return (
|
|
143
|
+
<span
|
|
144
|
+
data-arc-id={domId}
|
|
145
|
+
style={{
|
|
146
|
+
display: "inline-flex",
|
|
147
|
+
alignItems: "center",
|
|
148
|
+
gap: 4,
|
|
149
|
+
fontSize: 11,
|
|
150
|
+
fontWeight: 700,
|
|
151
|
+
color: "#0ca678",
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
⤳ dalej: {to}
|
|
155
|
+
</span>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function textOf(node: ReactNode): string {
|
|
160
|
+
if (node == null || node === false) return "";
|
|
161
|
+
if (typeof node === "string" || typeof node === "number") return String(node);
|
|
162
|
+
if (Array.isArray(node)) return node.map(textOf).join("");
|
|
163
|
+
if (typeof node === "object" && "props" in (node as unknown as Record<string, unknown>)) {
|
|
164
|
+
return textOf(
|
|
165
|
+
(node as unknown as { props?: { children?: ReactNode } }).props?.children,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return "";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function Note({
|
|
172
|
+
side = "N",
|
|
173
|
+
title,
|
|
174
|
+
children,
|
|
175
|
+
}: {
|
|
176
|
+
side?: Direction;
|
|
177
|
+
title?: string;
|
|
178
|
+
children?: ReactNode;
|
|
179
|
+
}) {
|
|
180
|
+
const domId = useId();
|
|
181
|
+
const register = useRegister();
|
|
182
|
+
const { path } = useStructure();
|
|
183
|
+
const text = textOf(children);
|
|
184
|
+
register?.({
|
|
185
|
+
domId,
|
|
186
|
+
occurrence: { type: "note", occId: domId, text, side, title, path },
|
|
187
|
+
});
|
|
188
|
+
return (
|
|
189
|
+
<span
|
|
190
|
+
data-arc-id={domId}
|
|
191
|
+
style={{
|
|
192
|
+
display: "block",
|
|
193
|
+
margin: "6px 0",
|
|
194
|
+
padding: "6px 10px",
|
|
195
|
+
borderRadius: 8,
|
|
196
|
+
background: "rgba(234,179,8,0.10)",
|
|
197
|
+
borderLeft: "3px solid #eab308",
|
|
198
|
+
fontSize: 12.5,
|
|
199
|
+
color: GLASS.textMuted,
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
{title ? <b style={{ color: GLASS.text }}>{title}: </b> : "📝 "}
|
|
203
|
+
{children}
|
|
204
|
+
</span>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budowa modelu procesu z uporządkowanych wystąpień MDX + generycznego grafu.
|
|
3
|
+
*
|
|
4
|
+
* - steps: jeden nod NA wystąpienie (bez dedup); istniejący lub brakujący.
|
|
5
|
+
* - connectors: nody na najkrótszej SKIEROWANEJ ścieżce przepływu między
|
|
6
|
+
* dwoma opowiedzianymi krokami (hydraulika, której autor nie narrował).
|
|
7
|
+
* - attachments per krok: powiązane nody NIEwidoczne, pogrupowane wg grup
|
|
8
|
+
* doczepień profilu — gotowe do zwinięcia w badge / rozwinięcia w kolumny.
|
|
9
|
+
* - fazy (`<Step id>`) + wejścia (`<Entry>` … `<Continue to>`): subprocesy
|
|
10
|
+
* wpinające się w oś główną, także w jej środek.
|
|
11
|
+
*
|
|
12
|
+
* Czysty i bez zależności (testowalny bunem).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ProcessProfile } from "./profile";
|
|
16
|
+
import {
|
|
17
|
+
CONTEXT_REF_KIND,
|
|
18
|
+
type ContextInfo,
|
|
19
|
+
type ContinueEdge,
|
|
20
|
+
type MethodRef,
|
|
21
|
+
type NoteOccurrence,
|
|
22
|
+
type Occurrence,
|
|
23
|
+
type PathSeg,
|
|
24
|
+
type ProcessConnector,
|
|
25
|
+
type ProcessGraph,
|
|
26
|
+
type ProcessModel,
|
|
27
|
+
type ProcessNode,
|
|
28
|
+
type ProcessRelation,
|
|
29
|
+
type ProcessStep,
|
|
30
|
+
type StepOccurrence,
|
|
31
|
+
} from "./types";
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_MAX_PATH = 5;
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Graph helpers (skierowana adjacencja przepływu + BFS)
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Skierowana adjacencja przepływu lewo→prawo: każda krawędź wskazuje
|
|
41
|
+
* producent→konsument, więc ścieżka BFS podąża za realnym przepływem danych.
|
|
42
|
+
* Orientację wyznacza semantyka relacji z profilu:
|
|
43
|
+
* - "out" (mutates/emits): source produkuje target → source → target
|
|
44
|
+
* - "in" (queries/listens/handles): source konsumuje target → target → source
|
|
45
|
+
*/
|
|
46
|
+
function flowAdjacency(
|
|
47
|
+
relations: ProcessRelation[],
|
|
48
|
+
profile: ProcessProfile,
|
|
49
|
+
): Map<string, Set<string>> {
|
|
50
|
+
const adj = new Map<string, Set<string>>();
|
|
51
|
+
const link = (a: string, b: string) => {
|
|
52
|
+
let s = adj.get(a);
|
|
53
|
+
if (!s) adj.set(a, (s = new Set()));
|
|
54
|
+
s.add(b);
|
|
55
|
+
};
|
|
56
|
+
for (const r of relations) {
|
|
57
|
+
const semantics = profile.relations[r.kind]?.semantics ?? "in";
|
|
58
|
+
if (semantics === "out") link(r.from, r.to);
|
|
59
|
+
else link(r.to, r.from);
|
|
60
|
+
}
|
|
61
|
+
return adj;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function shortestPath(
|
|
65
|
+
adj: Map<string, Set<string>>,
|
|
66
|
+
start: string,
|
|
67
|
+
goal: string,
|
|
68
|
+
maxLen: number,
|
|
69
|
+
): string[] | null {
|
|
70
|
+
if (start === goal) return [start];
|
|
71
|
+
const prev = new Map<string, string>();
|
|
72
|
+
const depth = new Map<string, number>([[start, 0]]);
|
|
73
|
+
const queue = [start];
|
|
74
|
+
while (queue.length) {
|
|
75
|
+
const cur = queue.shift()!;
|
|
76
|
+
const d = depth.get(cur)!;
|
|
77
|
+
if (d >= maxLen) continue;
|
|
78
|
+
for (const nb of adj.get(cur) ?? []) {
|
|
79
|
+
if (depth.has(nb)) continue;
|
|
80
|
+
depth.set(nb, d + 1);
|
|
81
|
+
prev.set(nb, cur);
|
|
82
|
+
if (nb === goal) {
|
|
83
|
+
const path = [goal];
|
|
84
|
+
let p = cur;
|
|
85
|
+
while (p !== start) {
|
|
86
|
+
path.push(p);
|
|
87
|
+
p = prev.get(p)!;
|
|
88
|
+
}
|
|
89
|
+
path.push(start);
|
|
90
|
+
return path.reverse();
|
|
91
|
+
}
|
|
92
|
+
queue.push(nb);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Fazy i wejścia (Entry/Continue) — przydział depth × lane
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
interface EntryBlock {
|
|
103
|
+
id: string;
|
|
104
|
+
title?: string;
|
|
105
|
+
/** Indeksy kroków (w tablicy steps) należące do wejścia, w kolejności. */
|
|
106
|
+
stepIdx: number[];
|
|
107
|
+
/** Cel `<Continue to>`, jeśli zadeklarowany. */
|
|
108
|
+
to?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function entrySeg(path: PathSeg[]): PathSeg | undefined {
|
|
112
|
+
return path.find((s) => s.type === "entry");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Przydziel depth (czas, x) i lane (tor, y).
|
|
117
|
+
*
|
|
118
|
+
* Przebieg 1 — oś główna (kroki poza `<Entry>`): sekwencja + bloki
|
|
119
|
+
* Parallel/Branch jak dotychczas; przy okazji mapa faz `phaseStart`
|
|
120
|
+
* (pierwsza głębokość i occId pierwszego kroku każdej fazy `<Step id>`).
|
|
121
|
+
*
|
|
122
|
+
* Przebieg 2 — wejścia: każdy blok `<Entry>` to łańcuch o długości L z celem
|
|
123
|
+
* `<Continue to=T>`; kroki dostają depth = start(T) − L + i (clamp ≥ 0) oraz
|
|
124
|
+
* tor spoza pasma głównego — wejścia naprzemiennie nad osią (lane ujemne)
|
|
125
|
+
* i pod nią (> maxLane). Ostatni krok wejścia dostaje krawędź „continues”
|
|
126
|
+
* do pierwszego kroku fazy docelowej.
|
|
127
|
+
*/
|
|
128
|
+
function assignDepthLane(
|
|
129
|
+
steps: ProcessStep[],
|
|
130
|
+
stepOccs: StepOccurrence[],
|
|
131
|
+
continues: { to: string; entryId?: string }[],
|
|
132
|
+
): ContinueEdge[] {
|
|
133
|
+
// --- Przebieg 1: oś główna -----------------------------------------------
|
|
134
|
+
let depthCursor = 0;
|
|
135
|
+
let block: {
|
|
136
|
+
id: string;
|
|
137
|
+
base: number;
|
|
138
|
+
maxLocal: number;
|
|
139
|
+
laneDepth: Map<number, number>;
|
|
140
|
+
} | null = null;
|
|
141
|
+
const finalize = () => {
|
|
142
|
+
if (block) {
|
|
143
|
+
depthCursor = block.base + block.maxLocal + 1;
|
|
144
|
+
block = null;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const phaseStart = new Map<string, { depth: number; occId: string }>();
|
|
149
|
+
let firstPhase: { depth: number; occId: string } | null = null;
|
|
150
|
+
const entries = new Map<string, EntryBlock>();
|
|
151
|
+
const entryOrder: string[] = [];
|
|
152
|
+
|
|
153
|
+
steps.forEach((step, i) => {
|
|
154
|
+
const path = stepOccs[i].path ?? [];
|
|
155
|
+
const E = entrySeg(path);
|
|
156
|
+
if (E) {
|
|
157
|
+
// Kroki wejścia zbieramy do przebiegu 2.
|
|
158
|
+
let e = entries.get(E.id);
|
|
159
|
+
if (!e) {
|
|
160
|
+
e = { id: E.id, title: E.title, stepIdx: [] };
|
|
161
|
+
entries.set(E.id, e);
|
|
162
|
+
entryOrder.push(E.id);
|
|
163
|
+
}
|
|
164
|
+
e.stepIdx.push(i);
|
|
165
|
+
step.entryId = E.id;
|
|
166
|
+
step.entryTitle = E.title;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const phase = path.find((s) => s.type === "step" && s.phaseId);
|
|
171
|
+
const P = path.find((s) => s.type === "parallel" || s.type === "branch");
|
|
172
|
+
if (!P) {
|
|
173
|
+
finalize();
|
|
174
|
+
step.depth = depthCursor;
|
|
175
|
+
step.lane = 0;
|
|
176
|
+
depthCursor += 1;
|
|
177
|
+
} else {
|
|
178
|
+
if (!block || block.id !== P.id) {
|
|
179
|
+
finalize();
|
|
180
|
+
block = { id: P.id, base: depthCursor, maxLocal: 0, laneDepth: new Map() };
|
|
181
|
+
}
|
|
182
|
+
const lane = P.laneIndex ?? 0;
|
|
183
|
+
const local = block.laneDepth.get(lane) ?? 0;
|
|
184
|
+
step.depth = block.base + local;
|
|
185
|
+
step.lane = lane;
|
|
186
|
+
step.alt = P.type === "branch";
|
|
187
|
+
step.blockId = P.id;
|
|
188
|
+
block.laneDepth.set(lane, local + 1);
|
|
189
|
+
block.maxLocal = Math.max(block.maxLocal, local);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (phase?.phaseId) {
|
|
193
|
+
step.phaseId = phase.phaseId;
|
|
194
|
+
if (!phaseStart.has(phase.phaseId)) {
|
|
195
|
+
phaseStart.set(phase.phaseId, { depth: step.depth, occId: step.occId });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!firstPhase) firstPhase = { depth: step.depth, occId: step.occId };
|
|
199
|
+
});
|
|
200
|
+
finalize();
|
|
201
|
+
|
|
202
|
+
// Cele Continue → bloki Entry (marker niesie id swojego Entry w ścieżce).
|
|
203
|
+
for (const c of continues) {
|
|
204
|
+
if (!c.entryId) continue;
|
|
205
|
+
const e = entries.get(c.entryId);
|
|
206
|
+
if (e && e.to === undefined) e.to = c.to;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- Przebieg 2: wejścia ---------------------------------------------------
|
|
210
|
+
const continueEdges: ContinueEdge[] = [];
|
|
211
|
+
const mainLanes = steps
|
|
212
|
+
.filter((s) => !s.entryId)
|
|
213
|
+
.map((s) => s.lane);
|
|
214
|
+
const maxMainLane = mainLanes.length ? Math.max(...mainLanes) : 0;
|
|
215
|
+
let aboveCount = 0;
|
|
216
|
+
let belowCount = 0;
|
|
217
|
+
|
|
218
|
+
entryOrder.forEach((entryId, k) => {
|
|
219
|
+
const e = entries.get(entryId)!;
|
|
220
|
+
const target = (e.to !== undefined ? phaseStart.get(e.to) : undefined) ?? firstPhase;
|
|
221
|
+
const targetDepth = target?.depth ?? 0;
|
|
222
|
+
const L = e.stepIdx.length;
|
|
223
|
+
const start = Math.max(0, targetDepth - L);
|
|
224
|
+
const above = k % 2 === 0;
|
|
225
|
+
const lane = above ? -(1 + aboveCount++) : maxMainLane + 1 + belowCount++;
|
|
226
|
+
e.stepIdx.forEach((idx, i) => {
|
|
227
|
+
steps[idx].depth = start + i;
|
|
228
|
+
steps[idx].lane = lane;
|
|
229
|
+
});
|
|
230
|
+
if (target && e.to !== undefined && L > 0) {
|
|
231
|
+
continueEdges.push({
|
|
232
|
+
fromOcc: steps[e.stepIdx[L - 1]].occId,
|
|
233
|
+
toOcc: target.occId,
|
|
234
|
+
phaseId: e.to,
|
|
235
|
+
});
|
|
236
|
+
} else if (target && L > 0) {
|
|
237
|
+
// Entry bez Continue → wpięcie w pierwszą fazę dokumentu.
|
|
238
|
+
continueEdges.push({
|
|
239
|
+
fromOcc: steps[e.stepIdx[L - 1]].occId,
|
|
240
|
+
toOcc: target.occId,
|
|
241
|
+
phaseId: "",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return continueEdges;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Public builder
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
export interface BuildModelInput {
|
|
254
|
+
graph: ProcessGraph;
|
|
255
|
+
profile: ProcessProfile;
|
|
256
|
+
occurrences: Occurrence[];
|
|
257
|
+
maxPath?: number;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Znajdź kontekst po referencji `<Arc context="ai.chat">` (nazwy kropkowane) lub id. */
|
|
261
|
+
export function resolveContextRef(
|
|
262
|
+
contexts: ContextInfo[],
|
|
263
|
+
refId: string,
|
|
264
|
+
): ContextInfo | undefined {
|
|
265
|
+
const byId = contexts.find((c) => c.id === refId);
|
|
266
|
+
if (byId) return byId;
|
|
267
|
+
// Ścieżka kropkowana po nazwach: "ai.chat" → kontekst o name "chat",
|
|
268
|
+
// którego przodkowie mają nazwy "ai" (sufiks ścieżki nazw).
|
|
269
|
+
const segs = refId.split(".");
|
|
270
|
+
const nameOf = (id: string) =>
|
|
271
|
+
contexts.find((c) => c.id === id)?.name ?? id.replace(/^ctx_/, "");
|
|
272
|
+
return contexts.find((c) => {
|
|
273
|
+
const namePath = c.path.map(nameOf);
|
|
274
|
+
if (segs.length > namePath.length) return false;
|
|
275
|
+
return segs.every((s, i) => namePath[namePath.length - segs.length + i] === s);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Syntetyczny nod dla referencji całego kontekstu. */
|
|
280
|
+
function contextNode(ctx: ContextInfo): ProcessNode {
|
|
281
|
+
return {
|
|
282
|
+
id: `ctx:${ctx.id}`,
|
|
283
|
+
kind: CONTEXT_REF_KIND,
|
|
284
|
+
label: ctx.title ?? ctx.name ?? ctx.id,
|
|
285
|
+
contextPath: ctx.path,
|
|
286
|
+
meta: { context: ctx },
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Indeks relacji + doczepienia (reużywane przez layout do rekurencyjnych
|
|
292
|
+
// rozwinięć doczepionych karteczek)
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
export interface RelationIndex {
|
|
296
|
+
nodeById: Map<string, ProcessNode>;
|
|
297
|
+
outByNode: Map<string, ProcessRelation[]>;
|
|
298
|
+
inByNode: Map<string, ProcessRelation[]>;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function buildRelationIndex(graph: ProcessGraph): RelationIndex {
|
|
302
|
+
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
303
|
+
const outByNode = new Map<string, ProcessRelation[]>();
|
|
304
|
+
const inByNode = new Map<string, ProcessRelation[]>();
|
|
305
|
+
for (const r of graph.relations) {
|
|
306
|
+
let out = outByNode.get(r.from);
|
|
307
|
+
if (!out) outByNode.set(r.from, (out = []));
|
|
308
|
+
out.push(r);
|
|
309
|
+
let inn = inByNode.get(r.to);
|
|
310
|
+
if (!inn) inByNode.set(r.to, (inn = []));
|
|
311
|
+
inn.push(r);
|
|
312
|
+
}
|
|
313
|
+
return { nodeById, outByNode, inByNode };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Doczepienia noda wg grup profilu: powiązane nody spoza `excluded`,
|
|
318
|
+
* pogrupowane po grupie relacji. `method` pozwala profilowi zawęzić
|
|
319
|
+
* forward-zależności per metoda (np. metoda agregatu Arc).
|
|
320
|
+
*/
|
|
321
|
+
export function attachmentsFor(
|
|
322
|
+
index: RelationIndex,
|
|
323
|
+
profile: ProcessProfile,
|
|
324
|
+
node: ProcessNode,
|
|
325
|
+
method: MethodRef | undefined,
|
|
326
|
+
excluded: ReadonlySet<string>,
|
|
327
|
+
): Partial<Record<string, string[]>> {
|
|
328
|
+
const id = node.id;
|
|
329
|
+
const override = profile.forwardDeps?.(node, method) ?? null;
|
|
330
|
+
const attachments: Partial<Record<string, string[]>> = {};
|
|
331
|
+
for (const [group, spec] of Object.entries(profile.attachmentGroups)) {
|
|
332
|
+
let ids: string[];
|
|
333
|
+
const overridden = override?.[group];
|
|
334
|
+
if (overridden !== undefined) {
|
|
335
|
+
ids = overridden;
|
|
336
|
+
} else {
|
|
337
|
+
const pool =
|
|
338
|
+
spec.direction === "out" ? index.outByNode.get(id) : index.inByNode.get(id);
|
|
339
|
+
ids = (pool ?? [])
|
|
340
|
+
.filter((r) => spec.relations.includes(r.kind))
|
|
341
|
+
.map((r) => (spec.direction === "out" ? r.to : r.from));
|
|
342
|
+
}
|
|
343
|
+
const filtered = [...new Set(ids)].filter(
|
|
344
|
+
(t) => t !== id && !excluded.has(t) && index.nodeById.has(t),
|
|
345
|
+
);
|
|
346
|
+
if (filtered.length) attachments[group] = filtered;
|
|
347
|
+
}
|
|
348
|
+
return attachments;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function buildProcessModel(input: BuildModelInput): ProcessModel {
|
|
352
|
+
const { graph, profile, occurrences, maxPath = DEFAULT_MAX_PATH } = input;
|
|
353
|
+
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
354
|
+
const contexts = graph.contexts ?? [];
|
|
355
|
+
|
|
356
|
+
// Rozdziel wystąpienia: kroki / continue / notatki (kolejność dokumentu).
|
|
357
|
+
const stepOccs: StepOccurrence[] = [];
|
|
358
|
+
const continues: { to: string; entryId?: string }[] = [];
|
|
359
|
+
const notesFor = new Map<string, NoteOccurrence[]>(); // occId kroku → notatki
|
|
360
|
+
let lastStepOcc: StepOccurrence | null = null;
|
|
361
|
+
for (const o of occurrences) {
|
|
362
|
+
if (o.type === "step") {
|
|
363
|
+
stepOccs.push(o);
|
|
364
|
+
lastStepOcc = o;
|
|
365
|
+
} else if (o.type === "continue") {
|
|
366
|
+
continues.push({ to: o.to, entryId: entrySeg(o.path ?? [])?.id });
|
|
367
|
+
} else if (o.type === "note" && lastStepOcc) {
|
|
368
|
+
const list = notesFor.get(lastStepOcc.occId) ?? [];
|
|
369
|
+
list.push(o);
|
|
370
|
+
notesFor.set(lastStepOcc.occId, list);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Rozwiązanie referencji: nody grafu + syntetyczne nody kontekstów.
|
|
375
|
+
const resolve = (nodeId: string, kind: string): ProcessNode | undefined => {
|
|
376
|
+
if (kind === CONTEXT_REF_KIND) {
|
|
377
|
+
const raw = nodeId.startsWith("ctx:") ? nodeId.slice(4) : nodeId;
|
|
378
|
+
const ctx = resolveContextRef(contexts, raw);
|
|
379
|
+
return ctx ? contextNode(ctx) : undefined;
|
|
380
|
+
}
|
|
381
|
+
return nodeById.get(nodeId);
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// 1) Kroki — jeden na wystąpienie.
|
|
385
|
+
const steps: ProcessStep[] = stepOccs.map((o, i) => {
|
|
386
|
+
const node = o.ref ? resolve(o.ref.nodeId, o.ref.kind) : undefined;
|
|
387
|
+
const path = o.path ?? [];
|
|
388
|
+
return {
|
|
389
|
+
occId: o.occId,
|
|
390
|
+
seq: i + 1,
|
|
391
|
+
ref: o.ref,
|
|
392
|
+
exists: !!node,
|
|
393
|
+
node,
|
|
394
|
+
label: o.label,
|
|
395
|
+
method: o.ref?.method,
|
|
396
|
+
attachments: {},
|
|
397
|
+
sides: o.sides,
|
|
398
|
+
expand: o.expand,
|
|
399
|
+
notes: (notesFor.get(o.occId) ?? []).map((n) => ({
|
|
400
|
+
occId: n.occId,
|
|
401
|
+
side: n.side ?? ("N" as const),
|
|
402
|
+
title: n.title,
|
|
403
|
+
text: n.text,
|
|
404
|
+
})),
|
|
405
|
+
depth: 0,
|
|
406
|
+
lane: 0,
|
|
407
|
+
stepTitle: path.find((s) => s.type === "step")?.title,
|
|
408
|
+
laneTitle: path.find((s) => s.type === "lane")?.title,
|
|
409
|
+
};
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const continueEdges = assignDepthLane(steps, stepOccs, continues);
|
|
413
|
+
|
|
414
|
+
// Pierwsze wystąpienie (seq) per istniejący nod.
|
|
415
|
+
const firstSeqByNode = new Map<string, number>();
|
|
416
|
+
for (const s of steps) {
|
|
417
|
+
if (s.exists && !firstSeqByNode.has(s.node!.id)) {
|
|
418
|
+
firstSeqByNode.set(s.node!.id, s.seq);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const stepNodeIds = [...firstSeqByNode.keys()];
|
|
422
|
+
|
|
423
|
+
// 2) Connectory — nody na SKIEROWANEJ ścieżce przepływu między krokami.
|
|
424
|
+
const flowAdj = flowAdjacency(graph.relations, profile);
|
|
425
|
+
const connectorMap = new Map<string, ProcessConnector>();
|
|
426
|
+
for (let i = 0; i < stepNodeIds.length; i++) {
|
|
427
|
+
for (let j = i + 1; j < stepNodeIds.length; j++) {
|
|
428
|
+
const a = stepNodeIds[i];
|
|
429
|
+
const b = stepNodeIds[j];
|
|
430
|
+
let path = shortestPath(flowAdj, a, b, maxPath);
|
|
431
|
+
let from = a;
|
|
432
|
+
let to = b;
|
|
433
|
+
if (!path) {
|
|
434
|
+
path = shortestPath(flowAdj, b, a, maxPath);
|
|
435
|
+
from = b;
|
|
436
|
+
to = a;
|
|
437
|
+
}
|
|
438
|
+
if (!path) continue;
|
|
439
|
+
const intermediates = path.filter((id) => !firstSeqByNode.has(id));
|
|
440
|
+
const between: [number, number] = [
|
|
441
|
+
firstSeqByNode.get(from)!,
|
|
442
|
+
firstSeqByNode.get(to)!,
|
|
443
|
+
];
|
|
444
|
+
intermediates.forEach((id, k) => {
|
|
445
|
+
const node = nodeById.get(id);
|
|
446
|
+
if (!node || connectorMap.has(id)) return;
|
|
447
|
+
connectorMap.set(id, {
|
|
448
|
+
id,
|
|
449
|
+
node,
|
|
450
|
+
between,
|
|
451
|
+
order: k + 1,
|
|
452
|
+
count: intermediates.length,
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const connectors = [...connectorMap.values()];
|
|
458
|
+
|
|
459
|
+
// 3) Zbiór widocznych.
|
|
460
|
+
const visible = new Set<string>([...stepNodeIds, ...connectorMap.keys()]);
|
|
461
|
+
|
|
462
|
+
// 4) Doczepienia per krok — powiązane nody NIEwidoczne, wg grup profilu.
|
|
463
|
+
const index = buildRelationIndex(graph);
|
|
464
|
+
for (const s of steps) {
|
|
465
|
+
if (!s.exists || s.node!.kind === CONTEXT_REF_KIND) continue;
|
|
466
|
+
s.attachments = attachmentsFor(index, profile, s.node!, s.method, visible);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return { steps, connectors, visible, firstSeqByNode, continueEdges };
|
|
470
|
+
}
|