@arcote.tech/arc-map 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 +4 -6
- package/src/app/nodes.tsx +0 -247
- package/src/app/theme.ts +1 -0
- package/src/app/usecase-view.tsx +64 -144
- package/src/catalog.ts +0 -0
- package/src/index.ts +7 -2
- package/src/mapper.ts +78 -0
- package/src/module-index.ts +1 -1
- package/src/scan.ts +1 -1
- package/src/server.ts +48 -1
- package/src/types.ts +39 -0
- package/tests/catalog.test.ts +67 -0
- package/src/app/arc.tsx +0 -92
- package/src/app/contexts.tsx +0 -72
- package/src/app/mdx-view.tsx +0 -74
- package/src/app/ref-context.tsx +0 -49
- package/src/app/structure.tsx +0 -95
- package/src/app/subgraph.ts +0 -127
- package/src/app/usecase-layout.ts +0 -249
- package/src/app/usecase-model.ts +0 -346
- package/src/refs.ts +0 -121
- package/tests/refs.test.ts +0 -56
- package/tests/subgraph.test.ts +0 -54
- package/tests/usecase-model.test.ts +0 -197
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.
|
|
4
|
+
"version": "0.7.30",
|
|
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
|
-
"@
|
|
23
|
-
"remark-frontmatter": "^5.0.0",
|
|
24
|
-
"remark-gfm": "^4.0.0"
|
|
22
|
+
"@arcote.tech/arc-process": "^0.7.30"
|
|
25
23
|
},
|
|
26
24
|
"peerDependencies": {
|
|
27
|
-
"@arcote.tech/arc": "^0.7.
|
|
28
|
-
"@arcote.tech/platform": "^0.7.
|
|
25
|
+
"@arcote.tech/arc": "^0.7.30",
|
|
26
|
+
"@arcote.tech/platform": "^0.7.30",
|
|
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
|
|
package/src/app/usecase-view.tsx
CHANGED
|
@@ -1,48 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Use-case mode:
|
|
2
|
+
* Use-case mode: drzewo folderów | ProcessDiagram (proza MDX + diagram).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
} from "@
|
|
17
|
-
import {
|
|
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 {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
const
|
|
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
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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 {
|
|
17
|
-
|
|
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
|
+
}
|