@arcote.tech/arc-process 0.7.28 → 0.7.30

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc-process",
3
3
  "type": "module",
4
- "version": "0.7.28",
4
+ "version": "0.7.30",
5
5
  "private": false,
6
6
  "description": "Process description & diagram fragment for Arc framework — generic graph model with profiles, MDX flow components and a deterministic 8-direction layout",
7
7
  "main": "./src/index.ts",
@@ -18,11 +18,12 @@
18
18
  "@mdx-js/mdx": "^3.1.0",
19
19
  "@xyflow/react": "^12.0.0",
20
20
  "remark-frontmatter": "^5.0.0",
21
- "remark-gfm": "^4.0.0"
21
+ "remark-gfm": "^4.0.0",
22
+ "shiki": "^1.24.0"
22
23
  },
23
24
  "peerDependencies": {
24
- "@arcote.tech/arc": "^0.7.28",
25
- "@arcote.tech/platform": "^0.7.28",
25
+ "@arcote.tech/arc": "^0.7.30",
26
+ "@arcote.tech/platform": "^0.7.30",
26
27
  "react": ">=18.0.0",
27
28
  "react-dom": ">=18.0.0",
28
29
  "typescript": "^5.0.0"
@@ -163,16 +163,15 @@ export function layoutProcess(input: LayoutInput): ProcessLayout {
163
163
  placeGroups(m.groups, s.occId, cx, cy, STEP_W, STEP_H, nodes, edges);
164
164
  }
165
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 sekwencją.
172
- if (a.blockId && a.blockId === b.blockId && a.lane !== b.lane) continue;
173
- if (a.entryId !== b.entryId) continue;
166
+ // --- Szkielet narracji (per tor, z fork/join na blokach) ---------------------
167
+ // główna i każde Entry to osobne tory blok Entry wtrącony w dokument
168
+ // nie przerywa ciągłości osi. Bloki Parallel/Branch dostają jawne
169
+ // rozwidlenie (krok przed blokiem → pierwszy krok KAŻDEGO lane'a) i
170
+ // scalenie (ostatni krok każdego lane'a krok po bloku).
171
+ let bbIdx = 0;
172
+ const backbone = (a: (typeof steps)[number], b: (typeof steps)[number]) =>
174
173
  edges.push({
175
- id: `bb:${i}`,
174
+ id: `bb:${bbIdx++}`,
176
175
  source: a.occId,
177
176
  target: b.occId,
178
177
  color: BACKBONE_COLOR,
@@ -180,6 +179,58 @@ export function layoutProcess(input: LayoutInput): ProcessLayout {
180
179
  opacity: 0.6,
181
180
  backbone: true,
182
181
  });
182
+
183
+ const trackMap = new Map<string, (typeof steps)[number][]>();
184
+ for (const s of steps) {
185
+ const key = s.entryId ?? "";
186
+ let track = trackMap.get(key);
187
+ if (!track) trackMap.set(key, (track = []));
188
+ track.push(s);
189
+ }
190
+
191
+ interface TrackSeg {
192
+ block?: string;
193
+ members: (typeof steps)[number][];
194
+ }
195
+ for (const track of trackMap.values()) {
196
+ // Segmentacja toru: pojedyncze kroki i runy wspólnego blockId.
197
+ const segs: TrackSeg[] = [];
198
+ for (const s of track) {
199
+ const last = segs[segs.length - 1];
200
+ if (s.blockId && last?.block === s.blockId) last.members.push(s);
201
+ else segs.push({ block: s.blockId, members: [s] });
202
+ }
203
+
204
+ const laneGroups = (seg: TrackSeg) => {
205
+ const byLane = new Map<number, (typeof steps)[number][]>();
206
+ for (const s of seg.members) {
207
+ let g = byLane.get(s.lane);
208
+ if (!g) byLane.set(s.lane, (g = []));
209
+ g.push(s);
210
+ }
211
+ return [...byLane.values()];
212
+ };
213
+ const firstsOf = (seg: TrackSeg) =>
214
+ seg.block ? laneGroups(seg).map((g) => g[0]) : [seg.members[0]];
215
+ const lastsOf = (seg: TrackSeg) =>
216
+ seg.block
217
+ ? laneGroups(seg).map((g) => g[g.length - 1])
218
+ : [seg.members[seg.members.length - 1]];
219
+
220
+ for (let i = 0; i < segs.length; i++) {
221
+ const seg = segs[i];
222
+ // Sekwencje wewnątrz lane'ów bloku.
223
+ if (seg.block) {
224
+ for (const g of laneGroups(seg)) {
225
+ for (let k = 1; k < g.length; k++) backbone(g[k - 1], g[k]);
226
+ }
227
+ }
228
+ if (i === 0) continue;
229
+ // Fork/join: każdy koniec poprzedniego segmentu → każdy początek bieżącego.
230
+ for (const a of lastsOf(segs[i - 1])) {
231
+ for (const b of firstsOf(seg)) backbone(a, b);
232
+ }
233
+ }
183
234
  }
184
235
 
185
236
  // --- Krawędzie continues (Entry → faza, skosem) ------------------------------
package/src/mdx/arc.tsx CHANGED
@@ -16,14 +16,17 @@ import { kindColor } from "../model/profile";
16
16
  import {
17
17
  CONTEXT_REF_KIND,
18
18
  type Direction,
19
+ type PinSpec,
19
20
  type ResolvedRef,
20
21
  } from "../model/types";
21
22
  import { resolveContextRef } from "../model/build";
22
23
  import { MISSING_COLOR, tint } from "../react/theme";
24
+ import { isPinElement, PinCollectorContext } from "./pin";
23
25
  import {
24
26
  useFocus,
25
27
  useProcessData,
26
28
  useRegister,
29
+ useSelection,
27
30
  useStructure,
28
31
  } from "./contexts";
29
32
 
@@ -43,6 +46,7 @@ function textOf(node: ReactNode): string {
43
46
  if (node == null || node === false) return "";
44
47
  if (typeof node === "string" || typeof node === "number") return String(node);
45
48
  if (Array.isArray(node)) return node.map(textOf).join("");
49
+ if (isPinElement(node)) return ""; // treść pinów nie wchodzi do etykiety
46
50
  if (typeof node === "object" && "props" in (node as unknown as Record<string, unknown>)) {
47
51
  return textOf(
48
52
  (node as unknown as { props?: { children?: ReactNode } }).props?.children,
@@ -57,6 +61,7 @@ export function Arc(props: ArcProps) {
57
61
  const register = useRegister();
58
62
  const data = useProcessData();
59
63
  const focus = useFocus();
64
+ const selection = useSelection();
60
65
  const { path } = useStructure();
61
66
 
62
67
  const ref: ResolvedRef | null = context
@@ -64,6 +69,17 @@ export function Arc(props: ArcProps) {
64
69
  : (data?.profile.resolveRef(refProps) ?? null);
65
70
  const label = textOf(children);
66
71
 
72
+ // Piny z dzieci (<Person>/<Metric>/<Pin>): świeża tablica per render;
73
+ // dzieci renderują się PRZED efektami, a ArcRefsProvider czyta wpisy w
74
+ // useEffect — więc mutacja tej tablicy podczas renderu dzieci jest
75
+ // bezpieczna i occurrence ma komplet pinów, zanim ktokolwiek go odczyta.
76
+ const pins: PinSpec[] = [];
77
+ const collectPin = (pin: PinSpec) => {
78
+ if (!pins.some((p) => p.kind === pin.kind && p.name === pin.name)) {
79
+ pins.push(pin);
80
+ }
81
+ };
82
+
67
83
  // Rejestracja podczas renderu — idempotentna (klucz domId), przeżywa StrictMode.
68
84
  register?.({
69
85
  domId,
@@ -75,6 +91,7 @@ export function Arc(props: ArcProps) {
75
91
  path,
76
92
  sides,
77
93
  expand,
94
+ pins,
78
95
  },
79
96
  });
80
97
 
@@ -90,11 +107,17 @@ export function Arc(props: ArcProps) {
90
107
  ? `ctx:${resolvedNode.id}`
91
108
  : ref?.nodeId;
92
109
  const active = !!focusId && focus.focusNodeId === focusId;
93
- const color = resolved
110
+ // Kolor zawsze z ZADEKLAROWANEGO rodzaju (ref.kind) — element nieistniejący
111
+ // (design-first TODO) zachowuje kolor eventu/komendy itd., a brak w grafie
112
+ // sygnalizuje wyłącznie kreskowana ramka. Czerwień tylko dla refów
113
+ // nierozpoznanych w ogóle (złe propsy).
114
+ const declaredKind = resolved
94
115
  ? ref!.kind === CONTEXT_REF_KIND
95
- ? (data ? kindColor(data.profile, CONTEXT_REF_KIND) : "#7c3aed")
96
- : kindColor(data!.profile, (resolvedNode as { kind: string }).kind)
97
- : MISSING_COLOR;
116
+ ? CONTEXT_REF_KIND
117
+ : (resolvedNode as { kind: string }).kind
118
+ : ref?.kind;
119
+ const color =
120
+ declaredKind && data ? kindColor(data.profile, declaredKind) : MISSING_COLOR;
98
121
 
99
122
  return (
100
123
  <span
@@ -103,6 +126,7 @@ export function Arc(props: ArcProps) {
103
126
  data-arc-node={focusId ?? ""}
104
127
  onMouseEnter={() => resolved && focusId && focus.setFocusNodeId(focusId)}
105
128
  onMouseLeave={() => focus.setFocusNodeId(null)}
129
+ onClick={() => selection.selectOcc(domId)}
106
130
  title={
107
131
  resolved
108
132
  ? ref?.method
@@ -117,23 +141,39 @@ export function Arc(props: ArcProps) {
117
141
  padding: "1px 8px",
118
142
  margin: "0 1px",
119
143
  borderRadius: 8,
120
- border: `1.5px solid ${color}`,
121
- background: resolved ? tint(color, 0.1) : "rgba(225,29,72,0.08)",
144
+ border: `1.5px ${resolved ? "solid" : "dashed"} ${color}`,
145
+ background: tint(color, resolved ? 0.1 : 0.05),
122
146
  color: "#2a173f",
123
147
  fontWeight: 600,
124
148
  fontSize: "0.92em",
125
149
  cursor: "pointer",
126
150
  boxShadow: active ? `0 0 0 2px ${color}` : "none",
127
151
  whiteSpace: "nowrap",
152
+ opacity: resolved ? 1 : 0.85,
128
153
  }}
129
154
  >
130
155
  <span
131
156
  style={{ width: 7, height: 7, borderRadius: 2, background: color }}
132
157
  />
133
- {label || ref?.nodeId || "?"}
134
- {ref?.method ? (
135
- <span style={{ color: "#6b5b86", fontWeight: 500 }}>·{ref.method.name}</span>
136
- ) : null}
158
+ <PinCollectorContext.Provider value={collectPin}>
159
+ {label || ref?.nodeId || "?"}
160
+ {ref?.method ? (
161
+ <span style={{ color: "#6b5b86", fontWeight: 500 }}>·{ref.method.name}</span>
162
+ ) : null}
163
+ {renderPins(children)}
164
+ </PinCollectorContext.Provider>
137
165
  </span>
138
166
  );
139
167
  }
168
+
169
+ /** Wyrenderuj z dzieci WYŁĄCZNIE komponenty pinów (tekst poszedł do etykiety). */
170
+ function renderPins(children: ReactNode): ReactNode {
171
+ if (children == null || typeof children !== "object") return null;
172
+ if (Array.isArray(children)) {
173
+ return children.map((c, i) =>
174
+ isPinElement(c) ? <span key={i}>{c}</span> : renderPins(c),
175
+ );
176
+ }
177
+ if (isPinElement(children)) return children;
178
+ return null;
179
+ }
@@ -40,6 +40,17 @@ export interface RegEntry {
40
40
  export const RegisterContext = createContext<((entry: RegEntry) => void) | null>(null);
41
41
  export const useRegister = () => useContext(RegisterContext);
42
42
 
43
+ /** Wybór kroku → drawer szczegółów (occId lub null = zamknięty). */
44
+ export interface SelectionState {
45
+ selectedOcc: string | null;
46
+ selectOcc: (occId: string | null) => void;
47
+ }
48
+ export const SelectionContext = createContext<SelectionState>({
49
+ selectedOcc: null,
50
+ selectOcc: () => {},
51
+ });
52
+ export const useSelection = () => useContext(SelectionContext);
53
+
43
54
  /** Stan widoku + akcje (toggle rozwinięć / zwinięć kontekstów). */
44
55
  export interface ViewStateActions {
45
56
  state: ProcessViewState;
@@ -11,6 +11,7 @@ import { useEffect, useState, type ComponentType, type ReactNode } from "react";
11
11
  import * as runtime from "react/jsx-runtime";
12
12
  import type { Occurrence } from "../model/types";
13
13
  import { Arc } from "./arc";
14
+ import { Metric, Person, Pin } from "./pin";
14
15
  import { Branch, Continue, Entry, Lane, Note, Parallel, Step } from "./structure";
15
16
  import { ArcRefsProvider } from "./ref-context";
16
17
  import { GLASS } from "../react/theme";
@@ -26,6 +27,9 @@ const mdxComponents = {
26
27
  Entry,
27
28
  Continue,
28
29
  Note,
30
+ Pin,
31
+ Person,
32
+ Metric,
29
33
  h1: (p: ElProps) => (
30
34
  <h1 style={{ fontSize: 24, color: GLASS.text, margin: "4px 0 12px" }} {...p} />
31
35
  ),
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Przypięcia (`<Pin>`, `<Person>`, `<Metric>`) — deklarowane w MDX WEWNĄTRZ
3
+ * `<Arc>`:
4
+ *
5
+ * ```mdx
6
+ * <Arc aggregate="leads" mutationMethod="submitLead">
7
+ * zgłoszenie leada<Person name="prospekt"/><Metric name="leads.submitted"/>
8
+ * </Arc>
9
+ * ```
10
+ *
11
+ * Pin rejestruje się u rodzica (kolektor z kontekstu `<Arc>`) podczas
12
+ * renderu i renderuje mini-chip inline. Rodzaje pinów definiuje PROFIL
13
+ * (`pinKinds`); wartości rozwiązują się z generycznego katalogu
14
+ * (arc.catalog.yml) po `name` — w modelu, nie tutaj. `Person`/`Metric` to
15
+ * cienkie aliasy generycznego `Pin`, przyszłe rodzaje nie wymagają zmian.
16
+ */
17
+
18
+ import { createContext, useContext, type ReactNode } from "react";
19
+ import type { PinSpec } from "../model/types";
20
+ import { GLASS, tint } from "../react/theme";
21
+ import { useProcessData } from "./contexts";
22
+
23
+ /** Kolektor pinów dostarczany przez `<Arc>` swoim dzieciom. */
24
+ export const PinCollectorContext = createContext<((pin: PinSpec) => void) | null>(
25
+ null,
26
+ );
27
+
28
+ function textOf(node: ReactNode): string {
29
+ if (node == null || node === false) return "";
30
+ if (typeof node === "string" || typeof node === "number") return String(node);
31
+ if (Array.isArray(node)) return node.map(textOf).join("");
32
+ return "";
33
+ }
34
+
35
+ export interface PinProps {
36
+ /** Rodzaj przypięcia (klucz `pinKinds` profilu), np. "persona", "metric". */
37
+ type: string;
38
+ /** Nazwa wpisu katalogu (arc.catalog.yml). */
39
+ name: string;
40
+ /** Etykieta (fallback: label z katalogu → name). */
41
+ label?: string;
42
+ children?: ReactNode;
43
+ }
44
+
45
+ export function Pin({ type, name, label, children }: PinProps) {
46
+ const collect = useContext(PinCollectorContext);
47
+ const data = useProcessData();
48
+ const resolvedLabel = label ?? (textOf(children) || undefined);
49
+
50
+ collect?.({ kind: type, name, label: resolvedLabel });
51
+
52
+ const spec = data?.profile.pinKinds?.[type];
53
+ const color = spec?.color ?? "#64748b";
54
+ return (
55
+ <span
56
+ data-arc-pin={type}
57
+ title={`${spec?.label ?? type}: ${name}`}
58
+ style={{
59
+ display: "inline-flex",
60
+ alignItems: "center",
61
+ gap: 3,
62
+ marginLeft: 4,
63
+ padding: "0 6px",
64
+ borderRadius: 99,
65
+ border: `1px solid ${tint(color, 0.55)}`,
66
+ background: tint(color, 0.08),
67
+ color: GLASS.textMuted,
68
+ fontSize: "0.78em",
69
+ fontWeight: 600,
70
+ whiteSpace: "nowrap",
71
+ verticalAlign: "baseline",
72
+ }}
73
+ >
74
+ {spec?.icon ?? "◦"} {resolvedLabel ?? name}
75
+ </span>
76
+ );
77
+ }
78
+
79
+ /** Persona-wykonawca kroku. */
80
+ export function Person(props: Omit<PinProps, "type">) {
81
+ return <Pin type="persona" {...props} />;
82
+ }
83
+
84
+ /** Metryka przypięta do kroku. */
85
+ export function Metric(props: Omit<PinProps, "type">) {
86
+ return <Pin type="metric" {...props} />;
87
+ }
88
+
89
+ // Znacznik dla textOf w <Arc> — treść pinów nie wchodzi do etykiety kroku.
90
+ (Pin as unknown as Record<string, unknown>).__arcPin = true;
91
+ (Person as unknown as Record<string, unknown>).__arcPin = true;
92
+ (Metric as unknown as Record<string, unknown>).__arcPin = true;
93
+
94
+ /** Czy węzeł React jest komponentem przypięcia (pomijany w etykiecie). */
95
+ export function isPinElement(node: unknown): boolean {
96
+ const type = (node as { type?: unknown } | null)?.type;
97
+ return (
98
+ typeof type === "function" &&
99
+ (type as unknown as Record<string, unknown>).__arcPin === true
100
+ );
101
+ }
@@ -21,12 +21,14 @@ import {
21
21
  type NoteOccurrence,
22
22
  type Occurrence,
23
23
  type PathSeg,
24
+ type PinSpec,
24
25
  type ProcessConnector,
25
26
  type ProcessGraph,
26
27
  type ProcessModel,
27
28
  type ProcessNode,
28
29
  type ProcessRelation,
29
30
  type ProcessStep,
31
+ type ResolvedPin,
30
32
  type StepOccurrence,
31
33
  } from "./types";
32
34
 
@@ -212,36 +214,62 @@ function assignDepthLane(
212
214
  .filter((s) => !s.entryId)
213
215
  .map((s) => s.lane);
214
216
  const maxMainLane = mainLanes.length ? Math.max(...mainLanes) : 0;
215
- let aboveCount = 0;
216
- let belowCount = 0;
217
217
 
218
- entryOrder.forEach((entryId, k) => {
218
+ interface EntryPlan {
219
+ e: EntryBlock;
220
+ targetDepth: number;
221
+ targetOcc?: string;
222
+ toPhase?: string;
223
+ }
224
+ const plans: EntryPlan[] = entryOrder.map((entryId) => {
219
225
  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) => {
226
+ const target =
227
+ (e.to !== undefined ? phaseStart.get(e.to) : undefined) ?? firstPhase;
228
+ return {
229
+ e,
230
+ targetDepth: target?.depth ?? 0,
231
+ targetOcc: target?.occId,
232
+ toPhase: e.to,
233
+ };
234
+ });
235
+
236
+ // Wyprzedzenie: oś główna przesuwa się w prawo na tyle, by KAŻDY łańcuch
237
+ // Entry zmieścił się PRZED swoją fazą docelową — krawędź „continues” zawsze
238
+ // biegnie w prawo, nigdy wstecz przez diagram.
239
+ const maxLead = Math.max(
240
+ 0,
241
+ ...plans.map((p) => p.e.stepIdx.length - p.targetDepth),
242
+ );
243
+ if (maxLead > 0) {
244
+ for (const s of steps) if (!s.entryId) s.depth += maxLead;
245
+ for (const p of plans) p.targetDepth += maxLead;
246
+ }
247
+
248
+ const placeEntry = (p: EntryPlan, lane: number) => {
249
+ const L = p.e.stepIdx.length;
250
+ const start = Math.max(0, p.targetDepth - L);
251
+ p.e.stepIdx.forEach((idx, i) => {
227
252
  steps[idx].depth = start + i;
228
253
  steps[idx].lane = lane;
229
254
  });
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.
255
+ if (p.targetOcc && L > 0) {
238
256
  continueEdges.push({
239
- fromOcc: steps[e.stepIdx[L - 1]].occId,
240
- toOcc: target.occId,
241
- phaseId: "",
257
+ fromOcc: steps[p.e.stepIdx[L - 1]].occId,
258
+ toOcc: p.targetOcc,
259
+ phaseId: p.toPhase ?? "",
242
260
  });
243
261
  }
244
- });
262
+ };
263
+
264
+ // Tory naprzemiennie nad/pod osią (kolejność dokumentu), a w obrębie strony
265
+ // wejścia celujące dalej w prawo lądują bliżej osi — ich krawędzie
266
+ // „continues” nie przecinają wtedy torów pozostałych wejść.
267
+ const above = plans.filter((_, k) => k % 2 === 0);
268
+ const below = plans.filter((_, k) => k % 2 === 1);
269
+ above.sort((a, b) => b.targetDepth - a.targetDepth);
270
+ below.sort((a, b) => b.targetDepth - a.targetDepth);
271
+ above.forEach((p, i) => placeEntry(p, -(1 + i)));
272
+ below.forEach((p, i) => placeEntry(p, maxMainLane + 1 + i));
245
273
 
246
274
  return continueEdges;
247
275
  }
@@ -381,6 +409,28 @@ export function buildProcessModel(input: BuildModelInput): ProcessModel {
381
409
  return nodeById.get(nodeId);
382
410
  };
383
411
 
412
+ // Rozwiązywanie pinów przeciw katalogowi: rodzaj pinu (profil) wskazuje
413
+ // rodzaj wpisu katalogu (catalogKind, fallback: klucz pinu), wpis po name.
414
+ const catalogByKey = new Map(
415
+ (graph.catalog ?? []).map((c) => [`${c.kind} ${c.name}`, c]),
416
+ );
417
+ const resolvePin = (pin: PinSpec): ResolvedPin => {
418
+ const spec = profile.pinKinds?.[pin.kind];
419
+ const catalogKind = spec?.catalogKind ?? pin.kind;
420
+ const entry = catalogByKey.get(`${catalogKind} ${pin.name}`);
421
+ const label =
422
+ pin.label ??
423
+ (typeof entry?.data.label === "string" ? entry.data.label : undefined) ??
424
+ pin.name;
425
+ return {
426
+ kind: pin.kind,
427
+ name: pin.name,
428
+ label,
429
+ data: entry?.data,
430
+ resolved: !!entry,
431
+ };
432
+ };
433
+
384
434
  // 1) Kroki — jeden na wystąpienie.
385
435
  const steps: ProcessStep[] = stepOccs.map((o, i) => {
386
436
  const node = o.ref ? resolve(o.ref.nodeId, o.ref.kind) : undefined;
@@ -396,6 +446,7 @@ export function buildProcessModel(input: BuildModelInput): ProcessModel {
396
446
  attachments: {},
397
447
  sides: o.sides,
398
448
  expand: o.expand,
449
+ pins: (o.pins ?? []).map(resolvePin),
399
450
  notes: (notesFor.get(o.occId) ?? []).map((n) => ({
400
451
  occId: n.occId,
401
452
  side: n.side ?? ("N" as const),
@@ -115,6 +115,8 @@ function groupStep(
115
115
  },
116
116
  label: ctx.title ?? ctx.name ?? ctx.id,
117
117
  attachments: {},
118
+ // Box kontekstu nie agreguje pinów członków (v1) — szczegóły po otwarciu.
119
+ pins: [],
118
120
  notes: members.flatMap((m) => m.notes),
119
121
  depth: Math.min(...members.map((m) => m.depth)),
120
122
  lane: first.lane,
@@ -54,6 +54,19 @@ export interface AttachmentGroupSpec {
54
54
  label?: string;
55
55
  }
56
56
 
57
+ /** Rodzaj przypięcia (Pin) — wygląd badge'a i mapowanie na katalog. */
58
+ export interface PinKindSpec {
59
+ icon: string;
60
+ color: string;
61
+ /** Etykieta rodzaju (fallback: klucz). */
62
+ label?: string;
63
+ /**
64
+ * Rodzaj wpisu katalogu (klucz YAML), z którego pin się rozwiązuje —
65
+ * np. pin "persona" czyta wpisy "personas". Fallback: klucz pinu.
66
+ */
67
+ catalogKind?: string;
68
+ }
69
+
57
70
  export interface ProcessProfile {
58
71
  id: string;
59
72
  /** Rodzaje nodów tego profilu. */
@@ -62,6 +75,8 @@ export interface ProcessProfile {
62
75
  relations: Record<string, RelationSpec>;
63
76
  /** Grupy doczepień (badge/rozwinięcia) wokół kroków. */
64
77
  attachmentGroups: Record<string, AttachmentGroupSpec>;
78
+ /** Rodzaje przypięć (Pin) — persony, metryki, przyszłe. */
79
+ pinKinds?: Record<string, PinKindSpec>;
65
80
  /**
66
81
  * Mapuje propsy `<Arc>` na referencję do noda (bez walidacji istnienia —
67
82
  * tę robi model przeciw grafowi). Zwraca null, gdy propsy nie zawierają
@@ -70,11 +70,23 @@ export interface ContextInfo {
70
70
  hasCard?: boolean;
71
71
  }
72
72
 
73
+ /**
74
+ * Wpis generycznego katalogu deklaracji (arc.catalog.yml, ładowany przez
75
+ * narzędzie-hosta np. arc-map) — persony, metryki i przyszłe rodzaje.
76
+ */
77
+ export interface CatalogEntry {
78
+ kind: string;
79
+ name: string;
80
+ /** Pełny wpis YAML: name, label?, color?, description?, …dowolne pola. */
81
+ data: Record<string, unknown>;
82
+ }
83
+
73
84
  /** Kompletny graf wejściowy diagramu. */
74
85
  export interface ProcessGraph {
75
86
  nodes: ProcessNode[];
76
87
  relations: ProcessRelation[];
77
88
  contexts?: ContextInfo[];
89
+ catalog?: CatalogEntry[];
78
90
  }
79
91
 
80
92
  /** Pusty graf — wszystkie referencje będą czerwonymi chipami TODO. */
@@ -125,6 +137,27 @@ export interface PathSeg {
125
137
  // Wystąpienia (rejestrowane przez MDX w kolejności dokumentu)
126
138
  // ---------------------------------------------------------------------------
127
139
 
140
+ /**
141
+ * Przypięcie zadeklarowane w MDX wewnątrz `<Arc>` (np. `<Person name>`,
142
+ * `<Metric name>`) — rodzaj z profilu, wpis rozwiązywany z katalogu po name.
143
+ */
144
+ export interface PinSpec {
145
+ kind: string;
146
+ name: string;
147
+ /** Etykieta z MDX (fallback: label z katalogu → name). */
148
+ label?: string;
149
+ }
150
+
151
+ /** Przypięcie rozwiązane przeciw katalogowi grafu. */
152
+ export interface ResolvedPin {
153
+ kind: string;
154
+ name: string;
155
+ label: string;
156
+ /** Dane wpisu katalogu (color, description, …), gdy rozwiązany. */
157
+ data?: Record<string, unknown>;
158
+ resolved: boolean;
159
+ }
160
+
128
161
  /** Wystąpienie kroku — jedno na każdy `<Arc>` w narracji. */
129
162
  export interface StepOccurrence {
130
163
  type: "step";
@@ -136,6 +169,8 @@ export interface StepOccurrence {
136
169
  sides?: Partial<Record<string, Direction>>;
137
170
  /** Grupy relacji otwarte na starcie (`<Arc expand={["mutates"]}>`). */
138
171
  expand?: string[];
172
+ /** Przypięcia z komponentów wewnątrz `<Arc>` (Person/Metric/Pin). */
173
+ pins?: PinSpec[];
139
174
  }
140
175
 
141
176
  /** Marker `<Continue to="faza"/>` — zamyka bieżące Entry, wskazuje cel. */
@@ -184,6 +219,8 @@ export interface ProcessStep {
184
219
  sides?: Partial<Record<string, Direction>>;
185
220
  /** Grupy otwarte na starcie (z MDX). */
186
221
  expand?: string[];
222
+ /** Przypięcia rozwiązane przeciw katalogowi (persony, metryki, …). */
223
+ pins: ResolvedPin[];
187
224
  notes: StepNote[];
188
225
  /** Pozycja pozioma (czas / głębokość zależności), 0-based. */
189
226
  depth: number;