@arcote.tech/arc-map 0.7.28 → 0.7.29

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-map",
3
3
  "type": "module",
4
- "version": "0.7.28",
4
+ "version": "0.7.29",
5
5
  "private": false,
6
6
  "author": "Przemysław Krasiński [arcote.tech]",
7
7
  "description": "Arc Context Map — interactive map of context spaces, elements, event-flow and React useScope usages (dev tool)",
@@ -19,13 +19,11 @@
19
19
  "ts-morph": "^24.0.0",
20
20
  "@xyflow/react": "^12.0.0",
21
21
  "elkjs": "^0.9.3",
22
- "@mdx-js/mdx": "^3.1.0",
23
- "remark-frontmatter": "^5.0.0",
24
- "remark-gfm": "^4.0.0"
22
+ "@arcote.tech/arc-process": "^0.7.29"
25
23
  },
26
24
  "peerDependencies": {
27
- "@arcote.tech/arc": "^0.7.28",
28
- "@arcote.tech/platform": "^0.7.28",
25
+ "@arcote.tech/arc": "^0.7.29",
26
+ "@arcote.tech/platform": "^0.7.29",
29
27
  "react": "^18.0.0 || ^19.0.0",
30
28
  "react-dom": "^18.0.0 || ^19.0.0",
31
29
  "typescript": "^5.0.0"
package/src/app/nodes.tsx CHANGED
@@ -4,17 +4,6 @@
4
4
  import { Handle, Position, type NodeProps } from "@xyflow/react";
5
5
  import type { MapComponent, MapElement } from "../types";
6
6
  import { COMPONENT_COLOR, ELEMENT_COLOR, GLASS } from "./theme";
7
- import { useExpand, useFocus } from "./contexts";
8
- import type { RelKind, StepNode as StepModel } from "./usecase-model";
9
-
10
- const REL_LABEL: Record<RelKind, string> = {
11
- queries: "czyta",
12
- mutates: "mutuje",
13
- mutatedBy: "mutowane przez",
14
- queriedBy: "czytane przez",
15
- handledBy: "obsługiwane przez",
16
- usedByUI: "UI",
17
- };
18
7
 
19
8
  const KIND_ICON: Record<string, string> = {
20
9
  aggregate: "◆",
@@ -235,244 +224,8 @@ export function ComponentNode({ data, selected }: NodeProps) {
235
224
  );
236
225
  }
237
226
 
238
- // ---------------------------------------------------------------------------
239
- // Use-case data-flow nodes
240
- // ---------------------------------------------------------------------------
241
-
242
- /** A narrated `<Arc>` step on the backbone — with badges per relation kind. */
243
- export function StepNode({ data }: NodeProps) {
244
- const step = (data as { step: StepModel }).step;
245
- const expandedKinds = (data as { expanded: RelKind[] }).expanded ?? [];
246
- const { toggle } = useExpand();
247
- const { focusNodeId } = useFocus();
248
- const el = step.element;
249
- const missing = !step.exists;
250
- // Keep the declared kind even when the element does not exist yet, so we don't
251
- // lose knowledge — colour/label like any other node, just dashed + faded.
252
- const kind = (el?.kind ?? (step.ref?.kind as string) ?? "unknown") as keyof typeof ELEMENT_COLOR;
253
- const color = ELEMENT_COLOR[kind] ?? ELEMENT_COLOR.unknown;
254
- const myId = el?.id ?? step.ref?.nodeId;
255
- const dimmed = !!focusNodeId && focusNodeId !== myId;
256
- const focused = !!focusNodeId && focusNodeId === myId;
257
- const badgeKinds = Object.keys(step.badges) as RelKind[];
258
-
259
- return (
260
- <div
261
- style={{
262
- minWidth: 200,
263
- maxWidth: 230,
264
- boxSizing: "border-box",
265
- borderRadius: 14,
266
- // Dashed is reserved for MISSING (not-yet-implemented) elements. Branch
267
- // alternatives stay solid — their "alt" nature is shown by the ⋔ badge,
268
- // not by a dashed border that reads as "not implemented".
269
- border: `1px ${missing ? "dashed" : "solid"} ${color}`,
270
- borderLeft: `5px ${missing ? "dashed" : "solid"} ${color}`,
271
- background: GLASS.cardStrong,
272
- backdropFilter: GLASS.blurSoft,
273
- WebkitBackdropFilter: GLASS.blurSoft,
274
- padding: "10px 12px",
275
- position: "relative",
276
- opacity: dimmed ? 0.35 : missing ? 0.7 : 1,
277
- boxShadow: focused ? `0 0 0 2px ${color}` : GLASS.shadow,
278
- transition: "opacity 120ms ease, box-shadow 120ms ease",
279
- }}
280
- >
281
- <Handle type="target" position={Position.Left} style={{ background: color, border: "none" }} />
282
- <Handle type="source" position={Position.Right} style={{ background: color, border: "none" }} />
283
- <span
284
- style={{
285
- position: "absolute",
286
- top: -10,
287
- left: -10,
288
- width: 22,
289
- height: 22,
290
- borderRadius: 99,
291
- background: color,
292
- color: "#fff",
293
- fontSize: 12,
294
- fontWeight: 700,
295
- display: "flex",
296
- alignItems: "center",
297
- justifyContent: "center",
298
- boxShadow: "0 1px 4px rgba(0,0,0,0.25)",
299
- }}
300
- >
301
- {step.seq}
302
- </span>
303
- <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
304
- <span style={{ color, fontSize: 13 }}>{KIND_ICON[kind] ?? "•"}</span>
305
- <span style={{ fontWeight: 700, fontSize: 13, color: GLASS.text }}>
306
- {step.ref?.nodeId ?? step.label}
307
- </span>
308
- </div>
309
- <div style={{ display: "flex", alignItems: "center", gap: 6, marginTop: 2 }}>
310
- <span
311
- style={{
312
- fontSize: 9.5,
313
- fontWeight: 600,
314
- color,
315
- background: tint(color, 0.12),
316
- borderRadius: 5,
317
- padding: "1px 6px",
318
- textTransform: "uppercase",
319
- letterSpacing: 0.4,
320
- }}
321
- >
322
- {kind}
323
- </span>
324
- {step.method ? (
325
- <span style={{ fontSize: 11, color: "#3b2a55" }}>
326
- ·{step.method.name}
327
- <span style={{ color: GLASS.textFaint }}> ({step.method.kind})</span>
328
- </span>
329
- ) : null}
330
- </div>
331
- {step.label && step.label !== step.ref?.nodeId ? (
332
- <div style={{ fontSize: 11, color: GLASS.textMuted, marginTop: 3 }}>{step.label}</div>
333
- ) : null}
334
- {step.stepTitle || step.laneTitle || step.alt ? (
335
- <div style={{ display: "flex", gap: 5, marginTop: 4, flexWrap: "wrap" }}>
336
- {step.stepTitle ? (
337
- <span style={{ fontSize: 9, fontWeight: 700, color: "#1c7ed6", background: "rgba(28,126,214,0.1)", borderRadius: 4, padding: "0 5px" }}>
338
- ▸ {step.stepTitle}
339
- </span>
340
- ) : null}
341
- {step.alt ? (
342
- <span style={{ fontSize: 9, fontWeight: 700, color: "#e64980", background: "rgba(230,73,128,0.1)", borderRadius: 4, padding: "0 5px" }}>
343
- ⋔ {step.laneTitle ?? "alt"}
344
- </span>
345
- ) : step.laneTitle ? (
346
- <span style={{ fontSize: 9, fontWeight: 700, color: "#7c3aed", background: "rgba(124,58,237,0.1)", borderRadius: 4, padding: "0 5px" }}>
347
- ∥ {step.laneTitle}
348
- </span>
349
- ) : null}
350
- </div>
351
- ) : null}
352
- {badgeKinds.length ? (
353
- <div style={{ display: "flex", flexWrap: "wrap", gap: 4, marginTop: 7 }}>
354
- {badgeKinds.map((k) => {
355
- const active = expandedKinds.includes(k);
356
- const c = REL_BADGE_COLOR[k];
357
- return (
358
- <button
359
- key={k}
360
- onClick={(e) => {
361
- e.stopPropagation();
362
- toggle(step.occId, k);
363
- }}
364
- style={{
365
- fontSize: 9.5,
366
- fontWeight: 600,
367
- border: `1px solid ${c}`,
368
- background: active ? c : tint(c, 0.1),
369
- color: active ? "#fff" : c,
370
- borderRadius: 6,
371
- padding: "1px 6px",
372
- cursor: "pointer",
373
- }}
374
- title={`${REL_LABEL[k]}: ${step.badges[k]!.join(", ")}`}
375
- >
376
- {relBadgeLabel(k, el?.kind)} {step.badges[k]!.length}
377
- </button>
378
- );
379
- })}
380
- </div>
381
- ) : null}
382
- </div>
383
- );
384
- }
385
-
386
- const REL_BADGE_COLOR: Record<RelKind, string> = {
387
- queries: "#2f9e44",
388
- mutates: "#e11d48",
389
- mutatedBy: "#d97706",
390
- queriedBy: "#0ca678",
391
- handledBy: "#1c7ed6",
392
- usedByUI: "#4263eb",
393
- };
394
-
395
- function relBadgeLabel(k: RelKind, elKind?: string): string {
396
- if (k === "mutatedBy" && elKind === "event") return "emitowane przez";
397
- return REL_LABEL[k];
398
- }
399
-
400
- /** A dependency element revealed in a left/right column when a badge expands. */
401
- export function DepNode({ data }: NodeProps) {
402
- const el = (data as { element: MapElement }).element;
403
- const { focusNodeId } = useFocus();
404
- const color = ELEMENT_COLOR[el.kind] ?? "#64748b";
405
- const dimmed = !!focusNodeId && focusNodeId !== el.id;
406
- const focused = !!focusNodeId && focusNodeId === el.id;
407
- return (
408
- <div
409
- style={{
410
- width: 178,
411
- boxSizing: "border-box",
412
- borderRadius: 10,
413
- border: `1px solid ${color}`,
414
- borderLeft: `4px solid ${color}`,
415
- background: GLASS.card,
416
- padding: "6px 9px",
417
- opacity: dimmed ? 0.35 : 1,
418
- boxShadow: focused ? `0 0 0 2px ${color}` : GLASS.shadow,
419
- transition: "opacity 120ms ease",
420
- }}
421
- >
422
- <Handle type="target" position={Position.Left} style={{ background: color, border: "none" }} />
423
- <Handle type="source" position={Position.Right} style={{ background: color, border: "none" }} />
424
- <div style={{ display: "flex", gap: 5, alignItems: "center" }}>
425
- <span style={{ color, fontSize: 11 }}>{KIND_ICON[el.kind] ?? "•"}</span>
426
- <span style={{ fontWeight: 600, fontSize: 11.5, color: GLASS.text, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
427
- {el.name}
428
- </span>
429
- </div>
430
- <div style={{ fontSize: 8.5, color, textTransform: "uppercase", letterSpacing: 0.3 }}>{el.kind}</div>
431
- </div>
432
- );
433
- }
434
-
435
- /** An element on a connecting flow path between two narrated steps. Rendered
436
- * exactly like any other element node — no special "connector" marking. */
437
- export function ConnectorNode({ data }: NodeProps) {
438
- const el = (data as { element: MapElement }).element;
439
- const { focusNodeId } = useFocus();
440
- const color = ELEMENT_COLOR[el.kind] ?? "#64748b";
441
- const dimmed = !!focusNodeId && focusNodeId !== el.id;
442
- const focused = !!focusNodeId && focusNodeId === el.id;
443
- return (
444
- <div
445
- style={{
446
- width: 178,
447
- boxSizing: "border-box",
448
- borderRadius: 10,
449
- border: `1px solid ${color}`,
450
- borderLeft: `4px solid ${color}`,
451
- background: GLASS.card,
452
- padding: "6px 9px",
453
- opacity: dimmed ? 0.35 : 1,
454
- boxShadow: focused ? `0 0 0 2px ${color}` : GLASS.shadow,
455
- transition: "opacity 120ms ease",
456
- }}
457
- >
458
- <Handle type="target" position={Position.Left} style={{ background: color, border: "none" }} />
459
- <Handle type="source" position={Position.Right} style={{ background: color, border: "none" }} />
460
- <div style={{ display: "flex", gap: 5, alignItems: "center" }}>
461
- <span style={{ color, fontSize: 11 }}>{KIND_ICON[el.kind] ?? "•"}</span>
462
- <span style={{ fontWeight: 600, fontSize: 11.5, color: GLASS.text, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
463
- {el.name}
464
- </span>
465
- </div>
466
- <div style={{ fontSize: 8.5, color, textTransform: "uppercase", letterSpacing: 0.3 }}>{el.kind}</div>
467
- </div>
468
- );
469
- }
470
-
471
227
  export const nodeTypes = {
472
228
  space: SpaceNode,
473
229
  element: ElementNode,
474
230
  component: ComponentNode,
475
- step: StepNode,
476
- dep: DepNode,
477
- connector: ConnectorNode,
478
231
  };
package/src/app/theme.ts CHANGED
@@ -13,6 +13,7 @@ export const ELEMENT_COLOR: Record<ElementKind, string> = {
13
13
  listener: "#7048e8", // grape/lilac — policy / reaction
14
14
  route: "#e64980", // pink — external boundary
15
15
  page: "#4263eb", // indigo — UI / page
16
+ tool: "#0891b2", // cyan — AI tool (wołany przez LLM)
16
17
  unknown: "#868e96", // slate
17
18
  };
18
19
 
@@ -1,48 +1,37 @@
1
1
  /**
2
- * Use-case mode: folder tree | MDX prose | data-flow diagram.
2
+ * Use-case mode: drzewo folderów | ProcessDiagram (proza MDX + diagram).
3
3
  *
4
- * Selecting a use case fetches its raw MDX; rendering collects `<Arc>` refs (one
5
- * per occurrence, document order). The model builds a horizontal backbone of
6
- * steps + connectors; per-step badges expand into left/right dependency columns.
7
- * Hovering a chip highlights its step node and vice-versa.
4
+ * Wybór use case'a pobiera surowy MDX; render i cały pipeline diagramu
5
+ * (wystąpienia model collapse layout 8 kierunków) mieszka w
6
+ * @arcote.tech/arc-process arc-map dostarcza tylko drzewo, fetch i
7
+ * profil Arka z grafem mapy (`toProcessGraph`).
8
8
  */
9
9
 
10
+ import { useEffect, useMemo, useState } from "react";
10
11
  import {
11
- Background,
12
- Controls,
13
- MiniMap,
14
- ReactFlow,
15
- type Node,
16
- } from "@xyflow/react";
17
- import { useEffect, useMemo, useRef, useState } from "react";
18
- import type { MapElement, MapJson, UseCaseEntry } from "../types";
19
- import { buildUseCaseModel } from "./usecase-model";
20
- import { layoutUseCaseModel } from "./usecase-layout";
21
- import { nodeTypes } from "./nodes";
12
+ arcProfile,
13
+ localStorageStore,
14
+ toProcessGraph,
15
+ type ProcessNode,
16
+ } from "@arcote.tech/arc-process";
17
+ import { ProcessDiagram, type SourceProvider } from "@arcote.tech/arc-process/react";
18
+ import type { MapJson, UseCaseEntry } from "../types";
22
19
  import { UseCaseTree } from "./usecase-tree";
23
- import { MdxView } from "./mdx-view";
24
- import { ELEMENT_COLOR, GLASS } from "./theme";
25
- import {
26
- ExpandContext,
27
- FocusContext,
28
- MapDataContext,
29
- type ArcRegEntry,
30
- } from "./contexts";
31
-
32
- function elementIdOf(n: Node): string | undefined {
33
- const d = n.data as any;
34
- return d?.step?.element?.id ?? d?.element?.id;
35
- }
20
+ import { GLASS } from "./theme";
21
+
22
+ /** Kod źródłowy elementu z serwera mapy (`MapElement.source` → /api/source). */
23
+ const sourceProvider: SourceProvider = async (node: ProcessNode) => {
24
+ const source = (node.meta as { source?: { file: string; line?: number } })
25
+ ?.source;
26
+ if (!source?.file) return null;
27
+ const res = await fetch(`/api/source?file=${encodeURIComponent(source.file)}`);
28
+ if (!res.ok) return null;
29
+ return { file: source.file, line: source.line, code: await res.text() };
30
+ };
36
31
 
37
32
  export function UseCaseView({ map, useCases }: { map: MapJson; useCases: UseCaseEntry[] }) {
38
33
  const [selectedId, setSelectedId] = useState<string | null>(useCases[0]?.id ?? null);
39
34
  const [mdx, setMdx] = useState<string>("");
40
- const [refs, setRefs] = useState<ArcRegEntry[]>([]);
41
- const [expanded, setExpanded] = useState<Record<string, string[]>>({});
42
- const [focusNodeId, setFocusNodeId] = useState<string | null>(null);
43
- const [scrollToKey, setScrollToKey] = useState<string | null>(null);
44
-
45
- const proseRef = useRef<HTMLDivElement>(null);
46
35
 
47
36
  // Auto-select the first use case once the list loads (list arrives async).
48
37
  useEffect(() => {
@@ -52,122 +41,53 @@ export function UseCaseView({ map, useCases }: { map: MapJson; useCases: UseCase
52
41
  // Fetch raw MDX on selection.
53
42
  useEffect(() => {
54
43
  if (!selectedId) return;
55
- setRefs([]);
56
- setExpanded({});
44
+ setMdx("");
57
45
  fetch(`/api/use-cases/raw?id=${encodeURIComponent(selectedId)}`)
58
46
  .then((r) => (r.ok ? r.text() : Promise.reject(new Error(`HTTP ${r.status}`))))
59
47
  .then(setMdx)
60
48
  .catch((e) => setMdx(`# Błąd\n\nNie udało się wczytać: ${String(e)}`));
61
49
  }, [selectedId]);
62
50
 
63
- // Model from the ordered occurrences (no dedup).
64
- const model = useMemo(
65
- () =>
66
- buildUseCaseModel({
67
- map,
68
- occurrences: refs.map((r) => ({ occId: r.id, ref: r.ref, label: r.label, path: r.path })),
69
- }),
70
- [map, refs],
71
- );
72
-
73
- const layout = useMemo(
74
- () => layoutUseCaseModel({ model, map, expanded: expanded as Record<string, any> }),
75
- [model, map, expanded],
76
- );
77
-
78
- const toggle = useMemo(
79
- () => (occId: string, relKind: string) =>
80
- setExpanded((prev) => {
81
- const cur = prev[occId] ?? [];
82
- const next = cur.includes(relKind) ? cur.filter((k) => k !== relKind) : [...cur, relKind];
83
- return { ...prev, [occId]: next };
84
- }),
85
- [],
86
- );
87
-
88
- // occId step click -> scroll prose to its chip.
89
- const keyByOcc = useMemo(() => {
90
- const m = new Map<string, string>();
91
- for (const s of model.steps) if (s.ref) m.set(s.occId, s.ref.key);
92
- return m;
93
- }, [model]);
94
-
95
- // Highlighting is done inside the node components via FocusContext, so the
96
- // `nodes` array identity stays stable on hover (no re-render churn / flicker).
97
- const nodes = layout.nodes;
98
-
99
- // Prose scroll on node click.
100
- useEffect(() => {
101
- if (!scrollToKey || !proseRef.current) return;
102
- const el = proseRef.current.querySelector(`[data-arc-key="${CSS.escape(scrollToKey)}"]`);
103
- el?.scrollIntoView({ behavior: "smooth", block: "center" });
104
- setScrollToKey(null);
105
- }, [scrollToKey]);
106
-
107
- const focusState = useMemo(
108
- () => ({ focusNodeId, setFocusNodeId, scrollToKey, setScrollToKey }),
109
- [focusNodeId, scrollToKey],
110
- );
111
- const expandState = useMemo(() => ({ expanded, toggle }), [expanded, toggle]);
51
+ const graph = useMemo(() => toProcessGraph(map), [map]);
52
+ const store = useMemo(() => localStorageStore("arc-map"), []);
112
53
 
113
54
  return (
114
- <MapDataContext.Provider value={map}>
115
- <FocusContext.Provider value={focusState}>
116
- <ExpandContext.Provider value={expandState}>
117
- <div style={{ display: "grid", gridTemplateColumns: "240px minmax(360px, 1fr) minmax(460px, 1.2fr)", height: "100%" }}>
118
- {/* Tree */}
119
- <div style={{ borderRight: `1px solid ${GLASS.border}`, overflow: "auto", background: GLASS.card }}>
120
- <UseCaseTree useCases={useCases} selectedId={selectedId} onSelect={setSelectedId} />
121
- </div>
122
-
123
- {/* Prose */}
124
- <div ref={proseRef} style={{ overflow: "auto", padding: "24px 28px", borderRight: `1px solid ${GLASS.border}` }}>
125
- {selectedId ? (
126
- <MdxView key={selectedId} mdx={mdx} onRefs={setRefs} />
127
- ) : (
128
- <div style={{ color: GLASS.textMuted }}>Wybierz use-case z drzewa po lewej.</div>
129
- )}
130
- </div>
131
-
132
- {/* Diagram */}
133
- <div style={{ position: "relative" }}>
134
- <ReactFlow
135
- key={selectedId ?? "none"}
136
- nodes={nodes}
137
- edges={layout.edges}
138
- nodeTypes={nodeTypes}
139
- nodesDraggable={false}
140
- onNodeMouseEnter={(_, n) => {
141
- const id = elementIdOf(n);
142
- if (id) setFocusNodeId(id);
143
- }}
144
- onNodeMouseLeave={() => setFocusNodeId(null)}
145
- onNodeClick={(_, n) => {
146
- const k = keyByOcc.get(n.id);
147
- if (k) setScrollToKey(k);
148
- }}
149
- fitView
150
- fitViewOptions={{ padding: 0.18 }}
151
- minZoom={0.1}
152
- proOptions={{ hideAttribution: true }}
153
- >
154
- <Background color="#c9b8e8" gap={26} size={1.5} />
155
- <Controls />
156
- <MiniMap
157
- pannable
158
- zoomable
159
- nodeColor={(n) => {
160
- const d = n.data as any;
161
- const k = d?.step?.element?.kind ?? d?.element?.kind;
162
- return (k && ELEMENT_COLOR[k as MapElement["kind"]]) || (d?.step && !d.step.exists ? "#e11d48" : "#64748b");
163
- }}
164
- style={{ background: "rgba(255,255,255,0.6)", border: `1px solid ${GLASS.border}`, borderRadius: 12 }}
165
- />
166
- </ReactFlow>
167
- </div>
168
- </div>
169
- </ExpandContext.Provider>
170
- </FocusContext.Provider>
171
- </MapDataContext.Provider>
55
+ <div
56
+ style={{
57
+ display: "grid",
58
+ gridTemplateColumns: "240px 1fr",
59
+ height: "100%",
60
+ minHeight: 0,
61
+ overflow: "hidden",
62
+ }}
63
+ >
64
+ {/* Tree */}
65
+ <div
66
+ style={{
67
+ borderRight: `1px solid ${GLASS.border}`,
68
+ overflow: "auto",
69
+ minHeight: 0,
70
+ background: GLASS.card,
71
+ }}
72
+ >
73
+ <UseCaseTree useCases={useCases} selectedId={selectedId} onSelect={setSelectedId} />
74
+ </div>
75
+
76
+ {selectedId && mdx ? (
77
+ <ProcessDiagram
78
+ mdx={mdx}
79
+ profile={arcProfile}
80
+ graph={graph}
81
+ store={store}
82
+ docId={selectedId}
83
+ title={selectedId}
84
+ sourceProvider={sourceProvider}
85
+ />
86
+ ) : (
87
+ <div style={{ color: GLASS.textMuted, padding: 24 }}>
88
+ Wybierz use-case z drzewa po lewej.
89
+ </div>
90
+ )}
91
+ </div>
172
92
  );
173
93
  }
package/src/catalog.ts ADDED
Binary file
package/src/index.ts CHANGED
@@ -13,6 +13,11 @@ export type { ScanInput, ScanResult } from "./scan";
13
13
  export { buildModuleIndex } from "./module-index";
14
14
  export { discoverUseCases, parseFrontmatter } from "./use-cases/discover";
15
15
  export type { UseCasePackage, DiscoveredUseCase } from "./use-cases/discover";
16
- export { resolveRef } from "./refs";
17
- export type { ArcRefProps, ResolvedRef, ArcRefKind } from "./refs";
16
+ export {
17
+ arcProfile,
18
+ normalizePagePath,
19
+ resolveArcRef,
20
+ toProcessGraph,
21
+ } from "@arcote.tech/arc-process";
22
+ export type { ArcRefProps, ResolvedRef } from "@arcote.tech/arc-process";
18
23
  export * from "./types";
package/src/mapper.ts CHANGED
@@ -20,9 +20,11 @@ import {
20
20
  } from "@arcote.tech/arc";
21
21
  import {
22
22
  UNGROUPED_SPACE_ID,
23
+ type CatalogEntry,
23
24
  type ElementKind,
24
25
  type MapComponent,
25
26
  type MapComponentEdge,
27
+ type MapContextInfo,
26
28
  type MapEdge,
27
29
  type MapElement,
28
30
  type MapElementMethod,
@@ -46,6 +48,8 @@ export interface BuildMapInput {
46
48
  components?: MapComponent[];
47
49
  /** Page nodes (from arc `page()` registry), appended as kind `page`. */
48
50
  pages?: MapElement[];
51
+ /** Generyczny katalog deklaracji (arc.catalog.yml) — przechodzi 1:1. */
52
+ catalog?: CatalogEntry[];
49
53
  /** ISO timestamp; injected so the function stays deterministic for tests. */
50
54
  generatedAt?: string;
51
55
  }
@@ -62,6 +66,9 @@ function detectKind(el: ArcContextElementAny): ElementKind {
62
66
  if (el instanceof ArcView) return "view";
63
67
  if (el instanceof ArcListener) return "listener";
64
68
  if (el instanceof ArcRoute) return "route";
69
+ // AI tool (arc-ai) — bez instanceof (arc-map nie zależy od arc-ai);
70
+ // rozpoznanie po rozszerzalnym `types` z ArcFragmentBase.
71
+ if (typeof el.is === "function" && el.is("tool")) return "tool";
65
72
  return "unknown";
66
73
  }
67
74
 
@@ -144,6 +151,11 @@ function extractElement(
144
151
  name: el.name,
145
152
  kind,
146
153
  spaceId: spaceOf(el, module),
154
+ contextPath: (el as any).__contextPath,
155
+ source:
156
+ typeof (el as any).getSourceLocation === "function"
157
+ ? (el as any).getSourceLocation()
158
+ : undefined,
147
159
  moduleId: module?.moduleId,
148
160
  moduleName: module?.moduleName,
149
161
  scope: module?.scope,
@@ -239,6 +251,12 @@ function collectRawEdges(el: ArcContextElementAny, kind: ElementKind): RawEdge[]
239
251
  push("queries", depTargets(data.queryElements));
240
252
  push("mutates", depTargets(data.mutationElements));
241
253
 
254
+ if (kind === "tool") {
255
+ // AI tool (arc-ai) — deps przez publiczne gettery, nie `.data`.
256
+ push("queries", depTargets((el as any).queryElements));
257
+ push("mutates", depTargets((el as any).mutationElements));
258
+ }
259
+
242
260
  if (kind === "listener") {
243
261
  push("listens", depTargets((el as any).eventElements));
244
262
  }
@@ -387,6 +405,66 @@ export function buildMapJson(input: BuildMapInput): MapJson {
387
405
  edges,
388
406
  components: resolvedComponents,
389
407
  componentEdges: resolvedComponentEdges,
408
+ contexts: collectContexts(context),
409
+ catalog: input.catalog,
390
410
  generatedAt: input.generatedAt,
391
411
  };
392
412
  }
413
+
414
+ // ---------------------------------------------------------------------------
415
+ // Drzewo kontekstów
416
+ // ---------------------------------------------------------------------------
417
+
418
+ /**
419
+ * Zbierz hierarchię kontekstów dla `MapJson.contexts`:
420
+ * (a) z drzewa `ArcContext.children` (konteksty zagnieżdżone / po merge'u),
421
+ * (b) uzupełniająco ze stampów `__contextPath` elementów — działa też na
422
+ * runtime'ach, gdzie kontener drzewa nie dotarł, a stampy tak.
423
+ */
424
+ function collectContexts(context: ArcContextAny): MapContextInfo[] {
425
+ const out: MapContextInfo[] = [];
426
+ const seen = new Set<string>();
427
+
428
+ const walk = (ctx: ArcContextAny, ancestors: string[]) => {
429
+ for (const child of ctx.children ?? []) {
430
+ const path = [...ancestors, child.id];
431
+ if (!seen.has(child.id)) {
432
+ seen.add(child.id);
433
+ out.push({
434
+ id: child.id,
435
+ path,
436
+ name: child.meta?.name,
437
+ title: child.meta?.title,
438
+ description: child.meta?.description,
439
+ hasCard: child.meta?.card !== undefined || undefined,
440
+ });
441
+ }
442
+ walk(child, path);
443
+ }
444
+ };
445
+ walk(context, []);
446
+
447
+ for (const el of context.elements as ArcContextElementAny[]) {
448
+ const p = (el as any).__contextPath as string[] | undefined;
449
+ if (!p) continue;
450
+ for (let i = 0; i < p.length; i++) {
451
+ const id = p[i];
452
+ if (seen.has(id)) continue;
453
+ seen.add(id);
454
+ // Nazwa tylko, gdy kontekst faktycznie ją zadeklarował (stamp
455
+ // __contextName) — anonimowe konteksty (id z hasha) zostają bez nazwy
456
+ // i diagram ich nie zwija w boxy.
457
+ const declaredName =
458
+ id === (el as any).__contextId
459
+ ? ((el as any).__contextName as string | undefined)
460
+ : undefined;
461
+ out.push({
462
+ id,
463
+ path: p.slice(0, i + 1),
464
+ name: declaredName,
465
+ });
466
+ }
467
+ }
468
+
469
+ return out;
470
+ }