@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,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persystencja stanu widoku diagramu.
|
|
3
|
+
*
|
|
4
|
+
* `ProcessViewStore` to wspólny interfejs — fragment dostarcza:
|
|
5
|
+
* - {@link localStorageStore} — fallback bez scope (klucz `arc-process:<docId>`),
|
|
6
|
+
* - {@link functionStore} — adapter dla persystencji aplikacyjnej (np. agregat
|
|
7
|
+
* z {@link createProcessViewAggregate} — aplikacja wiąże load/save ze swoim
|
|
8
|
+
* scope w jednej linii),
|
|
9
|
+
* - {@link memoryStore} — ulotny (testy, SSR).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ProcessViewState } from "./view-state";
|
|
13
|
+
|
|
14
|
+
export interface ProcessViewStore {
|
|
15
|
+
load(docId: string): ProcessViewState | null;
|
|
16
|
+
save(docId: string, state: ProcessViewState): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Persystencja w localStorage — działa wszędzie, bez backendu i scope. */
|
|
20
|
+
export function localStorageStore(prefix = "arc-process"): ProcessViewStore {
|
|
21
|
+
return {
|
|
22
|
+
load(docId) {
|
|
23
|
+
try {
|
|
24
|
+
const raw = globalThis.localStorage?.getItem(`${prefix}:${docId}`);
|
|
25
|
+
return raw ? (JSON.parse(raw) as ProcessViewState) : null;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
save(docId, state) {
|
|
31
|
+
try {
|
|
32
|
+
globalThis.localStorage?.setItem(`${prefix}:${docId}`, JSON.stringify(state));
|
|
33
|
+
} catch {
|
|
34
|
+
// localStorage niedostępny (SSR / private mode) — stan pozostaje ulotny.
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Adapter funkcyjny — spina store z dowolną persystencją (agregat, API). */
|
|
41
|
+
export function functionStore(
|
|
42
|
+
load: (docId: string) => ProcessViewState | null,
|
|
43
|
+
save: (docId: string, state: ProcessViewState) => void,
|
|
44
|
+
): ProcessViewStore {
|
|
45
|
+
return { load, save };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Store w pamięci — do testów i renderu bez persystencji. */
|
|
49
|
+
export function memoryStore(): ProcessViewStore {
|
|
50
|
+
const mem = new Map<string, ProcessViewState>();
|
|
51
|
+
return {
|
|
52
|
+
load: (docId) => mem.get(docId) ?? null,
|
|
53
|
+
save: (docId, state) => void mem.set(docId, state),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stan widoku diagramu procesu — otwarcia rozwinięć i zwinięcia kontekstów.
|
|
3
|
+
*
|
|
4
|
+
* Rozwinięcia adresowane są ŚCIEŻKĄ rozwinięcia ({@link ExpansionKey}):
|
|
5
|
+
* korzeń to occId kroku ("occ:o12"), dzieci doklejają segment
|
|
6
|
+
* "grupa:nodId" ("occ:o12/mutates:orderPaid") — dzięki temu każda
|
|
7
|
+
* doczepiona karteczka może rekurencyjnie otwierać własne relacje.
|
|
8
|
+
*
|
|
9
|
+
* React-free — reducer + budowa stanu startowego z MDX; persystencję
|
|
10
|
+
* zapewnia {@link ProcessViewStore} (localStorage / agregat).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ProcessModel } from "../model/types";
|
|
14
|
+
|
|
15
|
+
/** Ścieżka rozwinięcia: "occ:<occId>" lub "<parentKey>/<grupa>:<nodId>". */
|
|
16
|
+
export type ExpansionKey = string;
|
|
17
|
+
|
|
18
|
+
export function rootKey(occId: string): ExpansionKey {
|
|
19
|
+
return `occ:${occId}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function childKey(
|
|
23
|
+
parent: ExpansionKey,
|
|
24
|
+
group: string,
|
|
25
|
+
nodeId: string,
|
|
26
|
+
): ExpansionKey {
|
|
27
|
+
return `${parent}/${group}:${nodeId}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ProcessViewState {
|
|
31
|
+
/** Klucz rozwinięcia → otwarte grupy relacji. */
|
|
32
|
+
expanded: Record<ExpansionKey, string[]>;
|
|
33
|
+
/** Id kontekstu → zwinięty (override nad domyślnym trybem). */
|
|
34
|
+
collapsedContexts: Record<string, boolean>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const EMPTY_VIEW_STATE: ProcessViewState = {
|
|
38
|
+
expanded: {},
|
|
39
|
+
collapsedContexts: {},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Stan startowy z modelu: `<Arc expand={["mutates"]}>` otwiera grupy na
|
|
44
|
+
* korzeniu kroku. Deterministyczny — pierwszy render zawsze taki sam.
|
|
45
|
+
*/
|
|
46
|
+
export function initialViewState(model: ProcessModel): ProcessViewState {
|
|
47
|
+
const expanded: Record<ExpansionKey, string[]> = {};
|
|
48
|
+
for (const s of model.steps) {
|
|
49
|
+
if (s.expand?.length) expanded[rootKey(s.occId)] = [...s.expand];
|
|
50
|
+
}
|
|
51
|
+
return { expanded, collapsedContexts: {} };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Przełącz grupę rozwinięcia pod danym kluczem. */
|
|
55
|
+
export function toggleGroup(
|
|
56
|
+
state: ProcessViewState,
|
|
57
|
+
key: ExpansionKey,
|
|
58
|
+
group: string,
|
|
59
|
+
): ProcessViewState {
|
|
60
|
+
const current = state.expanded[key] ?? [];
|
|
61
|
+
const next = current.includes(group)
|
|
62
|
+
? current.filter((g) => g !== group)
|
|
63
|
+
: [...current, group];
|
|
64
|
+
const expanded = { ...state.expanded };
|
|
65
|
+
if (next.length) expanded[key] = next;
|
|
66
|
+
else delete expanded[key];
|
|
67
|
+
// Zamknięcie grupy zamyka też rozwinięcia-dzieci pod tym kluczem.
|
|
68
|
+
if (!next.includes(group)) {
|
|
69
|
+
const prefix = `${key}/${group}:`;
|
|
70
|
+
for (const k of Object.keys(expanded)) {
|
|
71
|
+
if (k.startsWith(prefix)) delete expanded[k];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { ...state, expanded };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Przełącz zwinięcie kontekstu (klik w box). */
|
|
78
|
+
export function toggleContext(
|
|
79
|
+
state: ProcessViewState,
|
|
80
|
+
contextId: string,
|
|
81
|
+
defaultCollapsed: boolean,
|
|
82
|
+
): ProcessViewState {
|
|
83
|
+
const current = state.collapsedContexts[contextId] ?? defaultCollapsed;
|
|
84
|
+
return {
|
|
85
|
+
...state,
|
|
86
|
+
collapsedContexts: { ...state.collapsedContexts, [contextId]: !current },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { resolveArcRef, normalizePagePath } from "../src/profiles/arc";
|
|
3
|
+
|
|
4
|
+
describe("normalizePagePath", () => {
|
|
5
|
+
test("strips query, hash and trailing slash; keeps root", () => {
|
|
6
|
+
expect(normalizePagePath("/checkout/success?orderId=1")).toBe("/checkout/success");
|
|
7
|
+
expect(normalizePagePath("/credits#top")).toBe("/credits");
|
|
8
|
+
expect(normalizePagePath("/checkout/")).toBe("/checkout");
|
|
9
|
+
expect(normalizePagePath("/")).toBe("/");
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe("resolveArcRef", () => {
|
|
14
|
+
test("page → identified by normalized path (not module key)", () => {
|
|
15
|
+
expect(resolveArcRef({ page: "/checkout/success?orderId=1" })).toMatchObject({
|
|
16
|
+
nodeId: "/checkout/success",
|
|
17
|
+
kind: "page",
|
|
18
|
+
key: "page:/checkout/success",
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
test("command / view / listener / route / event", () => {
|
|
24
|
+
expect(resolveArcRef({ command: "signIn" })).toMatchObject({ nodeId: "signIn", kind: "command", key: "command:signIn" });
|
|
25
|
+
expect(resolveArcRef({ view: "orders" })).toMatchObject({ nodeId: "orders", kind: "view" });
|
|
26
|
+
expect(resolveArcRef({ listener: "onX" })).toMatchObject({ nodeId: "onX", kind: "listener" });
|
|
27
|
+
expect(resolveArcRef({ route: "webhook" })).toMatchObject({ nodeId: "webhook", kind: "route" });
|
|
28
|
+
expect(resolveArcRef({ event: "taskCreated" })).toMatchObject({ nodeId: "taskCreated", kind: "event" });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("aggregate alone → aggregate node", () => {
|
|
32
|
+
expect(resolveArcRef({ aggregate: "tasks" })).toMatchObject({ nodeId: "tasks", kind: "aggregate", key: "aggregate:tasks" });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("aggregate + mutationMethod → aggregate node + method", () => {
|
|
36
|
+
const r = resolveArcRef({ aggregate: "userAccounts", mutationMethod: "checkEmail" });
|
|
37
|
+
expect(r).toMatchObject({
|
|
38
|
+
nodeId: "userAccounts",
|
|
39
|
+
kind: "aggregate",
|
|
40
|
+
method: { name: "checkEmail", kind: "mutation" },
|
|
41
|
+
key: "aggregate:userAccounts#mutate:checkEmail",
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("aggregate + queryMethod → query method", () => {
|
|
46
|
+
expect(resolveArcRef({ aggregate: "tasks", queryMethod: "getAll" })?.method).toEqual({ name: "getAll", kind: "query" });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("aggregate + event → the event node", () => {
|
|
50
|
+
expect(resolveArcRef({ aggregate: "tasks", event: "taskCreated" })).toMatchObject({ nodeId: "taskCreated", kind: "event" });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("no recognised prop → null", () => {
|
|
54
|
+
expect(resolveArcRef({})).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { buildProcessModel } from "../src/model/build";
|
|
3
|
+
import {
|
|
4
|
+
arcProfile,
|
|
5
|
+
resolveArcRef,
|
|
6
|
+
toProcessGraph,
|
|
7
|
+
type ArcGraphEdge,
|
|
8
|
+
type ArcGraphInput,
|
|
9
|
+
} from "../src/profiles/arc";
|
|
10
|
+
import type {
|
|
11
|
+
Occurrence,
|
|
12
|
+
PathSeg,
|
|
13
|
+
StepOccurrence,
|
|
14
|
+
} from "../src/model/types";
|
|
15
|
+
|
|
16
|
+
function el(id: string, kind: string, extra: Record<string, unknown> = {}) {
|
|
17
|
+
return { id, name: id, kind, ...extra };
|
|
18
|
+
}
|
|
19
|
+
function edge(source: string, target: string, kind: string): ArcGraphEdge {
|
|
20
|
+
return { source, target, kind };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// cmd czyta v, zapisuje e; listener l też zapisuje e; v,x widoki; e event.
|
|
24
|
+
const input: ArcGraphInput = {
|
|
25
|
+
elements: [
|
|
26
|
+
el("cmd", "command", { queryDeps: ["v"], mutationDeps: ["e"] }),
|
|
27
|
+
el("l", "listener", { mutationDeps: ["e"] }),
|
|
28
|
+
el("v", "view"),
|
|
29
|
+
el("e", "event"),
|
|
30
|
+
el("x", "view"),
|
|
31
|
+
],
|
|
32
|
+
edges: [edge("cmd", "v", "queries"), edge("cmd", "e", "mutates"), edge("l", "e", "mutates")],
|
|
33
|
+
components: [],
|
|
34
|
+
componentEdges: [],
|
|
35
|
+
};
|
|
36
|
+
const graph = toProcessGraph(input);
|
|
37
|
+
|
|
38
|
+
function occ(
|
|
39
|
+
occId: string,
|
|
40
|
+
props: Record<string, string>,
|
|
41
|
+
label = occId,
|
|
42
|
+
path?: PathSeg[],
|
|
43
|
+
): StepOccurrence {
|
|
44
|
+
return { type: "step", occId, ref: resolveArcRef(props), label, path };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("buildProcessModel — parytet z buildUseCaseModel", () => {
|
|
48
|
+
test("one step per occurrence (duplicates kept)", () => {
|
|
49
|
+
const m = buildProcessModel({
|
|
50
|
+
graph,
|
|
51
|
+
profile: arcProfile,
|
|
52
|
+
occurrences: [occ("o1", { command: "cmd" }), occ("o2", { command: "cmd" })],
|
|
53
|
+
});
|
|
54
|
+
expect(m.steps.map((s) => s.occId)).toEqual(["o1", "o2"]);
|
|
55
|
+
expect(m.steps.map((s) => s.seq)).toEqual([1, 2]);
|
|
56
|
+
expect(m.steps.every((s) => s.exists)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("missing reference → step with exists=false", () => {
|
|
60
|
+
const m = buildProcessModel({
|
|
61
|
+
graph,
|
|
62
|
+
profile: arcProfile,
|
|
63
|
+
occurrences: [occ("o1", { command: "ghost" })],
|
|
64
|
+
});
|
|
65
|
+
expect(m.steps[0].exists).toBe(false);
|
|
66
|
+
expect(m.steps[0].node).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("forward attachments exclude visible; query dep becomes attachment", () => {
|
|
70
|
+
const m = buildProcessModel({
|
|
71
|
+
graph,
|
|
72
|
+
profile: arcProfile,
|
|
73
|
+
occurrences: [occ("o1", { command: "cmd" }), occ("o2", { event: "e" })],
|
|
74
|
+
});
|
|
75
|
+
const cmd = m.steps.find((s) => s.occId === "o1")!;
|
|
76
|
+
expect(cmd.attachments.queries).toEqual(["v"]);
|
|
77
|
+
expect(cmd.attachments.mutates).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("reverse mutatedBy attachment (e pisane przez cmd[widoczny] i l[ukryty])", () => {
|
|
81
|
+
const m = buildProcessModel({
|
|
82
|
+
graph,
|
|
83
|
+
profile: arcProfile,
|
|
84
|
+
occurrences: [occ("o1", { command: "cmd" }), occ("o2", { event: "e" })],
|
|
85
|
+
});
|
|
86
|
+
const e = m.steps.find((s) => s.occId === "o2")!;
|
|
87
|
+
expect(e.attachments.mutatedBy).toEqual(["l"]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("connector leży na SKIEROWANEJ ścieżce przepływu między krokami", () => {
|
|
91
|
+
const chain = toProcessGraph({
|
|
92
|
+
elements: [
|
|
93
|
+
el("cmd", "command", { mutationDeps: ["e"] }),
|
|
94
|
+
el("e", "event"),
|
|
95
|
+
el("l", "listener"),
|
|
96
|
+
],
|
|
97
|
+
edges: [edge("cmd", "e", "emits"), edge("l", "e", "handles")],
|
|
98
|
+
});
|
|
99
|
+
const m = buildProcessModel({
|
|
100
|
+
graph: chain,
|
|
101
|
+
profile: arcProfile,
|
|
102
|
+
occurrences: [occ("o1", { command: "cmd" }), occ("o2", { listener: "l" })],
|
|
103
|
+
});
|
|
104
|
+
expect(m.connectors.map((c) => c.id)).toEqual(["e"]);
|
|
105
|
+
expect(m.connectors[0].between).toEqual([1, 2]);
|
|
106
|
+
expect(m.connectors[0].order).toBe(1);
|
|
107
|
+
expect(m.connectors[0].count).toBe(1);
|
|
108
|
+
expect(m.visible.has("e")).toBe(true);
|
|
109
|
+
expect(m.steps.find((s) => s.occId === "o1")!.attachments.mutates).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("wspólny downstream NIE jest connectorem", () => {
|
|
113
|
+
const m = buildProcessModel({
|
|
114
|
+
graph,
|
|
115
|
+
profile: arcProfile,
|
|
116
|
+
occurrences: [occ("o1", { command: "cmd" }), occ("o2", { listener: "l" })],
|
|
117
|
+
});
|
|
118
|
+
expect(m.connectors.length).toBe(0);
|
|
119
|
+
expect(m.visible.has("e")).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("usedByUI z componentEdges + referencje component", () => {
|
|
123
|
+
const ui = toProcessGraph({
|
|
124
|
+
elements: [el("acc", "aggregate")],
|
|
125
|
+
edges: [],
|
|
126
|
+
components: [
|
|
127
|
+
{ id: "pkg/login.tsx", file: "pkg/login.tsx", name: "login" },
|
|
128
|
+
{ id: "pkg/other.tsx", file: "pkg/other.tsx", name: "other" },
|
|
129
|
+
],
|
|
130
|
+
componentEdges: [{ source: "pkg/login.tsx", target: "acc", usage: "query" }],
|
|
131
|
+
});
|
|
132
|
+
const m = buildProcessModel({
|
|
133
|
+
graph: ui,
|
|
134
|
+
profile: arcProfile,
|
|
135
|
+
occurrences: [occ("o1", { aggregate: "acc" })],
|
|
136
|
+
});
|
|
137
|
+
expect(m.steps[0].attachments.usedByUI).toEqual(["pkg/login.tsx"]);
|
|
138
|
+
|
|
139
|
+
const m2 = buildProcessModel({
|
|
140
|
+
graph: ui,
|
|
141
|
+
profile: arcProfile,
|
|
142
|
+
occurrences: [occ("o1", { component: "pkg/login.tsx" })],
|
|
143
|
+
});
|
|
144
|
+
expect(m2.steps[0].exists).toBe(true);
|
|
145
|
+
expect(m2.steps[0].node?.kind).toBe("page");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("depth/lane: zwykła sekwencja inkrementuje depth, lane 0", () => {
|
|
149
|
+
const m = buildProcessModel({
|
|
150
|
+
graph,
|
|
151
|
+
profile: arcProfile,
|
|
152
|
+
occurrences: [occ("o1", { command: "cmd" }), occ("o2", { event: "e" })],
|
|
153
|
+
});
|
|
154
|
+
expect(m.steps.map((s) => [s.depth, s.lane])).toEqual([
|
|
155
|
+
[0, 0],
|
|
156
|
+
[1, 0],
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("depth/lane: <Parallel> dzieli lane'y na tej samej głębokości", () => {
|
|
161
|
+
const p = (occId: string, props: Record<string, string>, lane: number) =>
|
|
162
|
+
occ(occId, props, occId, [{ type: "parallel", id: "p1", laneIndex: lane }]);
|
|
163
|
+
const m = buildProcessModel({
|
|
164
|
+
graph,
|
|
165
|
+
profile: arcProfile,
|
|
166
|
+
occurrences: [
|
|
167
|
+
p("a", { command: "cmd" }, 0),
|
|
168
|
+
p("b", { view: "v" }, 1),
|
|
169
|
+
occ("c", { event: "e" }),
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
const by = new Map(m.steps.map((s) => [s.occId, s]));
|
|
173
|
+
expect([by.get("a")!.depth, by.get("a")!.lane]).toEqual([0, 0]);
|
|
174
|
+
expect([by.get("b")!.depth, by.get("b")!.lane]).toEqual([0, 1]);
|
|
175
|
+
expect([by.get("c")!.depth, by.get("c")!.lane]).toEqual([1, 0]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("depth/lane: <Branch>/<Lane> ustawia alt i lane'y", () => {
|
|
179
|
+
const b = (occId: string, props: Record<string, string>, lane: number, title: string) =>
|
|
180
|
+
occ(occId, props, occId, [
|
|
181
|
+
{ type: "branch", id: "br1", laneIndex: lane },
|
|
182
|
+
{ type: "lane", id: `l${lane}`, title, alt: true },
|
|
183
|
+
]);
|
|
184
|
+
const m = buildProcessModel({
|
|
185
|
+
graph,
|
|
186
|
+
profile: arcProfile,
|
|
187
|
+
occurrences: [b("ok", { view: "v" }, 0, "Sukces"), b("err", { event: "e" }, 1, "Błąd")],
|
|
188
|
+
});
|
|
189
|
+
const by = new Map(m.steps.map((s) => [s.occId, s]));
|
|
190
|
+
expect(by.get("ok")!.alt).toBe(true);
|
|
191
|
+
expect([by.get("ok")!.depth, by.get("ok")!.lane]).toEqual([0, 0]);
|
|
192
|
+
expect([by.get("err")!.depth, by.get("err")!.lane]).toEqual([0, 1]);
|
|
193
|
+
expect(by.get("ok")!.laneTitle).toBe("Sukces");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("aggregate mutationMethod używa per-metodowych zależności", () => {
|
|
197
|
+
const agg = toProcessGraph({
|
|
198
|
+
elements: [
|
|
199
|
+
el("acc", "aggregate", {
|
|
200
|
+
methods: [
|
|
201
|
+
{ name: "check", kind: "mutate", queryDeps: ["dirA"], mutationDeps: ["dirB"] },
|
|
202
|
+
{ name: "other", kind: "mutate", queryDeps: ["zzz"], mutationDeps: [] },
|
|
203
|
+
],
|
|
204
|
+
}),
|
|
205
|
+
el("dirA", "view"),
|
|
206
|
+
el("dirB", "event"),
|
|
207
|
+
el("zzz", "view"),
|
|
208
|
+
],
|
|
209
|
+
edges: [],
|
|
210
|
+
});
|
|
211
|
+
const m = buildProcessModel({
|
|
212
|
+
graph: agg,
|
|
213
|
+
profile: arcProfile,
|
|
214
|
+
occurrences: [occ("o1", { aggregate: "acc", mutationMethod: "check" })],
|
|
215
|
+
});
|
|
216
|
+
expect(m.steps[0].attachments.queries).toEqual(["dirA"]);
|
|
217
|
+
expect(m.steps[0].attachments.mutates).toEqual(["dirB"]);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("buildProcessModel — fazy, Entry/Continue, notatki, konteksty", () => {
|
|
222
|
+
const entryPath = (id: string, title: string): PathSeg[] => [
|
|
223
|
+
{ type: "entry", id, title },
|
|
224
|
+
];
|
|
225
|
+
const phasePath = (phaseId: string, title?: string): PathSeg[] => [
|
|
226
|
+
{ type: "step", id: `s-${phaseId}`, phaseId, title },
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
test("Entry z Continue wpina się przed fazę docelową (depth) na osobnym torze", () => {
|
|
230
|
+
const occurrences: Occurrence[] = [
|
|
231
|
+
// Entry: dwa kroki → Continue do fazy "kwalifikacja".
|
|
232
|
+
occ("e1", { command: "cmd" }, "e1", entryPath("en1", "Formularz")),
|
|
233
|
+
occ("e2", { event: "e" }, "e2", entryPath("en1", "Formularz")),
|
|
234
|
+
{ type: "continue", to: "kwalifikacja", path: entryPath("en1", "Formularz") },
|
|
235
|
+
// Oś główna: faza kwalifikacja o dwóch krokach.
|
|
236
|
+
occ("m1", { view: "v" }, "m1", phasePath("kwalifikacja")),
|
|
237
|
+
occ("m2", { view: "x" }, "m2", phasePath("kwalifikacja")),
|
|
238
|
+
];
|
|
239
|
+
const m = buildProcessModel({ graph, profile: arcProfile, occurrences });
|
|
240
|
+
const by = new Map(m.steps.map((s) => [s.occId, s]));
|
|
241
|
+
|
|
242
|
+
// Oś główna zaczyna od depth 0.
|
|
243
|
+
expect([by.get("m1")!.depth, by.get("m1")!.lane]).toEqual([0, 0]);
|
|
244
|
+
expect([by.get("m2")!.depth, by.get("m2")!.lane]).toEqual([1, 0]);
|
|
245
|
+
// Entry (łańcuch L=2, cel depth 0) → start = max(0, 0-2) = 0, tor nad osią.
|
|
246
|
+
expect(by.get("e1")!.lane).toBeLessThan(0);
|
|
247
|
+
expect(by.get("e2")!.lane).toBe(by.get("e1")!.lane);
|
|
248
|
+
expect(by.get("e1")!.entryTitle).toBe("Formularz");
|
|
249
|
+
// Krawędź continues: ostatni krok Entry → pierwszy krok fazy.
|
|
250
|
+
expect(m.continueEdges).toEqual([
|
|
251
|
+
{ fromOcc: "e2", toOcc: "m1", phaseId: "kwalifikacja" },
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("Entry wpinające się w ŚRODEK procesu dostaje depth przed fazą", () => {
|
|
256
|
+
const occurrences: Occurrence[] = [
|
|
257
|
+
occ("m1", { view: "v" }, "m1", phasePath("start")),
|
|
258
|
+
occ("m2", { view: "x" }, "m2", phasePath("wywiad")),
|
|
259
|
+
occ("m3", { event: "e" }, "m3", phasePath("wywiad")),
|
|
260
|
+
occ("a1", { command: "cmd" }, "a1", entryPath("en-admin", "Link admina")),
|
|
261
|
+
{ type: "continue", to: "wywiad", path: entryPath("en-admin", "Link admina") },
|
|
262
|
+
];
|
|
263
|
+
const m = buildProcessModel({ graph, profile: arcProfile, occurrences });
|
|
264
|
+
const by = new Map(m.steps.map((s) => [s.occId, s]));
|
|
265
|
+
// Faza "wywiad" zaczyna się na depth 1 → Entry (L=1) na depth 0.
|
|
266
|
+
expect(by.get("a1")!.depth).toBe(0);
|
|
267
|
+
expect(by.get("a1")!.lane).not.toBe(0);
|
|
268
|
+
expect(m.continueEdges).toEqual([
|
|
269
|
+
{ fromOcc: "a1", toOcc: "m2", phaseId: "wywiad" },
|
|
270
|
+
]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("dwa Entry rozkładają się naprzemiennie nad i pod osią", () => {
|
|
274
|
+
const occurrences: Occurrence[] = [
|
|
275
|
+
occ("f1", { command: "cmd" }, "f1", entryPath("en-a", "A")),
|
|
276
|
+
{ type: "continue", to: "p", path: entryPath("en-a", "A") },
|
|
277
|
+
occ("g1", { listener: "l" }, "g1", entryPath("en-b", "B")),
|
|
278
|
+
{ type: "continue", to: "p", path: entryPath("en-b", "B") },
|
|
279
|
+
occ("m1", { view: "v" }, "m1", phasePath("p")),
|
|
280
|
+
];
|
|
281
|
+
const m = buildProcessModel({ graph, profile: arcProfile, occurrences });
|
|
282
|
+
const by = new Map(m.steps.map((s) => [s.occId, s]));
|
|
283
|
+
expect(by.get("f1")!.lane).toBeLessThan(0); // pierwsze nad osią
|
|
284
|
+
expect(by.get("g1")!.lane).toBeGreaterThan(0); // drugie pod osią
|
|
285
|
+
expect(m.continueEdges.length).toBe(2);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("Entry bez Continue wpina się w pierwszą fazę dokumentu", () => {
|
|
289
|
+
const occurrences: Occurrence[] = [
|
|
290
|
+
occ("f1", { command: "cmd" }, "f1", entryPath("en-x", "X")),
|
|
291
|
+
occ("m1", { view: "v" }, "m1"),
|
|
292
|
+
];
|
|
293
|
+
const m = buildProcessModel({ graph, profile: arcProfile, occurrences });
|
|
294
|
+
expect(m.continueEdges).toEqual([{ fromOcc: "f1", toOcc: "m1", phaseId: "" }]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("notatka dokleja się do poprzedzającego kroku", () => {
|
|
298
|
+
const occurrences: Occurrence[] = [
|
|
299
|
+
occ("m1", { view: "v" }, "m1"),
|
|
300
|
+
{ type: "note", occId: "n1", text: "Ten widok admin wysyła linkiem.", side: "N" },
|
|
301
|
+
occ("m2", { event: "e" }, "m2"),
|
|
302
|
+
];
|
|
303
|
+
const m = buildProcessModel({ graph, profile: arcProfile, occurrences });
|
|
304
|
+
expect(m.steps[0].notes).toEqual([
|
|
305
|
+
{ occId: "n1", side: "N", title: undefined, text: "Ten widok admin wysyła linkiem." },
|
|
306
|
+
]);
|
|
307
|
+
expect(m.steps[1].notes).toEqual([]);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("<Arc context> rozwiązuje po kropkowanej ścieżce nazw", () => {
|
|
311
|
+
const g = toProcessGraph({
|
|
312
|
+
elements: [],
|
|
313
|
+
edges: [],
|
|
314
|
+
contexts: [
|
|
315
|
+
{ id: "ctx_ai", path: ["ctx_ai"], name: "ai" },
|
|
316
|
+
{ id: "ctx_chat", path: ["ctx_ai", "ctx_chat"], name: "chat", title: "Czat AI" },
|
|
317
|
+
],
|
|
318
|
+
});
|
|
319
|
+
const occurrences: Occurrence[] = [
|
|
320
|
+
{
|
|
321
|
+
type: "step",
|
|
322
|
+
occId: "c1",
|
|
323
|
+
ref: { nodeId: "ai.chat", kind: "context", key: "context:ai.chat" },
|
|
324
|
+
label: "agent AI dopytuje",
|
|
325
|
+
},
|
|
326
|
+
];
|
|
327
|
+
const m = buildProcessModel({ graph: g, profile: arcProfile, occurrences });
|
|
328
|
+
expect(m.steps[0].exists).toBe(true);
|
|
329
|
+
expect(m.steps[0].node?.kind).toBe("context");
|
|
330
|
+
expect(m.steps[0].node?.id).toBe("ctx:ctx_chat");
|
|
331
|
+
expect(m.steps[0].node?.label).toBe("Czat AI");
|
|
332
|
+
expect(m.steps[0].node?.contextPath).toEqual(["ctx_ai", "ctx_chat"]);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("sides/expand z wystąpienia przechodzą na krok", () => {
|
|
336
|
+
const o: StepOccurrence = {
|
|
337
|
+
...occ("o1", { command: "cmd" }),
|
|
338
|
+
sides: { mutates: "SE" },
|
|
339
|
+
expand: ["mutates"],
|
|
340
|
+
};
|
|
341
|
+
const m = buildProcessModel({ graph, profile: arcProfile, occurrences: [o] });
|
|
342
|
+
expect(m.steps[0].sides).toEqual({ mutates: "SE" });
|
|
343
|
+
expect(m.steps[0].expand).toEqual(["mutates"]);
|
|
344
|
+
});
|
|
345
|
+
});
|