@anlyx/ui 0.1.5 → 0.1.6-beta.1
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/dist/viewer/ViewerApp.js +20 -3
- package/dist/viewer/workspace/workspace.css +5895 -311
- package/dist/workspace/WorkspaceApp.d.ts +3 -2
- package/dist/workspace/WorkspaceApp.js +848 -125
- package/dist/workspace/workspace.css +5895 -311
- package/package.json +7 -4
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { BaseEdge, Handle, Position, ReactFlow, getSmoothStepPath } from "@xyflow/react";
|
|
3
|
-
import { BookOpen, Box, Braces, BriefcaseBusiness, Check, ChevronRight, Circle, Clock3, Code2, Copy, Database, Download, FileCode2, FileText, Flag, Folder, Gauge, Layers3, Languages, Lock, Minus, MousePointerClick, Network, PanelLeft, Plus, RotateCw, Search, ShieldCheck, Workflow, Zap } from "lucide-react";
|
|
4
|
-
import {
|
|
3
|
+
import { BookOpen, Box, Braces, BriefcaseBusiness, Check, ChevronRight, Circle, Clock3, Code2, Copy, Database, Download, FileCode2, FileText, Flag, Folder, Gauge, Layers3, Languages, Lock, Minus, MousePointerClick, Network, PanelLeft, Plus, RotateCw, Search, ShieldCheck, Tag, Workflow, Zap } from "lucide-react";
|
|
4
|
+
import { siExpress, siNodedotjs, siReact, siTypescript } from "simple-icons";
|
|
5
|
+
import { Fragment, createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
5
6
|
import { ScanTreeMap } from "./ScanTreeMap.js";
|
|
6
7
|
import { buildProjectWorkspaceViewModel } from "./project-view-model.js";
|
|
7
8
|
const WorkspaceLocaleContext = createContext("en");
|
|
@@ -240,13 +241,6 @@ function useWorkspaceLocale() {
|
|
|
240
241
|
function t(locale, key) {
|
|
241
242
|
return translations[locale][key] ?? translations.en[key];
|
|
242
243
|
}
|
|
243
|
-
const projectChromeLocales = [
|
|
244
|
-
{ value: "en", label: "English", shortLabel: "EN" },
|
|
245
|
-
{ value: "ko", label: "한국어", shortLabel: "KO" },
|
|
246
|
-
{ value: "zh", label: "中文", shortLabel: "ZH" },
|
|
247
|
-
{ value: "ja", label: "日本語", shortLabel: "JA" },
|
|
248
|
-
{ value: "fr", label: "Français", shortLabel: "FR" }
|
|
249
|
-
];
|
|
250
244
|
const projectChromeTranslations = {
|
|
251
245
|
en: {
|
|
252
246
|
agent: "AI Agent",
|
|
@@ -254,6 +248,10 @@ const projectChromeTranslations = {
|
|
|
254
248
|
disabled: "Disabled",
|
|
255
249
|
language: "Language",
|
|
256
250
|
lastAnalysis: "Last analysis",
|
|
251
|
+
overview: "Overview",
|
|
252
|
+
capabilities: "Capabilities",
|
|
253
|
+
dataLifecycle: "Data Lifecycle",
|
|
254
|
+
impactMap: "Impact Map",
|
|
257
255
|
map: "Map",
|
|
258
256
|
pages: "Pages",
|
|
259
257
|
source: "Source",
|
|
@@ -267,6 +265,10 @@ const projectChromeTranslations = {
|
|
|
267
265
|
disabled: "비활성",
|
|
268
266
|
language: "언어",
|
|
269
267
|
lastAnalysis: "마지막 분석",
|
|
268
|
+
overview: "Overview",
|
|
269
|
+
capabilities: "Capabilities",
|
|
270
|
+
dataLifecycle: "Data Lifecycle",
|
|
271
|
+
impactMap: "Impact Map",
|
|
270
272
|
map: "맵",
|
|
271
273
|
pages: "페이지",
|
|
272
274
|
source: "소스",
|
|
@@ -280,6 +282,10 @@ const projectChromeTranslations = {
|
|
|
280
282
|
disabled: "已停用",
|
|
281
283
|
language: "语言",
|
|
282
284
|
lastAnalysis: "最后分析",
|
|
285
|
+
overview: "Overview",
|
|
286
|
+
capabilities: "Capabilities",
|
|
287
|
+
dataLifecycle: "Data Lifecycle",
|
|
288
|
+
impactMap: "Impact Map",
|
|
283
289
|
map: "地图",
|
|
284
290
|
pages: "页面",
|
|
285
291
|
source: "来源",
|
|
@@ -293,6 +299,10 @@ const projectChromeTranslations = {
|
|
|
293
299
|
disabled: "無効",
|
|
294
300
|
language: "言語",
|
|
295
301
|
lastAnalysis: "最終分析",
|
|
302
|
+
overview: "Overview",
|
|
303
|
+
capabilities: "Capabilities",
|
|
304
|
+
dataLifecycle: "Data Lifecycle",
|
|
305
|
+
impactMap: "Impact Map",
|
|
296
306
|
map: "マップ",
|
|
297
307
|
pages: "ページ",
|
|
298
308
|
source: "ソース",
|
|
@@ -306,6 +316,10 @@ const projectChromeTranslations = {
|
|
|
306
316
|
disabled: "Désactivé",
|
|
307
317
|
language: "Langue",
|
|
308
318
|
lastAnalysis: "Dernière analyse",
|
|
319
|
+
overview: "Overview",
|
|
320
|
+
capabilities: "Capabilities",
|
|
321
|
+
dataLifecycle: "Data Lifecycle",
|
|
322
|
+
impactMap: "Impact Map",
|
|
309
323
|
map: "Carte",
|
|
310
324
|
pages: "Pages",
|
|
311
325
|
source: "Source",
|
|
@@ -342,9 +356,9 @@ const layerIcons = {
|
|
|
342
356
|
result: Flag,
|
|
343
357
|
service: Layers3
|
|
344
358
|
};
|
|
345
|
-
export function WorkspaceApp({ data, projectData, streamUrl = "/_anlyx/events/stream", initialRecords = [] }) {
|
|
359
|
+
export function WorkspaceApp({ data, projectData, projectValidationReport, streamUrl = "/_anlyx/events/stream", initialRecords = [] }) {
|
|
346
360
|
if (projectData) {
|
|
347
|
-
return _jsx(ProjectWorkspacePreview, { data: projectData });
|
|
361
|
+
return (_jsx(ProjectWorkspacePreview, { data: projectData, ...(projectValidationReport ? { validationReport: projectValidationReport } : {}) }));
|
|
348
362
|
}
|
|
349
363
|
if (!data) {
|
|
350
364
|
return (_jsx("main", { className: "project-workspace-preview", role: "application", "aria-label": "Anlyx workspace", children: _jsxs("section", { className: "project-workspace-preview__empty", children: [_jsx("strong", { children: "No Anlyx project data loaded" }), _jsx("span", { children: "Start the local 4777 viewer with anlyx.project.json." })] }) }));
|
|
@@ -411,8 +425,9 @@ function LegacyWorkspaceApp({ data, streamUrl, initialRecords }) {
|
|
|
411
425
|
}, [streamUrl]);
|
|
412
426
|
return (_jsx(WorkspaceLocaleContext.Provider, { value: locale, children: _jsxs("main", { className: `live-workspace live-workspace--${activeView === "flows" ? tab : activeView}`, role: "application", "aria-label": t(locale, "appLabel"), children: [_jsx(WorkspaceSidebar, { activeView: activeView, locale: locale, onLocaleChange: setLocale, onViewChange: setActiveView }), _jsx("section", { className: "workspace-main", children: activeView === "json" ? (_jsx(JsonReaderView, { data: data })) : activeView === "map" ? (_jsx(ScanTreeMap, { data: data })) : (_jsxs("div", { className: "timing-layout", id: "workspace", children: [_jsxs("div", { className: "timing-content", children: [_jsx(WorkspaceHeader, { data: data, record: selectedRecord, recordCount: records.length, activeTab: tab, onTabChange: setTab }), _jsx(RequestContextPanel, { data: data, pageContext: pageContext, record: selectedRecord }), selectedRecord ? (_jsx(WorkspaceContent, { record: selectedRecord, tab: tab })) : (_jsx(EmptyWorkspace, { pageContext: pageContext }))] }), _jsx(FlowInspector, { pageContext: pageContext, record: selectedRecord, records: scopedRecords, selectedId: selectedRecord?.id, onSelect: setSelectedId })] })) })] }) }));
|
|
413
427
|
}
|
|
414
|
-
function ProjectWorkspacePreview({ data }) {
|
|
428
|
+
function ProjectWorkspacePreview({ data, validationReport }) {
|
|
415
429
|
const [activeTab, setActiveTab] = useState("pages");
|
|
430
|
+
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
|
416
431
|
const [projectLocale, setProjectLocale] = useState(() => projectDefaultLocale(data));
|
|
417
432
|
const [selectedPageId, setSelectedPageId] = useState(data.pages[0]?.id);
|
|
418
433
|
const model = useMemo(() => buildProjectWorkspaceViewModel(data, selectedPageId), [data, selectedPageId]);
|
|
@@ -420,7 +435,7 @@ function ProjectWorkspacePreview({ data }) {
|
|
|
420
435
|
useEffect(() => {
|
|
421
436
|
setProjectLocale(projectDefaultLocale(data));
|
|
422
437
|
}, [data]);
|
|
423
|
-
return (_jsxs("main", { className: "project-workspace-preview", role: "application", "aria-label": "Anlyx project workspace", children: [_jsx(ProjectTopBar, { model: model, locale: projectLocale, onLocaleChange: setProjectLocale }), _jsx(
|
|
438
|
+
return (_jsxs("main", { className: "project-workspace-preview", role: "application", "aria-label": "Anlyx project workspace", children: [_jsx(ProjectTopBar, { model: model, locale: projectLocale, onLocaleChange: setProjectLocale }), _jsxs("div", { className: `project-workspace-shell${isSidebarCollapsed ? " is-sidebar-collapsed" : ""}`, children: [_jsx(ProjectSidebar, { activeTab: activeTab, isCollapsed: isSidebarCollapsed, locale: projectLocale, onTabChange: setActiveTab, onToggleCollapse: () => setIsSidebarCollapsed((value) => !value) }), _jsx("div", { className: "project-workspace-surface", children: activeTab === "pages" ? (_jsx(ProjectPagesWorkspace, { data: data, model: model, ...(validationReport ? { validationReport } : {}), selectedPageId: model.selectedPage?.page.id, onPageSelect: setSelectedPageId, onOpenMap: () => setActiveTab("map") })) : activeTab === "map" ? (_jsx(ProjectMapView, { data: data, model: model })) : activeTab === "overview" ? (_jsx(ProjectOverviewView, { data: data })) : activeTab === "capabilities" ? (_jsx(ProjectCapabilitiesView, { data: data, model: model })) : (_jsx(ProjectJsonView, { data: data, rawJson: rawJson, model: model, ...(validationReport ? { validationReport } : {}) })) })] }), _jsx(ProjectStatusBar, { data: data, locale: projectLocale, model: model, ...(validationReport ? { validationReport } : {}) })] }));
|
|
424
439
|
}
|
|
425
440
|
function readProjectMetadataString(model, key) {
|
|
426
441
|
const value = model.project.metadata?.[key];
|
|
@@ -432,157 +447,738 @@ function projectAgentName(model) {
|
|
|
432
447
|
function projectSourceFile(model) {
|
|
433
448
|
return readProjectMetadataString(model, "sourceFile") ?? "anlyx.project.json";
|
|
434
449
|
}
|
|
435
|
-
function ProjectTopBar({
|
|
436
|
-
return (_jsxs("header", { className: "project-topbar", children: [_jsxs("div", { className: "project-topbar__brand", children: [_jsx("img", { alt: "Anlyx", src: "/workspace/anlyx-logo-transparent.png" }), _jsxs("span", { children: ["v", model.schemaVersion] })
|
|
450
|
+
function ProjectTopBar({ model }) {
|
|
451
|
+
return (_jsxs("header", { className: "project-topbar", children: [_jsxs("div", { className: "project-topbar__brand", children: [_jsx("img", { alt: "Anlyx", src: "/workspace/anlyx-logo-transparent.png" }), _jsxs("span", { children: ["v", model.schemaVersion] })] }), _jsxs("button", { className: "project-topbar__project", type: "button", children: [_jsx(Folder, { size: 17 }), _jsx("span", { children: model.project.name }), _jsx(ChevronRight, { size: 14 })] }), _jsx("div", { className: "project-topbar__spacer" })] }));
|
|
452
|
+
}
|
|
453
|
+
function projectTabLabel(tab, locale) {
|
|
454
|
+
if (tab === "pages")
|
|
455
|
+
return tp(locale, "pages");
|
|
456
|
+
if (tab === "map")
|
|
457
|
+
return tp(locale, "map");
|
|
458
|
+
if (tab === "overview")
|
|
459
|
+
return tp(locale, "overview");
|
|
460
|
+
if (tab === "capabilities")
|
|
461
|
+
return tp(locale, "capabilities");
|
|
462
|
+
return "JSON";
|
|
463
|
+
}
|
|
464
|
+
function ProjectSidebar({ activeTab, isCollapsed, locale, onTabChange, onToggleCollapse }) {
|
|
465
|
+
const items = [
|
|
466
|
+
{ icon: FileText, tab: "pages" },
|
|
467
|
+
{ icon: Network, tab: "map" },
|
|
468
|
+
{ icon: Gauge, tab: "overview" },
|
|
469
|
+
{ icon: Workflow, tab: "capabilities" },
|
|
470
|
+
{ icon: Braces, tab: "json" }
|
|
471
|
+
];
|
|
472
|
+
return (_jsxs("aside", { className: `project-sidebar${isCollapsed ? " is-collapsed" : ""}`, "aria-label": "Project navigation", children: [_jsxs("div", { className: "project-sidebar__group", children: [isCollapsed ? null : _jsx("h2", { children: "Project" }), items.map(({ icon: Icon, tab }) => (_jsxs("button", { "aria-current": activeTab === tab ? "page" : undefined, "aria-label": isCollapsed ? projectTabLabel(tab, locale) : undefined, className: activeTab === tab ? "is-active" : "", title: isCollapsed ? projectTabLabel(tab, locale) : undefined, type: "button", onClick: () => onTabChange(tab), children: [_jsx(Icon, { size: 17 }), isCollapsed ? null : _jsx("span", { children: projectTabLabel(tab, locale) })] }, tab)))] }), _jsxs("button", { "aria-label": isCollapsed ? "Expand project navigation" : "Collapse project navigation", className: "project-sidebar__collapse", title: isCollapsed ? "Expand" : undefined, type: "button", onClick: onToggleCollapse, children: [_jsx(PanelLeft, { size: 16 }), isCollapsed ? null : _jsx("span", { children: "Collapse" })] })] }));
|
|
473
|
+
}
|
|
474
|
+
function ProjectOverviewView({ data }) {
|
|
475
|
+
const overview = data.overview;
|
|
476
|
+
const stack = projectOverviewStack(overview.implementation);
|
|
477
|
+
const summary = overview.summary ??
|
|
478
|
+
"Inspect the authored Project JSON to understand pages, flows, requests, architecture, evidence, and unknowns.";
|
|
479
|
+
return (_jsx("section", { className: "anlyx-understanding anlyx-overview", "aria-label": "Project overview", children: _jsx("div", { className: "anlyx-overview-layout", children: _jsxs("div", { className: "anlyx-overview-main", children: [_jsxs("header", { className: "anlyx-readme-header", children: [_jsx("h1", { children: "Anlyx overview" }), _jsx("p", { children: summary })] }), _jsxs("section", { className: "anlyx-readme-section", "aria-label": "Built with", children: [_jsx("h2", { children: "Built with" }), stack.length > 0 ? (_jsx("div", { className: "anlyx-stack-grid", children: stack.map((item) => (_jsxs("div", { className: "anlyx-stack-item", children: [_jsx(ProjectStackIconView, { icon: item.icon, tone: item.tone }), _jsxs("div", { children: [_jsx("strong", { children: item.name }), _jsx("small", { children: item.detail })] })] }, item.name))) })) : (_jsx("p", { className: "project-muted", children: "No implementation stack authored." }))] }), _jsxs("section", { className: "anlyx-readme-section", "aria-label": "What it does", children: [_jsx("h2", { children: "What it does" }), _jsxs("div", { className: "anlyx-feature-list", children: [_jsx(ProjectReadmeFeature, { icon: FileText, title: "Inspect pages", description: "Explore the UI surfaces, components, and states that make up your app." }), _jsx(ProjectReadmeFeature, { icon: Workflow, title: "Trace request flows", description: "Follow the path from UI events to API requests and data responses." }), _jsx(ProjectReadmeFeature, { icon: Braces, title: "Review authored JSON", description: "Open the source Project JSON and understand the modeled structure." }), _jsx(ProjectReadmeFeature, { icon: Search, title: "Check evidence & unknowns", description: "See what\u2019s backed by evidence and what needs review or clarification." })] })] })] }) }) }));
|
|
480
|
+
}
|
|
481
|
+
function ProjectStackIconView({ icon, tone }) {
|
|
482
|
+
if (icon.kind === "lucide") {
|
|
483
|
+
const Icon = icon.icon;
|
|
484
|
+
return (_jsx("span", { className: `anlyx-stack-icon is-${tone}`, "aria-hidden": "true", children: _jsx(Icon, { size: 22 }) }));
|
|
485
|
+
}
|
|
486
|
+
return (_jsx("span", { className: `anlyx-stack-icon is-${tone}`, "aria-hidden": "true", style: { "--stack-icon-color": `#${icon.hex}` }, children: _jsx("svg", { role: "img", viewBox: "0 0 24 24", "aria-label": icon.title, children: _jsx("path", { d: icon.path }) }) }));
|
|
487
|
+
}
|
|
488
|
+
function ProjectReadmeFeature({ description, icon: Icon, title }) {
|
|
489
|
+
return (_jsxs("div", { className: "anlyx-readme-feature", children: [_jsx("span", { children: _jsx(Icon, { size: 20 }) }), _jsxs("div", { children: [_jsx("strong", { children: title }), _jsx("p", { children: description })] })] }));
|
|
490
|
+
}
|
|
491
|
+
function projectOverviewStack(implementation) {
|
|
492
|
+
return implementation.slice(0, 5).map((item) => ({
|
|
493
|
+
detail: item.description ?? capitalize(item.kind),
|
|
494
|
+
icon: stackIcon(item.name, item.kind),
|
|
495
|
+
name: stackDisplayName(item.name),
|
|
496
|
+
tone: stackTone(item.name, item.kind)
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
function stackDisplayName(value) {
|
|
500
|
+
if (/react/i.test(value))
|
|
501
|
+
return "React";
|
|
502
|
+
if (/typescript|ts\b/i.test(value))
|
|
503
|
+
return "TypeScript";
|
|
504
|
+
if (/node|runtime|cli/i.test(value))
|
|
505
|
+
return "Node.js";
|
|
506
|
+
if (/express/i.test(value))
|
|
507
|
+
return "Express";
|
|
508
|
+
if (/schema|project json|contract/i.test(value))
|
|
509
|
+
return "Project JSON";
|
|
510
|
+
return value;
|
|
511
|
+
}
|
|
512
|
+
function stackIcon(name, kind) {
|
|
513
|
+
const value = `${name} ${kind}`;
|
|
514
|
+
if (/react/i.test(value))
|
|
515
|
+
return simpleStackIcon(siReact);
|
|
516
|
+
if (/typescript|ts\b/i.test(value))
|
|
517
|
+
return simpleStackIcon(siTypescript);
|
|
518
|
+
if (/node|runtime|cli/i.test(value))
|
|
519
|
+
return simpleStackIcon(siNodedotjs);
|
|
520
|
+
if (/express/i.test(value))
|
|
521
|
+
return simpleStackIcon(siExpress);
|
|
522
|
+
if (/schema|json|contract/i.test(value))
|
|
523
|
+
return { kind: "lucide", icon: Braces };
|
|
524
|
+
return { kind: "lucide", icon: Code2 };
|
|
525
|
+
}
|
|
526
|
+
function simpleStackIcon(icon) {
|
|
527
|
+
return {
|
|
528
|
+
hex: icon.hex,
|
|
529
|
+
kind: "simple",
|
|
530
|
+
path: icon.path,
|
|
531
|
+
title: icon.title
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function stackTone(name, kind) {
|
|
535
|
+
const value = `${name} ${kind}`;
|
|
536
|
+
if (/react/i.test(value))
|
|
537
|
+
return "react";
|
|
538
|
+
if (/typescript|ts\b/i.test(value))
|
|
539
|
+
return "typescript";
|
|
540
|
+
if (/node|runtime|cli/i.test(value))
|
|
541
|
+
return "node";
|
|
542
|
+
if (/express/i.test(value))
|
|
543
|
+
return "express";
|
|
544
|
+
if (/schema|json|contract/i.test(value))
|
|
545
|
+
return "json";
|
|
546
|
+
return "json";
|
|
547
|
+
}
|
|
548
|
+
function ProjectCapabilitiesView({ data }) {
|
|
549
|
+
const defaultCapability = data.capabilities.find((capability) => /inspect page behavior/i.test(capability.name)) ??
|
|
550
|
+
data.capabilities[0];
|
|
551
|
+
const [selectedId, setSelectedId] = useState(defaultCapability?.id);
|
|
552
|
+
const [capabilityFilter, setCapabilityFilter] = useState("all");
|
|
553
|
+
const visibleCapabilities = data.capabilities.filter((capability) => {
|
|
554
|
+
if (capabilityFilter === "connected")
|
|
555
|
+
return capability.status === "connected";
|
|
556
|
+
if (capabilityFilter === "entry")
|
|
557
|
+
return Boolean(capability.entry);
|
|
558
|
+
if (capabilityFilter === "user")
|
|
559
|
+
return capability.actorRole === "user";
|
|
560
|
+
return true;
|
|
561
|
+
});
|
|
562
|
+
const selected = visibleCapabilities.find((capability) => capability.id === selectedId) ??
|
|
563
|
+
data.capabilities.find((capability) => capability.id === selectedId) ??
|
|
564
|
+
defaultCapability;
|
|
565
|
+
const connected = data.capabilities.filter((capability) => capability.status === "connected").length;
|
|
566
|
+
const userFacing = data.capabilities.filter((capability) => capability.actorRole !== "system").length;
|
|
567
|
+
const unresolved = data.capabilities.filter((capability) => capability.status !== "connected").length;
|
|
568
|
+
return (_jsx("section", { className: "anlyx-understanding anlyx-capabilities", "aria-label": "Project capabilities", children: _jsxs("div", { className: "anlyx-overview-layout", children: [_jsxs("div", { className: "anlyx-overview-main", children: [_jsxs("div", { className: "anlyx-metric-row", children: [_jsx(ProjectUnderstandingMetric, { icon: Workflow, label: "Total capabilities", value: String(data.capabilities.length), detail: "Authored" }), _jsx(ProjectUnderstandingMetric, { icon: Check, label: "Connected", value: String(connected), detail: `${percentage(connected, data.capabilities.length)}% of total` }), _jsx(ProjectUnderstandingMetric, { icon: MousePointerClick, label: "User-facing", value: String(userFacing), detail: `${percentage(userFacing, data.capabilities.length)}% of total` }), _jsx(ProjectUnderstandingMetric, { icon: Gauge, label: "Unresolved", value: String(unresolved), detail: "Requires review" })] }), _jsxs("div", { className: "anlyx-filter-row", "aria-label": "Filter capabilities by actor", children: [_jsx("span", { children: "Filter by" }), _jsx("button", { className: capabilityFilter === "all" ? "is-selected" : "", type: "button", onClick: () => setCapabilityFilter("all"), children: "All" }), _jsx("button", { className: capabilityFilter === "user" ? "is-selected" : "", type: "button", onClick: () => setCapabilityFilter("user"), children: "User" }), _jsx("button", { className: capabilityFilter === "entry" ? "is-selected" : "", type: "button", onClick: () => setCapabilityFilter("entry"), children: "Entry surface" }), _jsx("button", { className: capabilityFilter === "connected" ? "is-selected" : "", type: "button", onClick: () => setCapabilityFilter("connected"), children: "Connected only" })] }), data.capabilities.length === 0 ? (_jsx(ProjectSurfaceEmpty, { title: "No capabilities authored", description: "Add capabilities to describe product behavior without opening source code." })) : (_jsxs("div", { className: "anlyx-capability-table", role: "table", "aria-label": "Capabilities", children: [_jsxs("div", { className: "anlyx-capability-row is-header", role: "row", children: [_jsx("span", { children: "Actor" }), _jsx("span", { children: "Capability" }), _jsx("span", { children: "Entry surface" }), _jsx("span", { children: "Request" }), _jsx("span", { children: "Data touched" }), _jsx("span", { children: "Status" })] }), visibleCapabilities.map((capability) => (_jsxs("button", { className: `anlyx-capability-row${capability.id === selected?.id ? " is-selected" : ""}`, type: "button", onClick: () => setSelectedId(capability.id), children: [_jsxs("span", { className: `anlyx-role-badge is-${capability.actorRole}`, children: [_jsx("span", { className: `anlyx-role-dot is-${capability.actorRole}` }), capitalize(capability.actorRole)] }), _jsxs("span", { className: "anlyx-capability-name", children: [_jsx("strong", { children: capability.name }), _jsx("em", { children: capability.description ?? capability.visibleResult ?? "No capability description authored." })] }), _jsx("span", { children: capability.entry?.label ?? "No entry authored" }), _jsx("span", { children: requestSummary(data, capability.requestIds) }), _jsx("span", { children: capability.dataRefs.map((ref) => ref.name).join(", ") || "No data refs" }), _jsx(ProjectStatusPill, { status: capability.status })] }, capability.id))), _jsxs("div", { className: "anlyx-capability-footer", children: ["Showing ", visibleCapabilities.length, " of ", data.capabilities.length, " capabilities"] })] }))] }), _jsx("aside", { className: "anlyx-surface-rail", "aria-label": "Capability details", children: selected ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "anlyx-rail-section is-tinted", children: [_jsx("h2", { children: selected.name }), _jsx(ProjectStatusPill, { status: selected.status })] }), _jsxs("div", { className: "anlyx-rail-section", children: [_jsx("h3", { children: "Why this matters" }), _jsx("p", { children: capabilityWhyThisMatters(selected) })] }), _jsxs("div", { className: "anlyx-rail-section", children: [_jsx("h3", { children: "Trace summary" }), _jsxs("dl", { className: "anlyx-capability-compact-facts", children: [_jsxs("div", { children: [_jsx("dt", { children: "Actor" }), _jsx("dd", { children: capitalize(selected.actorRole) })] }), _jsxs("div", { children: [_jsx("dt", { children: "Request" }), _jsx("dd", { children: requestSummary(data, selected.requestIds) })] }), _jsxs("div", { children: [_jsx("dt", { children: "Confidence" }), _jsx("dd", { children: capitalize(selected.confidence ?? "unknown") })] })] })] }), _jsxs("div", { className: "anlyx-rail-section", children: [_jsx("h3", { children: "Evidence summary" }), _jsx(ProjectCapabilityEvidenceSummary, { capability: selected })] })] })) : (_jsx(ProjectSurfaceEmpty, { title: "No capability selected", description: "Select a capability row." })) })] }) }));
|
|
569
|
+
}
|
|
570
|
+
function ProjectCapabilityEvidenceSummary({ capability }) {
|
|
571
|
+
const pageCount = new Set(capability.pageIds).size;
|
|
572
|
+
const flowCount = new Set(capability.flowIds).size;
|
|
573
|
+
const dataCount = capability.dataRefs.length;
|
|
574
|
+
return (_jsxs("div", { className: "anlyx-evidence-summary-list", children: [_jsxs("span", { children: [_jsx(FileText, { size: 14 }), pageCount, " pages analyzed"] }), _jsxs("span", { children: [_jsx(Workflow, { size: 14 }), flowCount, " flows connected"] }), _jsxs("span", { children: [_jsx(Database, { size: 14 }), dataCount, " data objects referenced"] })] }));
|
|
575
|
+
}
|
|
576
|
+
function ProjectTrustSummary({ data, validationReport }) {
|
|
577
|
+
const coverage = projectPageCoverageSummary(data, validationReport);
|
|
578
|
+
const coverageStatus = validationReport?.summary.coverageStatus ?? data.coverage?.status;
|
|
579
|
+
const sourceIssueCount = validationReport?.summary.sourceIssueCount;
|
|
580
|
+
const sourceIssueDetails = validationReport
|
|
581
|
+
? formatSourceIssueDetails(validationReport.summary.sourceIssueBreakdown)
|
|
582
|
+
: "";
|
|
583
|
+
const issueCount = validationReport?.issues.length;
|
|
584
|
+
const shouldShow = Boolean(coverage.detected) ||
|
|
585
|
+
coverageStatus === "partial" ||
|
|
586
|
+
coverageStatus === "unknown" ||
|
|
587
|
+
(sourceIssueCount ?? 0) > 0 ||
|
|
588
|
+
(issueCount ?? 0) > 0;
|
|
589
|
+
if (!shouldShow) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
return (_jsxs("section", { className: "project-trust-summary", "aria-label": "Project analysis coverage", children: [_jsxs("div", { children: [_jsx("span", { className: "project-trust-summary__eyebrow", children: "Analysis scope" }), _jsx("strong", { children: coverageStatus === "partial" ? "Partial analysis" : "Coverage summary" })] }), _jsxs("dl", { children: [_jsxs("div", { children: [_jsx("dt", { children: "Pages" }), _jsx("dd", { children: coverage.value })] }), sourceIssueCount !== undefined ? (_jsxs("div", { children: [_jsx("dt", { children: "Source issues" }), _jsxs("dd", { className: sourceIssueCount > 0 ? "is-warning" : "is-ok", children: [sourceIssueCount, sourceIssueDetails ? _jsx("small", { children: sourceIssueDetails }) : null] })] })) : null, issueCount !== undefined ? (_jsxs("div", { children: [_jsx("dt", { children: "Validation issues" }), _jsx("dd", { className: issueCount > 0 ? "is-warning" : "is-ok", children: issueCount })] })) : null] })] }));
|
|
593
|
+
}
|
|
594
|
+
function capabilityWhyThisMatters(capability) {
|
|
595
|
+
if (/inspect page behavior/i.test(capability.name)) {
|
|
596
|
+
return "This capability lets a user inspect authored pages, primary requests, selected flow layers, evidence, and unknowns—providing clarity into how data moves and where gaps exist.";
|
|
597
|
+
}
|
|
598
|
+
return capability.visibleResult ?? capability.description ?? "This capability ties a user-facing action to authored requests, data, evidence, and confidence.";
|
|
437
599
|
}
|
|
438
|
-
function
|
|
439
|
-
return (
|
|
600
|
+
function ProjectUnderstandingMetric({ detail, icon: Icon, label, value }) {
|
|
601
|
+
return (_jsxs("div", { className: "anlyx-understanding-metric", children: [_jsx("span", { children: _jsx(Icon, { size: 20 }) }), _jsxs("div", { children: [_jsx("small", { children: label }), _jsx("strong", { children: value }), _jsx("em", { children: detail })] })] }));
|
|
440
602
|
}
|
|
441
|
-
function
|
|
442
|
-
return (_jsxs("div", { className: "
|
|
603
|
+
function ProjectSurfaceEmpty({ description, title }) {
|
|
604
|
+
return (_jsxs("div", { className: "anlyx-surface-empty", children: [_jsx(Minus, { size: 18 }), _jsx("strong", { children: title }), _jsx("p", { children: description })] }));
|
|
443
605
|
}
|
|
444
|
-
function
|
|
445
|
-
return
|
|
606
|
+
function ProjectStatusPill({ status }) {
|
|
607
|
+
return _jsx("span", { className: `anlyx-status-pill is-${status}`, children: status });
|
|
446
608
|
}
|
|
447
|
-
function
|
|
448
|
-
|
|
449
|
-
|
|
609
|
+
function percentage(value, total) {
|
|
610
|
+
if (total <= 0)
|
|
611
|
+
return 0;
|
|
612
|
+
return Math.round((value / total) * 100);
|
|
613
|
+
}
|
|
614
|
+
function requestSummary(data, requestIds) {
|
|
615
|
+
const requests = requestIds
|
|
616
|
+
.map((id) => data.requests.find((request) => request.id === id))
|
|
617
|
+
.filter((request) => Boolean(request));
|
|
618
|
+
if (requests.length === 0)
|
|
619
|
+
return "No request authored";
|
|
620
|
+
return requests
|
|
621
|
+
.slice(0, 2)
|
|
622
|
+
.map((request) => `${request.method ?? "REQ"} ${request.path ?? request.label ?? request.id}`)
|
|
623
|
+
.join(", ");
|
|
624
|
+
}
|
|
625
|
+
function ProjectPagesWorkspace({ data, model, onOpenMap, onPageSelect, selectedPageId, validationReport }) {
|
|
626
|
+
const [isPageIndexCollapsed, setIsPageIndexCollapsed] = useState(false);
|
|
627
|
+
return (_jsxs("div", { className: `anlyx-pages project-pages-layout${isPageIndexCollapsed ? " is-index-collapsed" : ""}`, children: [_jsx(ProjectPageIndex, { data: data, isCollapsed: isPageIndexCollapsed, model: model, ...(validationReport ? { validationReport } : {}), selectedPageId: selectedPageId, onPageSelect: onPageSelect, onToggleCollapse: () => setIsPageIndexCollapsed((value) => !value) }), _jsx(ProjectPagesView, { data: data, model: model, ...(validationReport ? { validationReport } : {}), selectedPage: model.selectedPage, onOpenMap: onOpenMap })] }));
|
|
628
|
+
}
|
|
629
|
+
function ProjectPageIndex({ data, isCollapsed, model, onPageSelect, onToggleCollapse, selectedPageId, validationReport }) {
|
|
630
|
+
const pages = model.pageGroups.flatMap((group) => group.pages);
|
|
631
|
+
const pageCoverage = projectPageCoverageSummary(data, validationReport);
|
|
632
|
+
const [query, setQuery] = useState("");
|
|
633
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
634
|
+
const visibleGroups = normalizedQuery
|
|
635
|
+
? model.pageGroups
|
|
636
|
+
.map((group) => ({
|
|
637
|
+
...group,
|
|
638
|
+
pages: group.pages.filter((page) => page.title.toLowerCase().includes(normalizedQuery) ||
|
|
639
|
+
page.path.toLowerCase().includes(normalizedQuery))
|
|
640
|
+
}))
|
|
641
|
+
.filter((group) => group.pages.length > 0)
|
|
642
|
+
: model.pageGroups;
|
|
643
|
+
return (_jsxs("aside", { className: `anlyx-page-index project-page-index-panel${isCollapsed ? " is-collapsed" : ""}`, "aria-label": "Page index", children: [_jsxs("div", { className: "project-panel-title", children: [isCollapsed ? null : (_jsxs("div", { children: [_jsx("h2", { children: "Page Index" }), _jsx("span", { children: pageCoverage.label })] })), _jsx("button", { "aria-expanded": !isCollapsed, "aria-label": isCollapsed ? "Expand page index" : "Collapse page index", type: "button", onClick: onToggleCollapse, children: _jsx(PanelLeft, { size: 15 }) })] }), isCollapsed ? (_jsx("nav", { className: "project-page-index--collapsed", "aria-label": "Project pages", children: pages.map((page) => (_jsx("button", { "aria-label": `${page.title} ${page.path}`, className: selectedPageId === page.id ? "is-active" : "", title: `${page.title} ${page.path}`, type: "button", onClick: () => onPageSelect(page.id), children: _jsx(BookOpen, { size: 16 }) }, page.id))) })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "project-page-index__search", children: [_jsx(Search, { size: 16 }), _jsx("input", { "aria-label": "Search pages", placeholder: "Search pages...", type: "search", value: query, onChange: (event) => setQuery(event.currentTarget.value) }), _jsx("kbd", { children: "/" })] }), _jsx("nav", { className: "project-page-index", "aria-label": "Project pages", children: visibleGroups.map((group) => (_jsxs("section", { children: [_jsxs("h2", { children: [_jsx("span", { children: group.name }), _jsx("span", { children: group.pages.length })] }), group.pages.map((page) => (_jsxs("button", { className: selectedPageId === page.id ? "is-active" : "", type: "button", onClick: () => onPageSelect(page.id), children: [_jsx(BookOpen, { size: 15 }), _jsx("span", { children: page.title }), _jsx("em", { children: page.path })] }, page.id)))] }, group.id))) }), _jsxs("div", { className: "project-page-index-panel__footer", children: [_jsx("span", { children: "Total pages" }), _jsx("strong", { children: model.totals.pages })] })] }))] }));
|
|
644
|
+
}
|
|
645
|
+
function ProjectPagesView({ data, model, onOpenMap, selectedPage, validationReport }) {
|
|
646
|
+
const defaultRequestId = getDefaultPageRequestId(selectedPage);
|
|
647
|
+
const [selectedRequestId, setSelectedRequestId] = useState(defaultRequestId);
|
|
648
|
+
const [isRailOpen, setIsRailOpen] = useState(false);
|
|
450
649
|
useEffect(() => {
|
|
451
|
-
setSelectedRequestId(
|
|
452
|
-
}, [
|
|
650
|
+
setSelectedRequestId(defaultRequestId);
|
|
651
|
+
}, [defaultRequestId, selectedPage?.page.id]);
|
|
453
652
|
if (!selectedPage) {
|
|
454
653
|
return (_jsxs("section", { className: "project-workspace-preview__empty", children: [_jsx("strong", { children: "No pages authored yet" }), _jsx("span", { children: "Ask the project Agent to write pages into anlyx.project.json." })] }));
|
|
455
654
|
}
|
|
456
655
|
const selectedRequest = selectedPage.requests.find((request) => request.id === selectedRequestId) ??
|
|
457
|
-
selectedPage
|
|
656
|
+
getDefaultPageRequest(selectedPage);
|
|
458
657
|
const selectedRequestLabel = selectedRequest?.path ?? selectedRequest?.label ?? selectedRequest?.id ?? "selected request";
|
|
658
|
+
const requestGroups = groupRequestsByRole(selectedPage.requests);
|
|
459
659
|
const selectedRequestFlows = selectedRequest
|
|
460
660
|
? selectedPage.flows.filter((flow) => flow.request?.id === selectedRequest.id ||
|
|
461
661
|
(flow.request?.path && flow.request.path === selectedRequest.path))
|
|
462
662
|
: [];
|
|
463
663
|
const visibleFlows = selectedRequest ? selectedRequestFlows : selectedPage.flows;
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
664
|
+
const unknownEvidence = selectedPage.evidence.filter((item) => item.status === "not-proven" || item.status === "unknown");
|
|
665
|
+
const selectedFlow = visibleFlows[0];
|
|
666
|
+
return (_jsxs("div", { className: `project-page-workspace${isRailOpen ? "" : " is-rail-collapsed"}`, children: [_jsxs("main", { className: "anlyx-page-report project-page-workspace__main", children: [!isRailOpen ? (_jsxs("button", { className: "anlyx-rail-reopen", type: "button", onClick: () => setIsRailOpen(true), children: [_jsx(PanelLeft, { size: 14 }), "Details"] })) : null, _jsx(ProjectTrustSummary, { data: data, ...(validationReport ? { validationReport } : {}) }), _jsx(PageBrief, { selectedPage: selectedPage }), _jsxs("div", { className: "project-page-two-up", children: [_jsx(ProjectSection, { title: "Story", children: _jsx("p", { className: "project-story-copy", children: selectedPage.page.description ??
|
|
667
|
+
"No story was authored for this page yet. Ask the project Agent to describe what this page does and why it exists." }) }), _jsx(ProjectSection, { className: "anlyx-user-actions", title: "User actions", children: _jsx("div", { className: "anlyx-action-list project-action-list", children: selectedPage.features.length > 0 ? (selectedPage.features.map((feature) => (_jsx(ProjectFeatureCard, { feature: feature }, feature.id)))) : (_jsx("p", { className: "project-muted", children: "No user actions authored for this page." })) }) })] }), _jsx(RequestsByRole, { groups: requestGroups, selectedRequest: selectedRequest, onRequestSelect: setSelectedRequestId }), _jsx(ProjectSection, { className: "anlyx-flow-section", meta: selectedRequest
|
|
668
|
+
? `${selectedRequest.method ?? "HTTP"} ${selectedRequestLabel}`
|
|
669
|
+
: undefined, title: "Selected Flow", children: selectedFlow ? (_jsx(ProjectFlowTrace, { flow: selectedFlow })) : (_jsx("p", { className: "project-muted", children: "No backend flow linked to this request." })) }), _jsxs("div", { className: "anlyx-trust-unknowns project-page-two-up project-page-two-up--trust", children: [_jsx(ProjectSection, { className: "anlyx-trust-breakdown", title: "Trust Breakdown", children: _jsxs("div", { className: "anlyx-trust-grid project-evidence-row", children: [_jsx(EvidenceChip, { label: "Source-matched", total: selectedPage.evidenceSummary.total, value: selectedPage.evidenceSummary.sourceMatched }), _jsx(EvidenceChip, { label: "Agent-inferred", total: selectedPage.evidenceSummary.total, value: selectedPage.evidenceSummary.agentInferred }), _jsx(EvidenceChip, { label: "Observed", total: selectedPage.evidenceSummary.total, value: selectedPage.evidenceSummary.observed }), _jsx(EvidenceChip, { label: "Not-proven", total: selectedPage.evidenceSummary.total, value: selectedPage.evidenceSummary.notProven }), _jsx(EvidenceChip, { label: "Unknown", total: selectedPage.evidenceSummary.total, value: selectedPage.evidenceSummary.unknown })] }) }), _jsx(ProjectSection, { className: "anlyx-unknowns", title: "Unknowns", children: unknownEvidence.length > 0 ? (_jsx("ul", { className: "anlyx-unknown-list project-unknown-list", children: unknownEvidence.slice(0, 4).map((item) => (_jsx("li", { children: item.detail ?? item.label }, item.id))) })) : (_jsxs("div", { className: "anlyx-unknown-empty", children: [_jsx("p", { children: "No unknown or not-proven evidence authored for this page." }), _jsx("p", { children: "All detected elements are source-matched." })] })) })] })] }), isRailOpen ? (_jsx(PageDetailsRail, { model: model, selectedPage: selectedPage, onCollapse: () => setIsRailOpen(false), onOpenMap: onOpenMap })) : null] }));
|
|
670
|
+
}
|
|
671
|
+
function PageBrief({ selectedPage }) {
|
|
672
|
+
return (_jsx("header", { className: "project-page-brief", children: _jsx("div", { className: "project-page-brief__body", children: _jsxs("div", { className: "project-page-brief__title", children: [_jsxs("div", { children: [_jsx("h1", { children: selectedPage.page.title }), _jsx("code", { children: selectedPage.page.path })] }), _jsx("p", { children: selectedPage.page.description ??
|
|
673
|
+
"No page story was authored yet. Ask the project Agent to describe what this page does and why it exists." })] }) }) }));
|
|
674
|
+
}
|
|
675
|
+
function RequestsByRole({ groups, onRequestSelect, selectedRequest }) {
|
|
676
|
+
return (_jsx(ProjectSection, { title: "Requests", children: _jsx("div", { className: "anlyx-requests-grid project-request-columns", children: groups.some((group) => group.requests.length > 0) ? (groups.map((group) => (_jsxs("section", { className: "project-request-column", children: [_jsxs("header", { children: [_jsx("strong", { children: group.label }), _jsx("span", { children: group.requests.length })] }), group.requests.length > 0 ? (group.requests.map((request) => (_jsx(ProjectRequestCard, { isSelected: selectedRequest?.id === request.id, request: request, onSelect: () => onRequestSelect(request.id) }, request.id)))) : (_jsxs("div", { className: "anlyx-request-empty", children: [_jsx(Minus, { size: 18 }), _jsxs("strong", { children: ["No ", group.label.toLowerCase(), " requests"] }), _jsx("span", { children: "None detected for this page." })] }))] }, group.role)))) : (_jsx("p", { className: "project-muted", children: "No requests linked to this page." })) }) }));
|
|
677
|
+
}
|
|
678
|
+
function getDefaultPageRequestId(selectedPage) {
|
|
679
|
+
return getDefaultPageRequest(selectedPage)?.id;
|
|
680
|
+
}
|
|
681
|
+
function getDefaultPageRequest(selectedPage) {
|
|
682
|
+
if (!selectedPage)
|
|
683
|
+
return undefined;
|
|
684
|
+
return (selectedPage.requests.find((request) => request.role === "primary") ??
|
|
685
|
+
selectedPage.requests.find((request) => request.role === "supporting") ??
|
|
686
|
+
selectedPage.requests[0]);
|
|
687
|
+
}
|
|
688
|
+
function groupRequestsByRole(requests) {
|
|
689
|
+
const groups = [
|
|
690
|
+
{ role: "primary", label: "Primary", requests: [] },
|
|
691
|
+
{ role: "supporting", label: "Supporting", requests: [] },
|
|
692
|
+
{ role: "background", label: "Background", requests: [] },
|
|
693
|
+
{ role: "external", label: "External", requests: [] }
|
|
694
|
+
];
|
|
695
|
+
for (const request of requests) {
|
|
696
|
+
const group = groups.find((item) => item.role === request.role);
|
|
697
|
+
group?.requests.push(request);
|
|
698
|
+
}
|
|
699
|
+
const externalGroup = groups.find((group) => group.role === "external");
|
|
700
|
+
const baseGroups = groups.filter((group) => group.role !== "external");
|
|
701
|
+
return externalGroup && externalGroup.requests.length > 0
|
|
702
|
+
? [...baseGroups, externalGroup]
|
|
703
|
+
: baseGroups;
|
|
468
704
|
}
|
|
469
|
-
function ProjectSection({ children, meta, title }) {
|
|
470
|
-
return (_jsxs("section", { className:
|
|
705
|
+
function ProjectSection({ children, className, index, meta, title }) {
|
|
706
|
+
return (_jsxs("section", { className: `anlyx-report-section project-section${className ? ` ${className}` : ""}`, children: [_jsxs("header", { className: "project-section__header", children: [_jsxs("h3", { children: [index ? (_jsx("span", { className: "project-section__number", "aria-hidden": "true", children: index })) : null, _jsx("span", { children: title })] }), meta ? _jsx("span", { children: meta }) : null] }), children] }));
|
|
471
707
|
}
|
|
472
708
|
function ProjectFeatureCard({ feature }) {
|
|
473
|
-
return (
|
|
709
|
+
return (_jsx("article", { className: "anlyx-action-item project-feature-card", children: _jsxs("div", { children: [_jsx("strong", { children: feature.name }), _jsx("p", { children: feature.description ?? "No feature description authored yet." })] }) }));
|
|
474
710
|
}
|
|
475
711
|
function ProjectRequestCard({ isSelected, onSelect, request }) {
|
|
476
712
|
const label = request.path ?? request.label ?? request.id;
|
|
477
|
-
return (_jsxs("button", { className: `project-request-card project-request-card--${request.role} ${isSelected ? "is-selected" : ""}`, type: "button", onClick: onSelect, children: [
|
|
713
|
+
return (_jsxs("button", { className: `anlyx-request-card project-request-card project-request-card--${request.role} ${isSelected ? "is-selected" : ""}`, type: "button", onClick: onSelect, children: [_jsxs("header", { children: [_jsx("span", { className: `project-method-badge project-method-badge--${request.method ?? "HTTP"}`, children: request.method ?? "HTTP" }), isSelected ? _jsx(Check, { size: 15 }) : null] }), _jsx("strong", { children: label }), _jsx("p", { children: request.description ?? request.purpose }), _jsx("footer", { children: _jsx("b", { children: request.role }) })] }));
|
|
478
714
|
}
|
|
479
715
|
function ProjectFlowTrace({ flow }) {
|
|
480
716
|
const layers = flow.layers.length > 0 ? flow.layers : [];
|
|
481
|
-
|
|
717
|
+
const [expandedLayerId, setExpandedLayerId] = useState();
|
|
718
|
+
return (_jsxs("article", { className: "anlyx-flow-trace project-flow-trace", children: [_jsxs("header", { className: "project-flow-trace__header", children: [_jsx("span", { children: flow.request?.role ?? "flow" }), _jsx("strong", { children: flow.name ?? flow.request?.path ?? flow.id })] }), _jsx("div", { className: "anlyx-flow-scroll", children: _jsx("div", { className: "anlyx-flow-track project-flow-trace__layers", children: layers.map((layer, index) => (_jsxs(Fragment, { children: [_jsx(ProjectFlowLayerCard, { isExpanded: expandedLayerId === layer.id, layer: layer, onToggle: () => setExpandedLayerId((current) => (current === layer.id ? undefined : layer.id)) }), index < layers.length - 1 ? (_jsx("span", { className: "anlyx-flow-arrow", "aria-hidden": "true", children: _jsx(ChevronRight, { size: 18 }) })) : null] }, layer.id))) }) })] }));
|
|
719
|
+
}
|
|
720
|
+
function ProjectFlowLayerCard({ isExpanded, onToggle, layer }) {
|
|
721
|
+
const source = formatSourceLocation(layer.source) ?? formatLayerHint(layer);
|
|
722
|
+
return (_jsxs("button", { "aria-expanded": isExpanded, className: `anlyx-flow-node project-flow-layer project-flow-layer--${layer.kind}${isExpanded ? " is-expanded" : ""}`, title: source, type: "button", onClick: onToggle, children: [_jsx("span", { className: "anlyx-flow-layer", children: titleCase(layer.kind) }), _jsx("strong", { className: "anlyx-flow-title", children: layer.label }), _jsx("em", { className: "anlyx-flow-source", children: source }), _jsx("small", { className: "anlyx-evidence-pill", children: evidenceStatusLabel(layer.status) })] }));
|
|
723
|
+
}
|
|
724
|
+
function EvidenceChip({ label, total, value }) {
|
|
725
|
+
const percent = total > 0 ? Math.round((value / total) * 100) : 0;
|
|
726
|
+
const status = label.toLowerCase().replace(/[^a-z]+/g, "-").replace(/^-|-$/g, "");
|
|
727
|
+
return (_jsxs("span", { className: `anlyx-trust-card anlyx-trust-card--${status} project-evidence-chip`, children: [_jsxs("span", { children: [_jsx("i", { "aria-hidden": "true" }), label] }), _jsx("strong", { children: value }), _jsxs("em", { children: [percent, "%"] })] }));
|
|
728
|
+
}
|
|
729
|
+
function PageDetailsRail({ model, onCollapse, onOpenMap, selectedPage }) {
|
|
730
|
+
const page = selectedPage.page;
|
|
731
|
+
const relatedPages = getRelatedPages(model, page.metadata?.["relatedPageIds"]);
|
|
732
|
+
const tags = getStringList(page.metadata?.["tags"]);
|
|
733
|
+
const evidenceLabel = formatEvidenceSummary(selectedPage.evidenceSummary);
|
|
734
|
+
return (_jsxs("aside", { className: "anlyx-page-rail project-page-details-panel", "aria-label": "Page details", children: [_jsxs("div", { className: "anlyx-page-rail__header", children: [_jsx("h2", { children: "Page Details" }), _jsx("button", { "aria-label": "Collapse page details", type: "button", onClick: onCollapse, children: _jsx(PanelLeft, { size: 14 }) })] }), _jsxs("dl", { children: [_jsx(ProjectDetailField, { label: "Area", value: selectedPage.areaName }), _jsx(ProjectDetailField, { label: "Route", value: page.path }), _jsx(ProjectDetailField, { label: "Page Type", value: metadataString(page.metadata?.["pageType"]) ?? "Not authored" }), _jsx(ProjectDetailField, { label: "Auth Required", value: metadataBooleanLabel(page.metadata?.["authRequired"]) }), _jsx(ProjectDetailField, { label: "Layout", value: metadataString(page.metadata?.["layout"]) ?? "Not authored" }), _jsx(ProjectDetailField, { label: "Last Seen", value: formatAnalysisTime(model.project.analyzedAt) ?? "Not authored" })] }), _jsxs("section", { children: [_jsx("h3", { children: "Evidence" }), _jsx("p", { children: evidenceLabel }), page.source ? _jsx("p", { children: formatSourceLocation(page.source) }) : null] }), _jsxs("section", { children: [_jsx("h3", { children: "Related Pages" }), relatedPages.length > 0 ? (_jsx("ul", { children: relatedPages.map((relatedPage) => (_jsxs("li", { children: [_jsx("strong", { children: relatedPage.title }), _jsx("span", { children: relatedPage.path })] }, relatedPage.id))) })) : (_jsx("p", { children: "No related pages authored." }))] }), _jsxs("section", { children: [_jsx("h3", { children: "Tags" }), tags.length > 0 ? (_jsx("div", { className: "project-page-tags", children: tags.map((tag) => (_jsxs("span", { children: [_jsx(Tag, { size: 12 }), tag] }, tag))) })) : (_jsx("p", { children: "No tags authored." }))] }), _jsxs("button", { type: "button", onClick: onOpenMap, children: [_jsx(Network, { size: 15 }), "View in Map"] })] }));
|
|
735
|
+
}
|
|
736
|
+
function ProjectDetailField({ label, value }) {
|
|
737
|
+
return (_jsxs("div", { children: [_jsx("dt", { children: label }), _jsx("dd", { children: value })] }));
|
|
738
|
+
}
|
|
739
|
+
function getRelatedPages(model, value) {
|
|
740
|
+
const ids = getStringList(value);
|
|
741
|
+
const pages = model.pageGroups.flatMap((group) => group.pages);
|
|
742
|
+
return ids.flatMap((id) => {
|
|
743
|
+
const page = pages.find((item) => item.id === id);
|
|
744
|
+
return page ? [{ id: page.id, path: page.path, title: page.title }] : [];
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
function getStringList(value) {
|
|
748
|
+
return Array.isArray(value)
|
|
749
|
+
? value.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
750
|
+
: [];
|
|
751
|
+
}
|
|
752
|
+
function metadataString(value) {
|
|
753
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
754
|
+
}
|
|
755
|
+
function metadataBooleanLabel(value) {
|
|
756
|
+
if (typeof value === "boolean")
|
|
757
|
+
return value ? "Yes" : "No";
|
|
758
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
759
|
+
return value;
|
|
760
|
+
return "Not authored";
|
|
761
|
+
}
|
|
762
|
+
function formatAnalysisTime(value) {
|
|
763
|
+
if (!value)
|
|
764
|
+
return undefined;
|
|
765
|
+
const date = new Date(value);
|
|
766
|
+
if (Number.isNaN(date.getTime()))
|
|
767
|
+
return value;
|
|
768
|
+
return date.toLocaleString("en", {
|
|
769
|
+
day: "2-digit",
|
|
770
|
+
hour: "2-digit",
|
|
771
|
+
minute: "2-digit",
|
|
772
|
+
month: "short",
|
|
773
|
+
year: "numeric"
|
|
774
|
+
});
|
|
482
775
|
}
|
|
483
|
-
function
|
|
484
|
-
|
|
776
|
+
function formatEvidenceSummary(summary) {
|
|
777
|
+
if (summary.total === 0)
|
|
778
|
+
return "Not authored";
|
|
779
|
+
const strongest = summary.sourceMatched > 0
|
|
780
|
+
? "Source-matched"
|
|
781
|
+
: summary.observed > 0
|
|
782
|
+
? "Observed"
|
|
783
|
+
: summary.agentInferred > 0
|
|
784
|
+
? "Agent-inferred"
|
|
785
|
+
: summary.notProven > 0
|
|
786
|
+
? "Not-proven"
|
|
787
|
+
: "Unknown";
|
|
788
|
+
return `${strongest} (${summary.total})`;
|
|
789
|
+
}
|
|
790
|
+
function formatSourceLocation(source) {
|
|
791
|
+
if (!source)
|
|
792
|
+
return undefined;
|
|
793
|
+
const line = source.lineStart ? `:${source.lineStart}` : "";
|
|
794
|
+
const symbol = source.symbol ? ` ${source.symbol}` : "";
|
|
795
|
+
return `${source.filePath}${line}${symbol}`;
|
|
796
|
+
}
|
|
797
|
+
function formatLayerHint(layer) {
|
|
798
|
+
return metadataString(layer.metadata?.["module"]) ?? metadataString(layer.metadata?.["file"]) ?? layer.id;
|
|
485
799
|
}
|
|
486
|
-
function
|
|
487
|
-
return
|
|
800
|
+
function titleCase(value) {
|
|
801
|
+
return value
|
|
802
|
+
.split("-")
|
|
803
|
+
.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
|
|
804
|
+
.join("-");
|
|
488
805
|
}
|
|
489
806
|
function ProjectMapView({ data, model }) {
|
|
490
|
-
const
|
|
807
|
+
const map = useMemo(() => buildProjectArchitectureMap(data), [data]);
|
|
808
|
+
const [selectedNodeId, setSelectedNodeId] = useState(undefined);
|
|
809
|
+
const [inspectorOpen, setInspectorOpen] = useState(false);
|
|
810
|
+
const selectedNode = map.nodes.find((node) => node.id === selectedNodeId);
|
|
811
|
+
const focus = useMemo(() => buildProjectArchitectureFocus(map, selectedNode?.id), [map, selectedNode?.id]);
|
|
812
|
+
const selectNode = (nodeId) => {
|
|
813
|
+
setSelectedNodeId(nodeId);
|
|
814
|
+
setInspectorOpen(true);
|
|
815
|
+
};
|
|
491
816
|
if (data.architecture.nodes.length === 0) {
|
|
492
817
|
return (_jsxs("section", { className: "project-map-placeholder", "aria-label": "Project architecture map", children: [_jsxs("div", { children: [_jsx(Network, { size: 24 }), _jsx("h2", { children: "Map" }), _jsx("p", { children: "No architecture graph was authored yet. The viewer will not invent mock project data." })] }), _jsxs("dl", { children: [_jsxs("div", { children: [_jsx("dt", { children: "Nodes" }), _jsx("dd", { children: "0" })] }), _jsxs("div", { children: [_jsx("dt", { children: "Edges" }), _jsx("dd", { children: "0" })] })] })] }));
|
|
493
818
|
}
|
|
494
|
-
return (_jsxs("section", { className: "project-architecture-map
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
result: 1450,
|
|
515
|
-
unknown: 810
|
|
516
|
-
};
|
|
517
|
-
function buildProjectArchitectureReactFlow(data) {
|
|
819
|
+
return (_jsxs("section", { className: "project-architecture-map anlyx-map-shell", "aria-label": "Project architecture map", children: [_jsxs("header", { className: "project-architecture-map__toolbar", children: [_jsxs("div", { children: [_jsx("h1", { children: "Map" }), _jsx("p", { children: "Agent-authored architecture map. The viewer renders nodes and edges from project JSON." })] }), _jsx("div", { className: "anlyx-map-header-actions", children: _jsx(ProjectArchitectureMapStats, { map: map, model: model }) })] }), _jsxs("div", { className: `anlyx-map-body${inspectorOpen ? " has-inspector" : ""}`, children: [_jsx("div", { className: `project-architecture-map__canvas${selectedNode ? " is-focused" : ""}${map.isCompact ? " is-small-graph" : ""}`, "data-testid": "project-architecture-map", style: {
|
|
820
|
+
"--anlyx-map-width": `${map.width}px`,
|
|
821
|
+
"--anlyx-map-height": `${map.height}px`
|
|
822
|
+
}, children: _jsxs("div", { className: "anlyx-map-stage", children: [_jsx("svg", { "aria-hidden": "true", className: "anlyx-map-edges", height: map.height, viewBox: `0 0 ${map.width} ${map.height}`, width: map.width, children: map.edges.map((edge) => (_jsx(ProjectArchitectureMapEdge, { edge: edge, focus: focus }, edge.id))) }), _jsx("div", { className: "anlyx-map-columns", "aria-hidden": "true", children: map.columns.map((column) => (_jsxs("div", { className: "anlyx-map-column-guide", style: { left: column.x, width: column.width }, children: [_jsx("strong", { children: column.label }), _jsx("span", { children: column.count })] }, column.id))) }), map.nodes.map((node) => (_jsx(ProjectArchitectureMapNode, { focus: focus, node: node, onSelect: selectNode, selected: node.id === selectedNode?.id }, node.id))), _jsxs("div", { className: "anlyx-map-legend", children: [_jsxs("span", { children: [_jsx("i", { className: "is-primary" }), " Selected path"] }), _jsxs("span", { children: [_jsx("i", { className: "is-shared" }), " Shared dependency"] }), _jsxs("span", { children: [_jsx("i", { className: "is-muted" }), " Other / Unknown"] })] })] }) }), inspectorOpen ? (_jsx(ProjectArchitectureInspector, { focus: focus, node: selectedNode, nodesById: map.nodesById, onClose: () => {
|
|
823
|
+
setInspectorOpen(false);
|
|
824
|
+
setSelectedNodeId(undefined);
|
|
825
|
+
} })) : null] })] }));
|
|
826
|
+
}
|
|
827
|
+
const projectArchitectureColumns = [
|
|
828
|
+
{ id: "pages", label: "Pages / Features", kinds: ["frontend"] },
|
|
829
|
+
{ id: "requests", label: "Requests", kinds: ["request"] },
|
|
830
|
+
{ id: "api", label: "API", kinds: ["api"] },
|
|
831
|
+
{ id: "controllers", label: "Controllers", kinds: ["controller", "handler", "middleware"] },
|
|
832
|
+
{ id: "services", label: "Services", kinds: ["service", "cache", "queue", "job", "external"] },
|
|
833
|
+
{ id: "repository", label: "Repository / Mapper", kinds: ["repository", "mapper"] },
|
|
834
|
+
{ id: "policy", label: "Policy / Schema", kinds: ["policy"] },
|
|
835
|
+
{ id: "data", label: "Data / JSON", kinds: ["database"] },
|
|
836
|
+
{ id: "result", label: "Results", kinds: ["result", "unknown"] }
|
|
837
|
+
];
|
|
838
|
+
function buildProjectArchitectureMap(data) {
|
|
518
839
|
const nodeIds = new Set(data.architecture.nodes.map((node) => node.id));
|
|
519
840
|
const validEdges = data.architecture.edges.filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target));
|
|
841
|
+
const evidenceById = new Map(data.evidence.map((evidence) => [evidence.id, evidence]));
|
|
520
842
|
const connectionCounts = new Map();
|
|
843
|
+
const upstreamCounts = new Map();
|
|
844
|
+
const downstreamCounts = new Map();
|
|
521
845
|
validEdges.forEach((edge) => {
|
|
522
846
|
connectionCounts.set(edge.source, (connectionCounts.get(edge.source) ?? 0) + 1);
|
|
523
847
|
connectionCounts.set(edge.target, (connectionCounts.get(edge.target) ?? 0) + 1);
|
|
848
|
+
downstreamCounts.set(edge.source, (downstreamCounts.get(edge.source) ?? 0) + 1);
|
|
849
|
+
upstreamCounts.set(edge.target, (upstreamCounts.get(edge.target) ?? 0) + 1);
|
|
850
|
+
});
|
|
851
|
+
const columnIdByKind = new Map(projectArchitectureColumns.flatMap((column) => column.kinds.map((kind) => [kind, column.id])));
|
|
852
|
+
const isCompact = data.architecture.nodes.length <= 12;
|
|
853
|
+
const filledColumnWidth = isCompact ? 176 : 196;
|
|
854
|
+
const nodeHeight = isCompact ? 90 : 96;
|
|
855
|
+
const columnGap = isCompact ? 28 : 34;
|
|
856
|
+
const rowGap = isCompact ? 16 : 22;
|
|
857
|
+
const top = isCompact ? 62 : 82;
|
|
858
|
+
const left = 18;
|
|
859
|
+
const grouped = new Map();
|
|
860
|
+
for (const node of data.architecture.nodes) {
|
|
861
|
+
const columnId = columnIdByKind.get(node.kind) ?? "result";
|
|
862
|
+
const nodes = grouped.get(columnId) ?? [];
|
|
863
|
+
nodes.push(node);
|
|
864
|
+
grouped.set(columnId, nodes);
|
|
865
|
+
}
|
|
866
|
+
const columnCounts = new Map();
|
|
867
|
+
for (const column of projectArchitectureColumns) {
|
|
868
|
+
columnCounts.set(column.id, grouped.get(column.id)?.length ?? 0);
|
|
869
|
+
}
|
|
870
|
+
let currentX = left;
|
|
871
|
+
const columns = projectArchitectureColumns
|
|
872
|
+
.filter((column) => (columnCounts.get(column.id) ?? 0) > 0)
|
|
873
|
+
.map((column) => {
|
|
874
|
+
const count = columnCounts.get(column.id) ?? 0;
|
|
875
|
+
const width = filledColumnWidth;
|
|
876
|
+
const positionedColumn = {
|
|
877
|
+
...column,
|
|
878
|
+
x: currentX,
|
|
879
|
+
width,
|
|
880
|
+
count
|
|
881
|
+
};
|
|
882
|
+
currentX += width + columnGap;
|
|
883
|
+
return positionedColumn;
|
|
884
|
+
});
|
|
885
|
+
const mapNodes = [];
|
|
886
|
+
for (const column of columns) {
|
|
887
|
+
const nodes = grouped.get(column.id) ?? [];
|
|
888
|
+
nodes
|
|
889
|
+
.sort((a, b) => {
|
|
890
|
+
const degreeDelta = (connectionCounts.get(b.id) ?? 0) - (connectionCounts.get(a.id) ?? 0);
|
|
891
|
+
return degreeDelta || a.label.localeCompare(b.label);
|
|
892
|
+
})
|
|
893
|
+
.forEach((node, index) => {
|
|
894
|
+
const evidenceStatus = projectArchitectureEvidenceStatus(node.evidenceIds, evidenceById);
|
|
895
|
+
const source = formatProjectArchitectureSource(node.source);
|
|
896
|
+
mapNodes.push({
|
|
897
|
+
...node,
|
|
898
|
+
columnId: column.id,
|
|
899
|
+
x: column.x,
|
|
900
|
+
y: top + index * (nodeHeight + rowGap),
|
|
901
|
+
width: column.width,
|
|
902
|
+
height: nodeHeight,
|
|
903
|
+
connectionCount: connectionCounts.get(node.id) ?? 0,
|
|
904
|
+
upstreamCount: upstreamCounts.get(node.id) ?? 0,
|
|
905
|
+
downstreamCount: downstreamCounts.get(node.id) ?? 0,
|
|
906
|
+
evidenceStatus,
|
|
907
|
+
evidenceLabel: node.evidenceIds.length > 0 ? evidenceStatusLabel(evidenceStatus) : "Unknown",
|
|
908
|
+
subtitle: source ?? projectArchitectureNodeSubtitle(node)
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
const nodesById = new Map(mapNodes.map((node) => [node.id, node]));
|
|
913
|
+
const logicalEdges = validEdges
|
|
914
|
+
.map((edge) => {
|
|
915
|
+
const sourceNode = nodesById.get(edge.source);
|
|
916
|
+
const targetNode = nodesById.get(edge.target);
|
|
917
|
+
if (!sourceNode || !targetNode)
|
|
918
|
+
return undefined;
|
|
919
|
+
return sourceNode && targetNode ? { ...edge, sourceNode, targetNode, routeX: 0 } : undefined;
|
|
920
|
+
})
|
|
921
|
+
.filter((edge) => Boolean(edge));
|
|
922
|
+
const mapEdgesWithoutLanes = logicalEdges.filter((edge) => {
|
|
923
|
+
if (edge.sourceNode.columnId === edge.targetNode.columnId)
|
|
924
|
+
return false;
|
|
925
|
+
return edge.sourceNode.x + edge.sourceNode.width < edge.targetNode.x;
|
|
524
926
|
});
|
|
525
|
-
const
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
slotCounts.set(slotKey, slot + 1);
|
|
927
|
+
const gutterCounts = new Map();
|
|
928
|
+
const mapEdges = mapEdgesWithoutLanes.map((edge) => {
|
|
929
|
+
const sourceRight = edge.sourceNode.x + edge.sourceNode.width;
|
|
930
|
+
const gap = Math.max(1, edge.targetNode.x - sourceRight);
|
|
931
|
+
const gutterKey = `${edge.sourceNode.columnId}->${edge.targetNode.columnId}`;
|
|
932
|
+
const gutterIndex = gutterCounts.get(gutterKey) ?? 0;
|
|
933
|
+
gutterCounts.set(gutterKey, gutterIndex + 1);
|
|
934
|
+
const laneOffset = (gutterIndex - 1) * 6;
|
|
935
|
+
const routeX = sourceRight + gap / 2 + laneOffset;
|
|
535
936
|
return {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
position: {
|
|
539
|
-
x: layerX,
|
|
540
|
-
y: 54 + (domainIndex.get(domain) ?? 0) * 138 + slot * 46
|
|
541
|
-
},
|
|
542
|
-
data: {
|
|
543
|
-
node: {
|
|
544
|
-
...node,
|
|
545
|
-
connectionCount: connectionCounts.get(node.id) ?? 0
|
|
546
|
-
}
|
|
547
|
-
},
|
|
548
|
-
draggable: false,
|
|
549
|
-
selectable: false
|
|
937
|
+
...edge,
|
|
938
|
+
routeX: Math.max(sourceRight + 14, Math.min(edge.targetNode.x - 14, routeX))
|
|
550
939
|
};
|
|
551
940
|
});
|
|
941
|
+
const maxColumnCount = Math.max(1, ...columns.map((column) => column.count));
|
|
552
942
|
return {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
selectable: false
|
|
564
|
-
}))
|
|
943
|
+
columns,
|
|
944
|
+
columnCounts,
|
|
945
|
+
nodes: mapNodes,
|
|
946
|
+
edges: mapEdges,
|
|
947
|
+
logicalEdges,
|
|
948
|
+
nodesById,
|
|
949
|
+
width: Math.max(0, currentX - columnGap + left),
|
|
950
|
+
height: top + maxColumnCount * (nodeHeight + rowGap) + (isCompact ? 34 : 80),
|
|
951
|
+
nodeWidth: filledColumnWidth,
|
|
952
|
+
isCompact
|
|
565
953
|
};
|
|
566
954
|
}
|
|
567
|
-
function
|
|
568
|
-
|
|
569
|
-
|
|
955
|
+
function buildProjectArchitectureFocus(map, selectedNodeId) {
|
|
956
|
+
if (!selectedNodeId) {
|
|
957
|
+
return { nodeIds: new Set(), edgeIds: new Set(), upstreamIds: [], downstreamIds: [] };
|
|
958
|
+
}
|
|
959
|
+
const upstreamIds = collectArchitectureReachable(map.logicalEdges, selectedNodeId, "upstream");
|
|
960
|
+
const downstreamIds = collectArchitectureReachable(map.logicalEdges, selectedNodeId, "downstream");
|
|
961
|
+
const nodeIds = new Set([selectedNodeId, ...upstreamIds, ...downstreamIds]);
|
|
962
|
+
const edgeIds = new Set(map.logicalEdges
|
|
963
|
+
.filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target))
|
|
964
|
+
.map((edge) => edge.id));
|
|
965
|
+
return { nodeIds, edgeIds, upstreamIds, downstreamIds };
|
|
966
|
+
}
|
|
967
|
+
function collectArchitectureReachable(edges, nodeId, direction) {
|
|
968
|
+
const result = [];
|
|
969
|
+
const visited = new Set([nodeId]);
|
|
970
|
+
const queue = [nodeId];
|
|
971
|
+
while (queue.length > 0) {
|
|
972
|
+
const current = queue.shift() ?? "";
|
|
973
|
+
const nextEdges = direction === "downstream"
|
|
974
|
+
? edges.filter((edge) => edge.source === current)
|
|
975
|
+
: edges.filter((edge) => edge.target === current);
|
|
976
|
+
for (const edge of nextEdges) {
|
|
977
|
+
const nextId = direction === "downstream" ? edge.target : edge.source;
|
|
978
|
+
if (visited.has(nextId))
|
|
979
|
+
continue;
|
|
980
|
+
visited.add(nextId);
|
|
981
|
+
result.push(nextId);
|
|
982
|
+
queue.push(nextId);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return result;
|
|
986
|
+
}
|
|
987
|
+
function ProjectArchitectureMapStats({ map, model }) {
|
|
988
|
+
const stats = [
|
|
989
|
+
{ label: "Pages", value: model.totals.pages },
|
|
990
|
+
{ label: "Requests", value: map.columnCounts.get("requests") ?? 0 },
|
|
991
|
+
{ label: "API", value: map.columnCounts.get("api") ?? 0 },
|
|
992
|
+
{ label: "Controllers", value: map.columnCounts.get("controllers") ?? 0 },
|
|
993
|
+
{ label: "Services", value: map.columnCounts.get("services") ?? 0 },
|
|
994
|
+
{ label: "Mappers", value: map.columnCounts.get("repository") ?? 0 },
|
|
995
|
+
{ label: "Results", value: map.columnCounts.get("result") ?? 0 }
|
|
996
|
+
];
|
|
997
|
+
return (_jsx("dl", { children: stats.map((stat) => (_jsxs("div", { children: [_jsx("dt", { children: stat.label }), _jsx("dd", { children: stat.value })] }, stat.label))) }));
|
|
998
|
+
}
|
|
999
|
+
function ProjectArchitectureMapNode({ focus, node, onSelect, selected }) {
|
|
570
1000
|
const Icon = projectArchitectureNodeIcon(node.kind);
|
|
571
|
-
|
|
1001
|
+
const muted = focus.nodeIds.size > 0 && !focus.nodeIds.has(node.id);
|
|
1002
|
+
const shared = node.upstreamCount > 1 || node.downstreamCount > 1;
|
|
1003
|
+
return (_jsxs("button", { className: `anlyx-map-node${selected ? " is-selected" : ""}${muted ? " is-muted" : ""}`, onClick: () => onSelect(node.id), style: { left: node.x, top: node.y, width: node.width, minHeight: node.height }, title: `${node.label}${node.source?.filePath ? ` · ${node.source.filePath}` : ""}`, type: "button", children: [_jsx("span", { className: "anlyx-map-node__icon", "aria-hidden": "true", children: _jsx(Icon, { size: 15 }) }), _jsxs("span", { className: "anlyx-map-node__body", children: [_jsx("span", { className: "anlyx-map-node__layer", children: projectArchitectureKindLabel(node.kind) }), _jsx("strong", { children: node.displayLabel ?? node.label }), _jsx("small", { children: node.subtitle })] }), _jsx("span", { className: `anlyx-map-evidence anlyx-map-evidence--${node.evidenceStatus}`, children: node.evidenceLabel }), shared ? _jsx("span", { className: "anlyx-map-node__degree", children: node.connectionCount }) : null] }));
|
|
1004
|
+
}
|
|
1005
|
+
function ProjectArchitectureMapEdge({ edge, focus }) {
|
|
1006
|
+
const selected = focus.edgeIds.has(edge.id);
|
|
1007
|
+
const muted = focus.edgeIds.size > 0 && !selected;
|
|
1008
|
+
const sourceX = edge.sourceNode.x + edge.sourceNode.width;
|
|
1009
|
+
const sourceY = edge.sourceNode.y + edge.sourceNode.height / 2;
|
|
1010
|
+
const targetX = edge.targetNode.x;
|
|
1011
|
+
const targetY = edge.targetNode.y + edge.targetNode.height / 2;
|
|
1012
|
+
const path = createProjectArchitectureOrthogonalPath(sourceX, sourceY, edge.routeX, targetX, targetY);
|
|
1013
|
+
const role = edge.role ?? "primary";
|
|
1014
|
+
const status = projectArchitectureEdgeStatus(edge);
|
|
1015
|
+
const visualRole = status === "not-proven" || status === "unknown"
|
|
1016
|
+
? "unknown"
|
|
1017
|
+
: role === "shared" || edge.targetNode.upstreamCount > 1
|
|
1018
|
+
? "shared"
|
|
1019
|
+
: "primary";
|
|
1020
|
+
return (_jsx("path", { className: `anlyx-map-edge anlyx-map-edge--${visualRole}${selected ? " is-selected" : ""}${muted ? " is-muted" : ""}`, d: path }));
|
|
1021
|
+
}
|
|
1022
|
+
function createProjectArchitectureOrthogonalPath(sourceX, sourceY, routeX, targetX, targetY) {
|
|
1023
|
+
return `M ${sourceX} ${sourceY} H ${routeX} V ${targetY} H ${targetX}`;
|
|
1024
|
+
}
|
|
1025
|
+
function projectArchitectureEdgeStatus(edge) {
|
|
1026
|
+
if (edge.confidence === "low")
|
|
1027
|
+
return "not-proven";
|
|
1028
|
+
if (edge.sourceNode.evidenceStatus === "not-proven" || edge.targetNode.evidenceStatus === "not-proven") {
|
|
1029
|
+
return "not-proven";
|
|
1030
|
+
}
|
|
1031
|
+
if (edge.sourceNode.evidenceStatus === "unknown" || edge.targetNode.evidenceStatus === "unknown") {
|
|
1032
|
+
return "unknown";
|
|
1033
|
+
}
|
|
1034
|
+
return edge.targetNode.evidenceStatus;
|
|
1035
|
+
}
|
|
1036
|
+
function ProjectArchitectureInspector({ focus, node, nodesById, onClose }) {
|
|
1037
|
+
if (!node) {
|
|
1038
|
+
return (_jsxs("aside", { className: "anlyx-map-inspector", "aria-label": "Map inspector", children: [_jsx("button", { className: "anlyx-map-inspector__close", onClick: onClose, type: "button", children: "Close" }), _jsx("h2", { children: "No node selected" })] }));
|
|
1039
|
+
}
|
|
1040
|
+
const upstream = focus.upstreamIds.map((id) => nodesById.get(id)).filter(Boolean);
|
|
1041
|
+
const downstream = focus.downstreamIds.map((id) => nodesById.get(id)).filter(Boolean);
|
|
1042
|
+
const contract = projectArchitectureNodeContract(node);
|
|
1043
|
+
return (_jsxs("aside", { className: "anlyx-map-inspector", "aria-label": "Map inspector", children: [_jsxs("header", { children: [_jsxs("div", { children: [_jsx("span", { children: "Selected" }), _jsx("h2", { children: node.displayLabel ?? node.label })] }), _jsx("button", { className: "anlyx-map-inspector__close", onClick: onClose, type: "button", children: "Close" })] }), _jsxs("dl", { className: "anlyx-map-inspector__meta", children: [_jsxs("div", { children: [_jsx("dt", { children: "Layer" }), _jsx("dd", { children: projectArchitectureKindLabel(node.kind) })] }), _jsxs("div", { children: [_jsx("dt", { children: "Title" }), _jsx("dd", { children: node.label })] }), _jsxs("div", { children: [_jsx("dt", { children: "Path" }), _jsx("dd", { children: formatProjectArchitectureSource(node.source) ?? "Not authored" })] }), _jsxs("div", { children: [_jsx("dt", { children: "Evidence" }), _jsx("dd", { children: node.evidenceLabel })] })] }), _jsx(ProjectArchitectureContractSection, { contract: contract }), _jsxs("section", { children: [_jsx("h3", { children: "Upstream" }), _jsx(ProjectArchitectureInspectorList, { emptyLabel: "No upstream nodes.", nodes: upstream })] }), _jsxs("section", { children: [_jsx("h3", { children: "Downstream path" }), _jsx(ProjectArchitectureInspectorList, { emptyLabel: "No downstream nodes.", nodes: downstream, ordered: true })] })] }));
|
|
1044
|
+
}
|
|
1045
|
+
function ProjectArchitectureContractSection({ contract }) {
|
|
1046
|
+
const hasContract = Boolean(contract.endpoint) ||
|
|
1047
|
+
Boolean(contract.inputName) ||
|
|
1048
|
+
Boolean(contract.outputName) ||
|
|
1049
|
+
contract.relatedModels.length > 0 ||
|
|
1050
|
+
contract.transforms.length > 0 ||
|
|
1051
|
+
Boolean(contract.mapping) ||
|
|
1052
|
+
Boolean(contract.inputShape) ||
|
|
1053
|
+
Boolean(contract.outputShape);
|
|
1054
|
+
return (_jsxs("section", { className: "anlyx-map-contract", children: [_jsx("h3", { children: "Data Contract" }), hasContract ? (_jsxs("div", { className: "anlyx-map-contract__body", children: [contract.endpoint ? (_jsx(ProjectArchitectureContractField, { label: "Endpoint", value: contract.endpoint })) : null, _jsx(ProjectArchitectureContractField, { label: "Input", value: formatProjectArchitectureContractName(contract.inputName, contract.inputKind) }), _jsx(ProjectArchitectureContractField, { label: "Output", value: formatProjectArchitectureContractName(contract.outputName, contract.outputKind) }), _jsx(ProjectArchitectureShapePreview, { shape: contract.outputShape ?? contract.inputShape }), contract.mapping ? (_jsx(ProjectArchitectureContractField, { label: "Mapping", value: contract.mapping })) : null, contract.transforms.length > 0 ? (_jsxs("div", { className: "anlyx-map-contract__group", children: [_jsx("span", { children: "Transforms" }), _jsx("ul", { children: contract.transforms.slice(0, 6).map((transform) => (_jsx("li", { children: transform }, transform))) })] })) : null, contract.relatedModels.length > 0 ? (_jsxs("div", { className: "anlyx-map-contract__group", children: [_jsx("span", { children: "Models" }), _jsx("ul", { children: contract.relatedModels.slice(0, 8).map((model) => (_jsx("li", { children: model }, model))) })] })) : null] })) : (_jsx("p", { className: "project-muted", children: "No contract authored." }))] }));
|
|
1055
|
+
}
|
|
1056
|
+
function ProjectArchitectureContractField({ label, value }) {
|
|
1057
|
+
return (_jsxs("div", { className: "anlyx-map-contract__field", children: [_jsx("span", { children: label }), _jsx("strong", { children: value ?? "No contract authored" })] }));
|
|
1058
|
+
}
|
|
1059
|
+
function ProjectArchitectureShapePreview({ shape }) {
|
|
1060
|
+
if (!shape) {
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
const entries = Object.entries(shape).slice(0, 8);
|
|
1064
|
+
if (entries.length === 0) {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
return (_jsxs("div", { className: "anlyx-map-contract__shape", children: [_jsx("span", { children: "Shape" }), _jsxs("pre", { children: [entries.map(([key, value]) => `${key}: ${value}`).join("\n"), Object.keys(shape).length > entries.length ? "\n..." : ""] })] }));
|
|
572
1068
|
}
|
|
573
|
-
function
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
return
|
|
1069
|
+
function ProjectArchitectureInspectorList({ emptyLabel, nodes, ordered = false }) {
|
|
1070
|
+
const visibleNodes = nodes.filter((node) => Boolean(node));
|
|
1071
|
+
if (visibleNodes.length === 0) {
|
|
1072
|
+
return _jsx("p", { className: "project-muted", children: emptyLabel });
|
|
1073
|
+
}
|
|
1074
|
+
return (_jsx("ol", { className: ordered ? "anlyx-map-path-list" : "anlyx-map-node-list", children: visibleNodes.slice(0, 8).map((node) => (_jsxs("li", { children: [_jsx("strong", { children: node.displayLabel ?? node.label }), _jsx("span", { children: projectArchitectureKindLabel(node.kind) })] }, node.id))) }));
|
|
1075
|
+
}
|
|
1076
|
+
function projectArchitectureNodeContract(node) {
|
|
1077
|
+
const metadata = recordFromUnknown(node.metadata);
|
|
1078
|
+
const contractRoot = recordFromUnknown(metadata?.["contracts"]) ?? metadata;
|
|
1079
|
+
const input = contractItemFromUnknown(contractRoot?.["input"]);
|
|
1080
|
+
const output = contractItemFromUnknown(contractRoot?.["output"]);
|
|
1081
|
+
return {
|
|
1082
|
+
endpoint: metadataString(contractRoot?.["endpoint"]),
|
|
1083
|
+
inputName: input.name,
|
|
1084
|
+
inputKind: input.kind,
|
|
1085
|
+
inputShape: input.shape,
|
|
1086
|
+
outputName: output.name,
|
|
1087
|
+
outputKind: output.kind,
|
|
1088
|
+
outputShape: output.shape,
|
|
1089
|
+
relatedModels: getStringList(contractRoot?.["relatedModels"]).length > 0
|
|
1090
|
+
? getStringList(contractRoot?.["relatedModels"])
|
|
1091
|
+
: getStringList(contractRoot?.["models"]),
|
|
1092
|
+
transforms: getStringList(contractRoot?.["transforms"]),
|
|
1093
|
+
mapping: metadataString(contractRoot?.["mapping"])
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
function contractItemFromUnknown(value) {
|
|
1097
|
+
if (typeof value === "string") {
|
|
1098
|
+
return { name: value, kind: undefined, shape: undefined };
|
|
1099
|
+
}
|
|
1100
|
+
const record = recordFromUnknown(value);
|
|
1101
|
+
if (!record) {
|
|
1102
|
+
return { name: undefined, kind: undefined, shape: undefined };
|
|
1103
|
+
}
|
|
1104
|
+
return {
|
|
1105
|
+
name: metadataString(record["name"]),
|
|
1106
|
+
kind: metadataString(record["kind"]),
|
|
1107
|
+
shape: shapeFromUnknown(record["shape"])
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function shapeFromUnknown(value) {
|
|
1111
|
+
const record = recordFromUnknown(value);
|
|
1112
|
+
if (!record) {
|
|
1113
|
+
return undefined;
|
|
1114
|
+
}
|
|
1115
|
+
const entries = Object.entries(record)
|
|
1116
|
+
.map(([key, item]) => [key, typeof item === "string" ? item : JSON.stringify(item)])
|
|
1117
|
+
.filter((entry) => typeof entry[1] === "string");
|
|
1118
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
1119
|
+
}
|
|
1120
|
+
function recordFromUnknown(value) {
|
|
1121
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1122
|
+
return undefined;
|
|
1123
|
+
}
|
|
1124
|
+
return value;
|
|
1125
|
+
}
|
|
1126
|
+
function formatProjectArchitectureContractName(name, kind) {
|
|
1127
|
+
if (!name) {
|
|
1128
|
+
return undefined;
|
|
1129
|
+
}
|
|
1130
|
+
return kind ? `${name} (${kind})` : name;
|
|
1131
|
+
}
|
|
1132
|
+
function projectArchitectureEvidenceStatus(evidenceIds, evidenceById) {
|
|
1133
|
+
const statuses = evidenceIds
|
|
1134
|
+
.map((id) => evidenceById.get(id)?.status)
|
|
1135
|
+
.filter((status) => Boolean(status));
|
|
1136
|
+
if (statuses.includes("source-matched"))
|
|
1137
|
+
return "source-matched";
|
|
1138
|
+
if (statuses.includes("observed"))
|
|
1139
|
+
return "observed";
|
|
1140
|
+
if (statuses.includes("measured"))
|
|
1141
|
+
return "measured";
|
|
1142
|
+
if (statuses.includes("agent-inferred"))
|
|
1143
|
+
return "agent-inferred";
|
|
1144
|
+
if (statuses.includes("not-proven"))
|
|
1145
|
+
return "not-proven";
|
|
1146
|
+
return "unknown";
|
|
1147
|
+
}
|
|
1148
|
+
function projectArchitectureNodeSubtitle(node) {
|
|
1149
|
+
const module = metadataString(node.metadata?.["module"]);
|
|
1150
|
+
const path = metadataString(node.metadata?.["path"]);
|
|
1151
|
+
const detail = metadataString(node.metadata?.["detail"]);
|
|
1152
|
+
return module ?? path ?? detail ?? node.domain ?? node.id;
|
|
1153
|
+
}
|
|
1154
|
+
function formatProjectArchitectureSource(source) {
|
|
1155
|
+
if (!source)
|
|
1156
|
+
return undefined;
|
|
1157
|
+
const line = source.lineStart ? `:${source.lineStart}` : "";
|
|
1158
|
+
const symbol = source.symbol ? ` ${source.symbol}` : "";
|
|
1159
|
+
return `${source.filePath}${line}${symbol}`;
|
|
1160
|
+
}
|
|
1161
|
+
function projectArchitectureKindLabel(kind) {
|
|
1162
|
+
const labels = {
|
|
1163
|
+
frontend: "Page",
|
|
1164
|
+
request: "HTTP",
|
|
1165
|
+
api: "API",
|
|
1166
|
+
controller: "CTRL",
|
|
1167
|
+
handler: "Handler",
|
|
1168
|
+
middleware: "Middleware",
|
|
1169
|
+
service: "SRV",
|
|
1170
|
+
policy: "Policy",
|
|
1171
|
+
mapper: "Map",
|
|
1172
|
+
repository: "Repo",
|
|
1173
|
+
database: "Data",
|
|
1174
|
+
cache: "Cache",
|
|
1175
|
+
queue: "Queue",
|
|
1176
|
+
job: "Job",
|
|
1177
|
+
external: "External",
|
|
1178
|
+
result: "UI",
|
|
1179
|
+
unknown: "Unknown"
|
|
1180
|
+
};
|
|
1181
|
+
return labels[kind] ?? titleCase(kind);
|
|
586
1182
|
}
|
|
587
1183
|
function projectArchitectureNodeIcon(kind) {
|
|
588
1184
|
if (kind === "database") {
|
|
@@ -605,24 +1201,47 @@ function projectArchitectureNodeIcon(kind) {
|
|
|
605
1201
|
}
|
|
606
1202
|
return Circle;
|
|
607
1203
|
}
|
|
608
|
-
function ProjectJsonView({ data, model, rawJson }) {
|
|
609
|
-
const jsonFiles = useMemo(() => projectJsonFiles(data, model, rawJson), [data, model, rawJson]);
|
|
1204
|
+
function ProjectJsonView({ data, model, rawJson, validationReport }) {
|
|
1205
|
+
const jsonFiles = useMemo(() => projectJsonFiles(data, model, rawJson, validationReport), [data, model, rawJson, validationReport]);
|
|
610
1206
|
const [selectedJsonFileId, setSelectedJsonFileId] = useState(jsonFiles[0]?.id ?? "project");
|
|
611
1207
|
const selectedJsonFile = jsonFiles.find((file) => file.id === selectedJsonFileId) ?? jsonFiles[0];
|
|
612
1208
|
const activeJson = selectedJsonFile?.content ?? rawJson;
|
|
613
1209
|
const lines = activeJson.split("\n");
|
|
614
1210
|
const inventory = useMemo(() => projectJsonInventoryItems(data, model), [data, model]);
|
|
615
1211
|
const EditorFileIcon = selectedJsonFile?.icon ?? Code2;
|
|
1212
|
+
const validationTone = validationReport
|
|
1213
|
+
? validationReport.valid
|
|
1214
|
+
? validationReport.issues.length > 0
|
|
1215
|
+
? "amber"
|
|
1216
|
+
: "green"
|
|
1217
|
+
: "red"
|
|
1218
|
+
: "green";
|
|
1219
|
+
const validationLabel = validationReport
|
|
1220
|
+
? validationReport.valid
|
|
1221
|
+
? validationReport.issues.length > 0
|
|
1222
|
+
? "Valid with warnings"
|
|
1223
|
+
: "Valid"
|
|
1224
|
+
: "Invalid"
|
|
1225
|
+
: "Valid";
|
|
616
1226
|
return (_jsxs("section", { className: "project-json-workspace", "aria-label": "Project JSON", children: [_jsxs("aside", { className: "project-json-outline", "aria-label": "JSON files", children: [_jsx("div", { className: "project-panel-title", children: _jsx("h2", { children: "JSON Files" }) }), _jsx("span", { className: "project-badge project-badge--green", children: "AI-authored" }), _jsx("div", { className: "project-json-file-list", role: "list", children: jsonFiles.map((file) => {
|
|
617
1227
|
const Icon = file.icon;
|
|
618
1228
|
return (_jsxs("button", { className: file.id === selectedJsonFile?.id ? "is-selected" : "", type: "button", onClick: () => setSelectedJsonFileId(file.id), children: [_jsx(Icon, { size: 17 }), _jsxs("span", { children: [_jsx("strong", { children: file.name }), _jsx("small", { children: file.description })] }), _jsx("em", { children: file.countLabel })] }, file.id));
|
|
619
|
-
}) })] }), _jsxs("article", { className: "project-json-editor", "aria-label": "Raw project JSON", children: [_jsxs("header", { children: [_jsxs("span", { className: "project-json-file", children: [_jsx(EditorFileIcon, { size: 16 }), selectedJsonFile?.name ?? "anlyx.project.json"] }), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => void navigator.clipboard?.writeText(activeJson), children: [_jsx(Copy, { size: 15 }), "Copy JSON"] }), _jsx("button", { type: "button", onClick: () => downloadProjectJson(activeJson, selectedJsonFile?.name), children: _jsx(Download, { size: 15 }) })] })] }), _jsx("pre", { children: lines.map((line, index) => (_jsxs("span", { children: [_jsx("em", { children: index + 1 }), _jsx("code", { children: line || " " })] }, `${index}:${line}`))) })] }), _jsxs("aside", { className: "project-json-details", "aria-label": "JSON details", children: [_jsxs(JsonDetailsCard, { title: "Schema", children: [_jsx(ProjectDetailRow, { label: "Version", value: data.schemaVersion }), _jsx(ProjectDetailRow, { label: "Validation", value:
|
|
1229
|
+
}) })] }), _jsxs("article", { className: "project-json-editor", "aria-label": "Raw project JSON", children: [_jsxs("header", { children: [_jsxs("span", { className: "project-json-file", children: [_jsx(EditorFileIcon, { size: 16 }), selectedJsonFile?.name ?? "anlyx.project.json"] }), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => void navigator.clipboard?.writeText(activeJson), children: [_jsx(Copy, { size: 15 }), "Copy JSON"] }), _jsx("button", { type: "button", onClick: () => downloadProjectJson(activeJson, selectedJsonFile?.name), children: _jsx(Download, { size: 15 }) })] })] }), _jsx("pre", { children: lines.map((line, index) => (_jsxs("span", { children: [_jsx("em", { children: index + 1 }), _jsx("code", { children: line || " " })] }, `${index}:${line}`))) })] }), _jsxs("aside", { className: "project-json-details", "aria-label": "JSON details", children: [_jsxs(JsonDetailsCard, { title: "Schema", children: [_jsx(ProjectDetailRow, { label: "Version", value: data.schemaVersion }), _jsx(ProjectDetailRow, { label: "Validation", value: validationLabel, tone: validationTone })] }), validationReport ? (_jsxs(JsonDetailsCard, { title: "Trust checks", children: [_jsx(ProjectDetailRow, { label: "Source issues", value: String(validationReport.summary.sourceIssueCount), tone: validationReport.summary.sourceIssueCount > 0 ? "amber" : "green" }), validationReport.summary.sourceIssueBreakdown ? (_jsxs(_Fragment, { children: [_jsx(ProjectDetailRow, { label: "Missing files", value: String(validationReport.summary.sourceIssueBreakdown.missingFiles), tone: validationReport.summary.sourceIssueBreakdown.missingFiles > 0
|
|
1230
|
+
? "amber"
|
|
1231
|
+
: "green" }), _jsx(ProjectDetailRow, { label: "Line issues", value: String(validationReport.summary.sourceIssueBreakdown.placeholderLines +
|
|
1232
|
+
validationReport.summary.sourceIssueBreakdown.outOfRangeLines), tone: validationReport.summary.sourceIssueBreakdown.placeholderLines +
|
|
1233
|
+
validationReport.summary.sourceIssueBreakdown.outOfRangeLines >
|
|
1234
|
+
0
|
|
1235
|
+
? "amber"
|
|
1236
|
+
: "green" }), _jsx(ProjectDetailRow, { label: "Symbol issues", value: String(validationReport.summary.sourceIssueBreakdown.missingSymbols), tone: validationReport.summary.sourceIssueBreakdown.missingSymbols > 0
|
|
1237
|
+
? "amber"
|
|
1238
|
+
: "green" })] })) : null, _jsx(ProjectDetailRow, { label: "Coverage", value: validationReport.summary.coverageStatus, tone: validationReport.summary.coverageStatus === "partial" ? "amber" : "green" }), _jsx(ProjectDetailRow, { label: "Issues", value: String(validationReport.issues.length), tone: validationReport.issues.length > 0 ? "amber" : "green" })] })) : null, data.coverage || validationReport ? (_jsxs(JsonDetailsCard, { title: "Coverage", children: [_jsx(ProjectDetailRow, { label: "Pages", value: coverageValue(validationReport?.summary.modeled.pages ?? data.pages.length, validationReport?.summary.detected?.pages ?? data.coverage?.detected?.pages) }), _jsx(ProjectDetailRow, { label: "Requests", value: coverageValue(validationReport?.summary.modeled.requests ?? data.requests.length, data.coverage?.detected?.requests ?? data.coverage?.detected?.backendEndpoints) }), _jsx(ProjectDetailRow, { label: "Flows", value: coverageValue(validationReport?.summary.modeled.flows ?? data.flows.length, validationReport?.summary.detected?.flows ?? data.coverage?.detected?.flows) })] })) : null, _jsxs(JsonDetailsCard, { title: "Project", children: [_jsx(ProjectDetailRow, { label: "Name", value: data.project.name }), _jsx(ProjectDetailRow, { label: "ID", value: data.project.id }), _jsx(ProjectDetailRow, { label: "Root", value: data.project.root ?? "not provided" })] }), _jsx(JsonDetailsCard, { title: "Counts", children: _jsx("div", { className: "project-json-counts", children: inventory.map((item) => (_jsx(JsonInventoryItem, { item: item }, item.id))) }) })] })] }));
|
|
620
1239
|
}
|
|
621
1240
|
function JsonInventoryItem({ item }) {
|
|
622
1241
|
const Icon = item.icon;
|
|
623
1242
|
return (_jsxs("a", { className: "project-json-count-item", href: `#json-${item.id}`, children: [_jsx(Icon, { size: 16 }), _jsx("span", { children: item.label }), _jsx("strong", { children: item.value })] }));
|
|
624
1243
|
}
|
|
625
|
-
function projectJsonFiles(data, model, rawJson) {
|
|
1244
|
+
function projectJsonFiles(data, model, rawJson, validationReport) {
|
|
626
1245
|
const files = [
|
|
627
1246
|
{
|
|
628
1247
|
id: "project",
|
|
@@ -664,6 +1283,22 @@ function projectJsonFiles(data, model, rawJson) {
|
|
|
664
1283
|
content: projectJsonString(data.features),
|
|
665
1284
|
icon: FileText
|
|
666
1285
|
},
|
|
1286
|
+
{
|
|
1287
|
+
id: "overview",
|
|
1288
|
+
name: ".anlyx/project/overview.json",
|
|
1289
|
+
description: "Human project summary",
|
|
1290
|
+
countLabel: data.overview.summary ? "authored" : "empty",
|
|
1291
|
+
content: projectJsonString(data.overview),
|
|
1292
|
+
icon: BookOpen
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
id: "capabilities",
|
|
1296
|
+
name: ".anlyx/project/capabilities.json",
|
|
1297
|
+
description: "Readable product capabilities",
|
|
1298
|
+
countLabel: String(data.capabilities.length),
|
|
1299
|
+
content: projectJsonString(data.capabilities),
|
|
1300
|
+
icon: Workflow
|
|
1301
|
+
},
|
|
667
1302
|
{
|
|
668
1303
|
id: "requests",
|
|
669
1304
|
name: ".anlyx/project/requests.json",
|
|
@@ -696,6 +1331,30 @@ function projectJsonFiles(data, model, rawJson) {
|
|
|
696
1331
|
content: projectJsonString(data.evidence),
|
|
697
1332
|
icon: ShieldCheck
|
|
698
1333
|
},
|
|
1334
|
+
{
|
|
1335
|
+
id: "data-lifecycles",
|
|
1336
|
+
name: ".anlyx/project/data-lifecycles.json",
|
|
1337
|
+
description: "Core data lifecycle maps",
|
|
1338
|
+
countLabel: String(data.dataLifecycles.length),
|
|
1339
|
+
content: projectJsonString(data.dataLifecycles),
|
|
1340
|
+
icon: Database
|
|
1341
|
+
},
|
|
1342
|
+
{
|
|
1343
|
+
id: "impact-maps",
|
|
1344
|
+
name: ".anlyx/project/impact-maps.json",
|
|
1345
|
+
description: "Product impact maps",
|
|
1346
|
+
countLabel: String(data.impactMaps.length),
|
|
1347
|
+
content: projectJsonString(data.impactMaps),
|
|
1348
|
+
icon: Network
|
|
1349
|
+
},
|
|
1350
|
+
{
|
|
1351
|
+
id: "coverage",
|
|
1352
|
+
name: ".anlyx/project/coverage.json",
|
|
1353
|
+
description: "Detected and modeled coverage",
|
|
1354
|
+
countLabel: data.coverage?.status ?? "unknown",
|
|
1355
|
+
content: projectJsonString(data.coverage ?? { status: "unknown" }),
|
|
1356
|
+
icon: Gauge
|
|
1357
|
+
},
|
|
699
1358
|
{
|
|
700
1359
|
id: "dictionary",
|
|
701
1360
|
name: ".anlyx/project/dictionary.json",
|
|
@@ -715,8 +1374,33 @@ function projectJsonFiles(data, model, rawJson) {
|
|
|
715
1374
|
icon: Clock3
|
|
716
1375
|
});
|
|
717
1376
|
}
|
|
1377
|
+
if (validationReport) {
|
|
1378
|
+
files.push({
|
|
1379
|
+
id: "validation-report",
|
|
1380
|
+
name: ".anlyx/validation-report.json",
|
|
1381
|
+
description: "Source and coverage validation",
|
|
1382
|
+
countLabel: `${validationReport.issues.length} issues`,
|
|
1383
|
+
content: projectJsonString(validationReport),
|
|
1384
|
+
icon: ShieldCheck
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
718
1387
|
return files;
|
|
719
1388
|
}
|
|
1389
|
+
function coverageValue(modeled, detected) {
|
|
1390
|
+
return detected === undefined ? String(modeled) : `${modeled} / ${detected}`;
|
|
1391
|
+
}
|
|
1392
|
+
function formatSourceIssueDetails(breakdown) {
|
|
1393
|
+
if (!breakdown) {
|
|
1394
|
+
return "";
|
|
1395
|
+
}
|
|
1396
|
+
const lineIssues = breakdown.placeholderLines + breakdown.outOfRangeLines;
|
|
1397
|
+
const parts = [
|
|
1398
|
+
breakdown.missingFiles > 0 ? `${breakdown.missingFiles} missing file` : "",
|
|
1399
|
+
breakdown.missingSymbols > 0 ? `${breakdown.missingSymbols} symbol` : "",
|
|
1400
|
+
lineIssues > 0 ? `${lineIssues} line` : ""
|
|
1401
|
+
].filter(Boolean);
|
|
1402
|
+
return parts.join(" / ");
|
|
1403
|
+
}
|
|
720
1404
|
function projectJsonInventoryItems(data, model) {
|
|
721
1405
|
return [
|
|
722
1406
|
{ id: "schemaVersion", label: "schemaVersion", value: data.schemaVersion, icon: Braces },
|
|
@@ -724,6 +1408,18 @@ function projectJsonInventoryItems(data, model) {
|
|
|
724
1408
|
{ id: "areas", label: "areas", value: String(model.totals.areas), icon: BriefcaseBusiness },
|
|
725
1409
|
{ id: "pages", label: "pages", value: String(model.totals.pages), icon: BookOpen },
|
|
726
1410
|
{ id: "features", label: "features", value: String(model.totals.features), icon: FileText },
|
|
1411
|
+
{
|
|
1412
|
+
id: "overview",
|
|
1413
|
+
label: "overview",
|
|
1414
|
+
value: data.overview.summary ? "authored" : "empty",
|
|
1415
|
+
icon: BookOpen
|
|
1416
|
+
},
|
|
1417
|
+
{
|
|
1418
|
+
id: "capabilities",
|
|
1419
|
+
label: "capabilities",
|
|
1420
|
+
value: String(data.capabilities.length),
|
|
1421
|
+
icon: Workflow
|
|
1422
|
+
},
|
|
727
1423
|
{ id: "requests", label: "requests", value: String(model.totals.requests), icon: Network },
|
|
728
1424
|
{ id: "flows", label: "flows", value: String(model.totals.flows), icon: Workflow },
|
|
729
1425
|
{
|
|
@@ -733,6 +1429,18 @@ function projectJsonInventoryItems(data, model) {
|
|
|
733
1429
|
icon: Layers3
|
|
734
1430
|
},
|
|
735
1431
|
{ id: "evidence", label: "evidence", value: String(model.totals.evidence), icon: ShieldCheck },
|
|
1432
|
+
{
|
|
1433
|
+
id: "dataLifecycles",
|
|
1434
|
+
label: "dataLifecycles",
|
|
1435
|
+
value: String(data.dataLifecycles.length),
|
|
1436
|
+
icon: Database
|
|
1437
|
+
},
|
|
1438
|
+
{
|
|
1439
|
+
id: "impactMaps",
|
|
1440
|
+
label: "impactMaps",
|
|
1441
|
+
value: String(data.impactMaps.length),
|
|
1442
|
+
icon: Network
|
|
1443
|
+
},
|
|
736
1444
|
{
|
|
737
1445
|
id: "measurements",
|
|
738
1446
|
label: "measurements",
|
|
@@ -754,12 +1462,27 @@ function JsonDetailsCard({ children, title }) {
|
|
|
754
1462
|
return (_jsxs("section", { className: "project-json-details-card", children: [_jsx("h2", { children: title }), children] }));
|
|
755
1463
|
}
|
|
756
1464
|
function ProjectDetailRow({ label, tone, value }) {
|
|
757
|
-
return (_jsxs("div", { className: "project-detail-row", children: [_jsx("dt", { children: label }), _jsx("dd", { className: tone
|
|
1465
|
+
return (_jsxs("div", { className: "project-detail-row", children: [_jsx("dt", { children: label }), _jsx("dd", { className: tone ? `is-${tone}` : "", children: value })] }));
|
|
758
1466
|
}
|
|
759
|
-
function ProjectStatusBar({ locale, model }) {
|
|
1467
|
+
function ProjectStatusBar({ data, locale, model, validationReport }) {
|
|
760
1468
|
const sourceFile = projectSourceFile(model);
|
|
761
1469
|
const generatedBy = projectAgentName(model);
|
|
762
|
-
|
|
1470
|
+
const pageCoverage = projectPageCoverageSummary(data, validationReport);
|
|
1471
|
+
const coverageStatus = validationReport?.summary.coverageStatus ?? data.coverage?.status;
|
|
1472
|
+
const sourceIssueCount = validationReport?.summary.sourceIssueCount ?? 0;
|
|
1473
|
+
const hasTrustWarning = coverageStatus === "partial" || sourceIssueCount > 0;
|
|
1474
|
+
return (_jsxs("div", { className: "project-statusbar", "aria-label": "Project source status", children: [_jsxs("div", { className: "project-statusbar__provenance", children: [_jsxs("span", { children: [_jsx(Code2, { size: 15 }), tp(locale, "source"), ": ", _jsx("strong", { children: sourceFile })] }), _jsxs("span", { children: [_jsx(Zap, { size: 15 }), tp(locale, "agent"), ": ", _jsx("strong", { children: generatedBy })] })] }), _jsxs("div", { className: "project-statusbar__summary", children: [_jsx("strong", { children: pageCoverage.label }), coverageStatus ? (_jsxs("span", { className: `project-status ${hasTrustWarning ? "project-status--amber" : "project-status--green"}`, children: [_jsx(ShieldCheck, { size: 15 }), coverageStatus === "partial" ? "Partial analysis" : "Coverage checked"] })) : null, _jsxs("span", { children: [_jsx(Clock3, { size: 15 }), tp(locale, "lastAnalysis"), ": ", formatDateTime(model.project.analyzedAt)] }), _jsxs("span", { className: "project-status project-status--green", children: [_jsx(Check, { size: 15 }), tp(locale, "upToDate")] })] })] }));
|
|
1475
|
+
}
|
|
1476
|
+
function projectPageCoverageSummary(data, validationReport) {
|
|
1477
|
+
const modeled = validationReport?.summary.modeled.pages ?? data.coverage?.modeled?.pages ?? data.pages.length;
|
|
1478
|
+
const detected = validationReport?.summary.detected?.pages ?? data.coverage?.detected?.pages;
|
|
1479
|
+
const value = coverageValue(modeled, detected);
|
|
1480
|
+
return {
|
|
1481
|
+
detected,
|
|
1482
|
+
label: detected !== undefined ? `${value} pages modeled` : `${modeled} pages analyzed`,
|
|
1483
|
+
modeled,
|
|
1484
|
+
value
|
|
1485
|
+
};
|
|
763
1486
|
}
|
|
764
1487
|
function evidenceStatusLabel(status) {
|
|
765
1488
|
if (status === "source-matched")
|