@work-graph/cli 0.2.0
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/README.md +31 -0
- package/bin/work-graph.mjs +238 -0
- package/package.json +38 -0
- package/vendor/packages/design-tokens/generated/gripe-dark-default.css +67 -0
- package/vendor/packages/design-tokens/generated/marketplace-default.css +67 -0
- package/vendor/packages/design-tokens/generated/workgraph-dark.css +67 -0
- package/vendor/packages/workgraph-mcp/README.md +28 -0
- package/vendor/packages/workgraph-mcp/bin/workgraph-mcp.mjs +21 -0
- package/vendor/packages/workgraph-mcp/package.json +37 -0
- package/vendor/packages/workgraph-mcp/src/handlers.mjs +761 -0
- package/vendor/packages/workgraph-mcp/src/index.mjs +638 -0
- package/vendor/packages/workgraph-mcp/src/prompts.mjs +162 -0
- package/vendor/public/assets/workgraph-logo.svg +11 -0
- package/vendor/public/fonts/GraphikLCG/GraphikLCG-Medium.woff2 +0 -0
- package/vendor/public/fonts/GraphikLCG/GraphikLCG-Regular.woff2 +0 -0
- package/vendor/public/fonts/GraphikLCG/GraphikLCG-Semibold.woff2 +0 -0
- package/vendor/public/fonts/GraphikLCG/stylesheet.css +25 -0
- package/vendor/public/graph-canvas-lit-flow.css +154 -0
- package/vendor/public/graph-canvas-lit-flow.css.map +7 -0
- package/vendor/public/graph-canvas-lit-flow.js +8530 -0
- package/vendor/public/graph-canvas-lit-flow.js.map +7 -0
- package/vendor/src/agentBehaviorRulesAudit.mjs +168 -0
- package/vendor/src/agentBehaviorRulesBundle.mjs +144 -0
- package/vendor/src/agentRunApi.mjs +136 -0
- package/vendor/src/agentToolLoopGuard.mjs +88 -0
- package/vendor/src/agentWorkerClaudeProvider.mjs +288 -0
- package/vendor/src/agentWorkerCursorSdkProvider.mjs +156 -0
- package/vendor/src/agentWorkerLiveLoop.mjs +455 -0
- package/vendor/src/agentWorkerLocalCliProvider.mjs +217 -0
- package/vendor/src/agentWorkerLocalRunner.mjs +246 -0
- package/vendor/src/agentWorkerOpenAiProvider.mjs +459 -0
- package/vendor/src/analyticsPanelProjection.mjs +212 -0
- package/vendor/src/analyticsRecordStore.mjs +165 -0
- package/vendor/src/analyticsRecordWorkItems.mjs +104 -0
- package/vendor/src/architectureL1Canon.mjs +419 -0
- package/vendor/src/architectureLayout.mjs +229 -0
- package/vendor/src/architectureSnapshot.mjs +490 -0
- package/vendor/src/architectureViewsProjection.mjs +116 -0
- package/vendor/src/atomInspector.mjs +253 -0
- package/vendor/src/atomInspectorApi.mjs +130 -0
- package/vendor/src/auditGapMatrixRefresh.mjs +121 -0
- package/vendor/src/backlogSchemaLint.mjs +176 -0
- package/vendor/src/blockedOnebaseGoPreflightEval.mjs +100 -0
- package/vendor/src/bracketIrTraceSignal.mjs +93 -0
- package/vendor/src/bvcAtomParser.mjs +210 -0
- package/vendor/src/bvcDialectRegistry.mjs +86 -0
- package/vendor/src/bvcFileFormat.mjs +218 -0
- package/vendor/src/bvcFormatCli.mjs +55 -0
- package/vendor/src/bvcLintCli.mjs +48 -0
- package/vendor/src/bvcNewWritePolicy.mjs +70 -0
- package/vendor/src/charterPreflightPromoteGate.mjs +194 -0
- package/vendor/src/claimNoEligibleEval.mjs +205 -0
- package/vendor/src/closingAnalysisSuggest.mjs +59 -0
- package/vendor/src/codeGapAnalyzer.mjs +308 -0
- package/vendor/src/codeGapBacklogFeeder.mjs +82 -0
- package/vendor/src/codeGapDraftIntakeApi.mjs +307 -0
- package/vendor/src/codeGapOperatorProjection.mjs +60 -0
- package/vendor/src/codeSyntaxHighlight.mjs +123 -0
- package/vendor/src/codegenEvidence.mjs +187 -0
- package/vendor/src/compilerRoundTripCli.mjs +164 -0
- package/vendor/src/dagreGraphLayout.mjs +78 -0
- package/vendor/src/draftIntakePromotionRules.mjs +205 -0
- package/vendor/src/epicWorkScope.mjs +85 -0
- package/vendor/src/evalLiveLlmEnv.mjs +63 -0
- package/vendor/src/evidenceReadModel.mjs +167 -0
- package/vendor/src/gfsOverlayProjectPassport.mjs +235 -0
- package/vendor/src/globalStepPathToBvcReferences.mjs +196 -0
- package/vendor/src/goldenPath.mjs +69 -0
- package/vendor/src/graphCanvasLayout.mjs +464 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCanvasMinimap.ts +261 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCanvasSvgEdges.ts +259 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCanvasTheme.css +152 -0
- package/vendor/src/graphCanvasLitFlow/client/graphCardNode.ts +328 -0
- package/vendor/src/graphCanvasLitFlow/client/mountGraphCanvasLitFlow.ts +322 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasEdgeLabels.mjs +58 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasEdgeRouter.mjs +142 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasLayoutProfile.mjs +32 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasNodeMetrics.mjs +45 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasProjection.mjs +115 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasProjectionToFlow.mjs +133 -0
- package/vendor/src/graphCanvasLitFlow/graphCanvasTraversal.mjs +77 -0
- package/vendor/src/graphCanvasLitFlow/layoutIntentRoadmapWorkStack.mjs +73 -0
- package/vendor/src/graphCanvasLitFlow/resolveGraphCanvasOverlaps.mjs +77 -0
- package/vendor/src/graphRagContextSlice.mjs +461 -0
- package/vendor/src/gvmVerifyWorkerGate.mjs +95 -0
- package/vendor/src/homeSnapshotApi.mjs +131 -0
- package/vendor/src/homeSnapshotProjection.mjs +275 -0
- package/vendor/src/inboxEventStream.mjs +140 -0
- package/vendor/src/intentComposerApi.mjs +245 -0
- package/vendor/src/intentGraphGbcSliceBoundary.mjs +258 -0
- package/vendor/src/intentGraphProjection.mjs +208 -0
- package/vendor/src/intentHierarchy.mjs +241 -0
- package/vendor/src/intentNodeLint.mjs +107 -0
- package/vendor/src/intentNodeRuntime.mjs +185 -0
- package/vendor/src/intentRoadmapCanvas.mjs +393 -0
- package/vendor/src/intentRoadmapEpicProjection.mjs +122 -0
- package/vendor/src/intentRoadmapMermaid.mjs +165 -0
- package/vendor/src/intentRoadmapProjection.mjs +85 -0
- package/vendor/src/intentTreeLint.mjs +114 -0
- package/vendor/src/intentTreeMigration.mjs +150 -0
- package/vendor/src/intentTreeWorkItems.mjs +227 -0
- package/vendor/src/kanbanBoardProjection.mjs +58 -0
- package/vendor/src/languageAdapterRegistry.mjs +180 -0
- package/vendor/src/languageAdapters/goAdapter.mjs +62 -0
- package/vendor/src/languageAdapters/jsTsAdapter.mjs +60 -0
- package/vendor/src/languageAdapters/jsonYamlAdapter.mjs +103 -0
- package/vendor/src/languageAdapters/onebaseOsAdapter.mjs +55 -0
- package/vendor/src/languageAdapters/plaintextAdapter.mjs +36 -0
- package/vendor/src/languageAdapters/shared.mjs +68 -0
- package/vendor/src/languageAdapters/stepAdapter.mjs +81 -0
- package/vendor/src/lintPlanWorkAlignment.mjs +136 -0
- package/vendor/src/loopHintRepeatToolEval.mjs +153 -0
- package/vendor/src/lowcodeScaffoldCli.mjs +386 -0
- package/vendor/src/markdownDocumentRender.mjs +208 -0
- package/vendor/src/memoryPanelProjection.mjs +116 -0
- package/vendor/src/memoryRecordWriter.mjs +243 -0
- package/vendor/src/memoryWorkerSlice.mjs +238 -0
- package/vendor/src/migrateStepToBvc.mjs +133 -0
- package/vendor/src/missionControlServerHandlers.mjs +195 -0
- package/vendor/src/missionControlUiClient.mjs +278 -0
- package/vendor/src/onebaseCliCapabilityProbe.mjs +107 -0
- package/vendor/src/onebaseCliRunner.mjs +145 -0
- package/vendor/src/onebaseGrossProfitStaticVerify.mjs +98 -0
- package/vendor/src/onebaseParityEvidenceSync.mjs +88 -0
- package/vendor/src/onebasePvrgGraphNodes.mjs +257 -0
- package/vendor/src/onebaseRestEvidenceAdapter.mjs +216 -0
- package/vendor/src/onebaseVectorDslCodegenReadiness.mjs +137 -0
- package/vendor/src/onebaseWorkItemTemplate.mjs +154 -0
- package/vendor/src/onebaseWorkerTools.mjs +586 -0
- package/vendor/src/operatorShellProjection.mjs +102 -0
- package/vendor/src/pipelineProseRender.mjs +180 -0
- package/vendor/src/pipelineStageLint.mjs +118 -0
- package/vendor/src/promptRulesEditorApi.mjs +174 -0
- package/vendor/src/promptRulesProjection.mjs +134 -0
- package/vendor/src/pvrg/bladeAdapter.mjs +40 -0
- package/vendor/src/pvrgTaskScope.mjs +152 -0
- package/vendor/src/releaseGateMatrix.mjs +188 -0
- package/vendor/src/schematicView.mjs +305 -0
- package/vendor/src/seedAnalyticsRecord.mjs +217 -0
- package/vendor/src/semanticSearchBm25.mjs +103 -0
- package/vendor/src/semanticSearchExcerpts.mjs +68 -0
- package/vendor/src/semanticSearchTfidfVector.mjs +86 -0
- package/vendor/src/semanticSearchWorkflow.mjs +366 -0
- package/vendor/src/stepAtomFormatter.mjs +413 -0
- package/vendor/src/stepGraphSlice.mjs +318 -0
- package/vendor/src/ui/atoms/badge.mjs +40 -0
- package/vendor/src/ui/atoms/badgeClient.mjs +32 -0
- package/vendor/src/ui/atoms/button.mjs +114 -0
- package/vendor/src/ui/atoms/buttonClient.mjs +49 -0
- package/vendor/src/ui/atoms/icon.mjs +23 -0
- package/vendor/src/ui/atoms/input.mjs +38 -0
- package/vendor/src/ui/atoms/modal.mjs +44 -0
- package/vendor/src/ui/atoms/select.mjs +98 -0
- package/vendor/src/ui/backlogShellButtons.mjs +238 -0
- package/vendor/src/ui/htmlEscape.mjs +11 -0
- package/vendor/src/ui/molecules/rating.mjs +48 -0
- package/vendor/src/ui/molecules/tabs.mjs +70 -0
- package/vendor/src/ui/organisms/modal.mjs +1 -0
- package/vendor/src/ui/pages/uiKitPage.mjs +147 -0
- package/vendor/src/ui/workItemStatusTone.mjs +36 -0
- package/vendor/src/unifiedLinkageProjection.mjs +264 -0
- package/vendor/src/verificationLoop.mjs +206 -0
- package/vendor/src/workGraphBacklogPersist.mjs +234 -0
- package/vendor/src/workGraphBacklogUiServer.mjs +9192 -0
- package/vendor/src/workGraphBoundedTargetFileRead.mjs +178 -0
- package/vendor/src/workGraphCycleSlice.mjs +184 -0
- package/vendor/src/workGraphDaemonTick.mjs +307 -0
- package/vendor/src/workGraphDaemonWatch.mjs +157 -0
- package/vendor/src/workGraphEngineRoot.mjs +136 -0
- package/vendor/src/workGraphInstallLayout.mjs +65 -0
- package/vendor/src/workGraphLlmUsefulnessEval.mjs +611 -0
- package/vendor/src/workGraphPhasePromoteReadyQueue.mjs +159 -0
- package/vendor/src/workGraphProjectHost.mjs +149 -0
- package/vendor/src/workGraphProjectInit.mjs +392 -0
- package/vendor/src/workGraphPromoteReadyApi.mjs +115 -0
- package/vendor/src/workGraphRecoveryPolicy.mjs +124 -0
- package/vendor/src/workGraphRunnerQueueProjection.mjs +187 -0
- package/vendor/src/workGraphRuntime.mjs +1008 -0
- package/vendor/src/workGraphToolSurfaceAudit.mjs +372 -0
- package/vendor/src/workGraphToolTransportRuntime.mjs +195 -0
- package/vendor/src/workGraphWorkerProvider.mjs +600 -0
- package/vendor/src/workItemBvcQuality.mjs +262 -0
- package/vendor/src/workItemCreateAnalysis.mjs +157 -0
- package/vendor/src/workItemDecisionPipeline.mjs +278 -0
- package/vendor/src/workItemEpicCascade.mjs +176 -0
- package/vendor/src/workItemExecutionGate.mjs +78 -0
- package/vendor/src/workItemHierarchy.mjs +226 -0
- package/vendor/src/workItemProseLint.mjs +133 -0
- package/vendor/src/workItemTextRusify.mjs +794 -0
- package/vendor/src/workItemTraceEnvelope.mjs +158 -0
- package/vendor/src/workItemUiReferences.mjs +272 -0
- package/vendor/src/workflowEpicGrouping.mjs +67 -0
- package/vendor/src/workflowTreeProjection.mjs +53 -0
- package/vendor/src/workspaceRegistry.mjs +150 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
import { parseBvcFileContent } from './bvcFileFormat.mjs';
|
|
7
|
+
|
|
8
|
+
export const ARCHITECTURE_L1_CANON_DEFAULT_PATH = 'architecture/main.bvc';
|
|
9
|
+
export const ARCHITECTURE_L1_CANON_ID = 'architecture-l1-blocks-v1';
|
|
10
|
+
export const ARCHITECTURE_L1_BLOCK_COUNT = 7;
|
|
11
|
+
|
|
12
|
+
const VALID_CONTAINER_KINDS = new Set([
|
|
13
|
+
'runtime',
|
|
14
|
+
'protocol',
|
|
15
|
+
'ui',
|
|
16
|
+
'schema',
|
|
17
|
+
'domain',
|
|
18
|
+
'research',
|
|
19
|
+
'storage',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const VALID_EDGE_TYPES = new Set([
|
|
23
|
+
'feeds',
|
|
24
|
+
'uses',
|
|
25
|
+
'validates',
|
|
26
|
+
'implements',
|
|
27
|
+
'defines',
|
|
28
|
+
'maps_to',
|
|
29
|
+
'relates_file',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const MIN_CONTAINER_ANALYSIS_LENGTH = 200;
|
|
33
|
+
const MIN_CONTAINER_DECISION_LENGTH = 80;
|
|
34
|
+
|
|
35
|
+
const EDGE_LINE_PATTERN = /^([^\s]+)\s*->\s*([^\s]+)\s*:\s*([^\s]+)\s*$/u;
|
|
36
|
+
|
|
37
|
+
const compareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string[] | string | undefined | null} value
|
|
41
|
+
*/
|
|
42
|
+
function joinTextSections(value) {
|
|
43
|
+
if (Array.isArray(value)) {
|
|
44
|
+
return value.join('\n').trim();
|
|
45
|
+
}
|
|
46
|
+
return String(value ?? '').trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {Record<string, string>} labels
|
|
51
|
+
*/
|
|
52
|
+
function parseIntentRootsFromLabels(labels) {
|
|
53
|
+
const raw = labels['architecture.intent_roots'] ?? '';
|
|
54
|
+
return raw
|
|
55
|
+
.split(',')
|
|
56
|
+
.map((entry) => entry.trim())
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.sort(compareText);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} value
|
|
63
|
+
*/
|
|
64
|
+
function unescapeContainerLabelMultiline(value) {
|
|
65
|
+
return String(value ?? '')
|
|
66
|
+
.replace(/\\n/g, '\n')
|
|
67
|
+
.trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {Record<string, string>} labels
|
|
72
|
+
*/
|
|
73
|
+
function parseContainersFromLabels(labels) {
|
|
74
|
+
/** @type {Map<string, { id: string, title: string, kind: string, paths: string[], basis: string, vector: string, goal: string, analysis: string, decision: string, labels: Record<string, string> }>} */
|
|
75
|
+
const byId = new Map();
|
|
76
|
+
|
|
77
|
+
for (const [key, value] of Object.entries(labels ?? {})) {
|
|
78
|
+
const fieldMatch = /^architecture\.container\.([^.]+)\.(title|kind|paths|basis|vector|goal|summary|analysis|decision)$/.exec(key);
|
|
79
|
+
const verdictMatch = /^architecture\.container\.([^.]+)\.decision\.verdict$/.exec(key);
|
|
80
|
+
const containerId = fieldMatch?.[1] ?? verdictMatch?.[1];
|
|
81
|
+
if (!containerId) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!byId.has(containerId)) {
|
|
86
|
+
byId.set(containerId, {
|
|
87
|
+
id: containerId,
|
|
88
|
+
title: '',
|
|
89
|
+
kind: 'runtime',
|
|
90
|
+
paths: [],
|
|
91
|
+
basis: '',
|
|
92
|
+
vector: '',
|
|
93
|
+
goal: '',
|
|
94
|
+
analysis: '',
|
|
95
|
+
decision: '',
|
|
96
|
+
labels: {},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const entry = byId.get(containerId);
|
|
101
|
+
if (verdictMatch) {
|
|
102
|
+
entry.labels['architecture.decision.verdict'] = value.trim();
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const field = fieldMatch[2];
|
|
107
|
+
if (field === 'paths') {
|
|
108
|
+
entry.paths = value
|
|
109
|
+
.split(',')
|
|
110
|
+
.map((path) => path.trim())
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.sort(compareText);
|
|
113
|
+
} else if (field === 'title') {
|
|
114
|
+
entry.title = value.trim();
|
|
115
|
+
} else if (field === 'kind') {
|
|
116
|
+
entry.kind = value.trim();
|
|
117
|
+
} else if (field === 'basis' || field === 'vector' || field === 'goal' || field === 'analysis' || field === 'decision') {
|
|
118
|
+
entry[field] = field === 'analysis' || field === 'decision'
|
|
119
|
+
? unescapeContainerLabelMultiline(value)
|
|
120
|
+
: value.trim();
|
|
121
|
+
} else if (field === 'summary' && !entry.basis) {
|
|
122
|
+
entry.basis = value.trim();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return [...byId.values()].sort((left, right) => compareText(left.id, right.id));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {string} vector
|
|
131
|
+
*/
|
|
132
|
+
function parseEdgesFromVector(vector) {
|
|
133
|
+
/** @type {Array<{ from: string, to: string, type: string }>} */
|
|
134
|
+
const edges = [];
|
|
135
|
+
|
|
136
|
+
for (const line of String(vector ?? '').split('\n')) {
|
|
137
|
+
const trimmed = line.trim();
|
|
138
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const match = EDGE_LINE_PATTERN.exec(trimmed);
|
|
143
|
+
if (!match) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
edges.push({
|
|
148
|
+
from: match[1],
|
|
149
|
+
to: match[2],
|
|
150
|
+
type: match[3],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return edges.sort((left, right) =>
|
|
155
|
+
compareText(left.from, right.from) ||
|
|
156
|
+
compareText(left.to, right.to) ||
|
|
157
|
+
compareText(left.type, right.type),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {import('./stepAtomFormatter.mjs').StepAtomDraft & { title?: string }} atom
|
|
163
|
+
*/
|
|
164
|
+
function mapBlockAtom(atom) {
|
|
165
|
+
const labels = atom.labels ?? {};
|
|
166
|
+
const blockId = labels['architecture.block_id']?.trim();
|
|
167
|
+
if (!blockId) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
id: blockId,
|
|
173
|
+
title: labels['architecture.title']?.trim() || atom.title || blockId,
|
|
174
|
+
summary: labels['architecture.summary']?.trim() || '',
|
|
175
|
+
group: labels['architecture.group']?.trim() || '',
|
|
176
|
+
basis: joinTextSections(atom.basis),
|
|
177
|
+
vector: joinTextSections(atom.vector),
|
|
178
|
+
goal: joinTextSections(atom.goal),
|
|
179
|
+
analysis: joinTextSections(atom.analysis),
|
|
180
|
+
decision: joinTextSections(atom.decision),
|
|
181
|
+
labels: pickArchitectureBlockLabels(labels),
|
|
182
|
+
layer: labels['architecture.layer']?.trim() || 'L1',
|
|
183
|
+
intentRoots: parseIntentRootsFromLabels(labels),
|
|
184
|
+
containers: parseContainersFromLabels(labels),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @param {Record<string, string>} labels
|
|
190
|
+
*/
|
|
191
|
+
function pickArchitectureBlockLabels(labels) {
|
|
192
|
+
/** @type {Record<string, string>} */
|
|
193
|
+
const picked = {};
|
|
194
|
+
const verdict = labels['architecture.decision.verdict']?.trim();
|
|
195
|
+
if (verdict) {
|
|
196
|
+
picked['architecture.decision.verdict'] = verdict;
|
|
197
|
+
}
|
|
198
|
+
return picked;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @param {string} content
|
|
203
|
+
* @param {{ filePath?: string, repoRoot?: string }} [options]
|
|
204
|
+
*/
|
|
205
|
+
export function parseArchitectureL1CanonContent(content, options = {}) {
|
|
206
|
+
const parsed = parseBvcFileContent(content, { filePath: options.filePath });
|
|
207
|
+
/** @type {{ id: string, version: number, charterRef: string, protocolRef: string, designOutput: string, basis: string, vector: string, goal: string } | null} */
|
|
208
|
+
let passport = null;
|
|
209
|
+
/** @type {Array<{ id: string, title: string, summary: string, basis: string, vector: string, goal: string, layer: string, intentRoots: string[], containers: Array<{ id: string, title: string, kind: string, paths: string[] }> }>} */
|
|
210
|
+
const blocks = [];
|
|
211
|
+
/** @type {Array<{ from: string, to: string, type: string }>} */
|
|
212
|
+
let edges = [];
|
|
213
|
+
|
|
214
|
+
for (const entry of parsed.atoms) {
|
|
215
|
+
const atom = entry.draft;
|
|
216
|
+
const profile = atom.profile || atom.labels?.['atom.profile'] || '';
|
|
217
|
+
|
|
218
|
+
if (profile === 'architecture_canon') {
|
|
219
|
+
passport = {
|
|
220
|
+
id: atom.labels?.['architecture.canon.id']?.trim() || ARCHITECTURE_L1_CANON_ID,
|
|
221
|
+
version: Number(atom.labels?.['architecture.canon.version'] ?? 1),
|
|
222
|
+
charterRef: atom.labels?.['architecture.charter.ref']?.trim() || '',
|
|
223
|
+
protocolRef: atom.labels?.['architecture.protocol.ref']?.trim() || '',
|
|
224
|
+
designOutput: atom.labels?.['architecture.design.output']?.trim() || 'architecture.snapshot.v1',
|
|
225
|
+
basis: joinTextSections(atom.basis),
|
|
226
|
+
vector: joinTextSections(atom.vector),
|
|
227
|
+
goal: joinTextSections(atom.goal),
|
|
228
|
+
};
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (profile === 'architecture_l1_block') {
|
|
233
|
+
const block = mapBlockAtom(atom);
|
|
234
|
+
if (block) {
|
|
235
|
+
blocks.push(block);
|
|
236
|
+
}
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (profile === 'architecture_canon_section' && atom.labels?.['architecture.section'] === 'l1_edges') {
|
|
241
|
+
edges = parseEdgesFromVector(joinTextSections(atom.vector));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
blocks.sort((left, right) => compareText(left.id, right.id));
|
|
246
|
+
|
|
247
|
+
const sourcePath = options.filePath ?? ARCHITECTURE_L1_CANON_DEFAULT_PATH;
|
|
248
|
+
const digest = createHash('sha256')
|
|
249
|
+
.update(JSON.stringify({ passport, blocks, edges }), 'utf8')
|
|
250
|
+
.digest('hex')
|
|
251
|
+
.slice(0, 8);
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
schema: 'architecture.l1-canon.v1',
|
|
255
|
+
sourcePath,
|
|
256
|
+
passport,
|
|
257
|
+
blocks,
|
|
258
|
+
edges,
|
|
259
|
+
digest,
|
|
260
|
+
parse: parsed,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @param {string} repoRoot
|
|
266
|
+
* @param {{ canonPath?: string }} [options]
|
|
267
|
+
*/
|
|
268
|
+
export function loadArchitectureL1Canon(repoRoot, options = {}) {
|
|
269
|
+
const canonPath = options.canonPath ?? ARCHITECTURE_L1_CANON_DEFAULT_PATH;
|
|
270
|
+
const absolutePath = join(repoRoot, canonPath);
|
|
271
|
+
let content;
|
|
272
|
+
let sourcePath = canonPath.replace(/\\/g, '/');
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
content = readFileSync(absolutePath, 'utf8');
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error && error.code === 'ENOENT' && repoRoot !== ARCHITECTURE_L1_CANON_REPO_ROOT) {
|
|
278
|
+
return loadArchitectureL1Canon(ARCHITECTURE_L1_CANON_REPO_ROOT, options);
|
|
279
|
+
}
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const canon = parseArchitectureL1CanonContent(content, { filePath: sourcePath, repoRoot });
|
|
284
|
+
|
|
285
|
+
validateArchitectureL1Canon(canon);
|
|
286
|
+
return canon;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* @param {ReturnType<typeof parseArchitectureL1CanonContent>} canon
|
|
291
|
+
*/
|
|
292
|
+
export function validateArchitectureL1Canon(canon) {
|
|
293
|
+
/** @type {string[]} */
|
|
294
|
+
const errors = [];
|
|
295
|
+
|
|
296
|
+
if (!canon.passport) {
|
|
297
|
+
errors.push('missing passport atom (atom.profile: architecture_canon)');
|
|
298
|
+
} else {
|
|
299
|
+
if (!canon.passport.id) {
|
|
300
|
+
errors.push('passport missing architecture.canon.id');
|
|
301
|
+
}
|
|
302
|
+
if (!Number.isFinite(canon.passport.version)) {
|
|
303
|
+
errors.push('passport architecture.canon.version must be a number');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (canon.blocks.length !== ARCHITECTURE_L1_BLOCK_COUNT) {
|
|
308
|
+
errors.push(`expected ${ARCHITECTURE_L1_BLOCK_COUNT} L1 blocks, got ${canon.blocks.length}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const blockIds = new Set(canon.blocks.map((block) => block.id));
|
|
312
|
+
if (blockIds.size !== canon.blocks.length) {
|
|
313
|
+
errors.push('duplicate architecture.block_id values');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const block of canon.blocks) {
|
|
317
|
+
if (!block.title) {
|
|
318
|
+
errors.push(`block ${block.id}: missing title`);
|
|
319
|
+
}
|
|
320
|
+
for (const container of block.containers) {
|
|
321
|
+
if (!container.title) {
|
|
322
|
+
errors.push(`block ${block.id}: container ${container.id} missing title`);
|
|
323
|
+
}
|
|
324
|
+
if (!VALID_CONTAINER_KINDS.has(container.kind)) {
|
|
325
|
+
errors.push(`block ${block.id}: container ${container.id} invalid kind ${container.kind}`);
|
|
326
|
+
}
|
|
327
|
+
if (!container.paths.length) {
|
|
328
|
+
errors.push(`block ${block.id}: container ${container.id} missing paths`);
|
|
329
|
+
}
|
|
330
|
+
validateContainerPipeline(container, block.id, errors);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!canon.edges.length) {
|
|
335
|
+
errors.push('missing L1 edges section');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (const edge of canon.edges) {
|
|
339
|
+
if (!blockIds.has(edge.from)) {
|
|
340
|
+
errors.push(`edge from unknown block: ${edge.from}`);
|
|
341
|
+
}
|
|
342
|
+
if (!blockIds.has(edge.to)) {
|
|
343
|
+
errors.push(`edge to unknown block: ${edge.to}`);
|
|
344
|
+
}
|
|
345
|
+
if (!VALID_EDGE_TYPES.has(edge.type)) {
|
|
346
|
+
errors.push(`edge ${edge.from}->${edge.to}: invalid type ${edge.type}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (errors.length) {
|
|
351
|
+
throw new Error(`Invalid architecture L1 canon (${canon.sourcePath}):\n- ${errors.join('\n- ')}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return canon;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* @param {{ id: string, analysis?: string, decision?: string, labels?: Record<string, string> }} container
|
|
359
|
+
* @param {string} blockId
|
|
360
|
+
* @param {string[]} errors
|
|
361
|
+
*/
|
|
362
|
+
function validateContainerPipeline(container, blockId, errors) {
|
|
363
|
+
const analysis = String(container.analysis ?? '').trim();
|
|
364
|
+
const decision = String(container.decision ?? '').trim();
|
|
365
|
+
|
|
366
|
+
if (analysis.length < MIN_CONTAINER_ANALYSIS_LENGTH) {
|
|
367
|
+
errors.push(`block ${blockId}: container ${container.id} analysis too short (min ${MIN_CONTAINER_ANALYSIS_LENGTH})`);
|
|
368
|
+
}
|
|
369
|
+
if (!analysis.includes('Целесообразность:') || !analysis.includes('Контекст и границы:')) {
|
|
370
|
+
errors.push(`block ${blockId}: container ${container.id} analysis must include «Целесообразность:» and «Контекст и границы:»`);
|
|
371
|
+
}
|
|
372
|
+
if (decision.length < MIN_CONTAINER_DECISION_LENGTH) {
|
|
373
|
+
errors.push(`block ${blockId}: container ${container.id} decision too short (min ${MIN_CONTAINER_DECISION_LENGTH})`);
|
|
374
|
+
}
|
|
375
|
+
if (!container.labels?.['architecture.decision.verdict']) {
|
|
376
|
+
errors.push(`block ${blockId}: container ${container.id} missing architecture.container.*.decision.verdict label`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Strip BVC triplet fields for layout helpers expecting legacy block shape.
|
|
382
|
+
* @param {{ id: string, title: string, summary: string, basis: string, vector: string, goal: string, layer: string, intentRoots: string[], containers: Array<{ id: string, title: string, kind: string, paths: string[] }> }} block
|
|
383
|
+
*/
|
|
384
|
+
export function toArchitectureL1BlockProjection(block) {
|
|
385
|
+
return {
|
|
386
|
+
id: block.id,
|
|
387
|
+
title: block.title,
|
|
388
|
+
summary: block.summary,
|
|
389
|
+
...(block.group ? { group: block.group } : {}),
|
|
390
|
+
intentRoots: [...block.intentRoots],
|
|
391
|
+
containers: block.containers.map((container) => ({
|
|
392
|
+
id: container.id,
|
|
393
|
+
title: container.title,
|
|
394
|
+
kind: container.kind,
|
|
395
|
+
paths: [...container.paths],
|
|
396
|
+
...(container.basis ? { basis: container.basis } : {}),
|
|
397
|
+
...(container.vector ? { vector: container.vector } : {}),
|
|
398
|
+
...(container.goal ? { goal: container.goal } : {}),
|
|
399
|
+
...(container.analysis ? { analysis: container.analysis } : {}),
|
|
400
|
+
...(container.decision ? { decision: container.decision } : {}),
|
|
401
|
+
...(container.labels && Object.keys(container.labels).length > 0 ? { labels: { ...container.labels } } : {}),
|
|
402
|
+
})),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const moduleDir = fileURLToPath(new URL('.', import.meta.url));
|
|
407
|
+
export const ARCHITECTURE_L1_CANON_REPO_ROOT = join(moduleDir, '..');
|
|
408
|
+
|
|
409
|
+
let cachedDefaultCanon = null;
|
|
410
|
+
|
|
411
|
+
export function getDefaultArchitectureL1Canon() {
|
|
412
|
+
if (!cachedDefaultCanon) {
|
|
413
|
+
cachedDefaultCanon = loadArchitectureL1Canon(ARCHITECTURE_L1_CANON_REPO_ROOT);
|
|
414
|
+
}
|
|
415
|
+
return cachedDefaultCanon;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export const ARCHITECTURE_L1_BLOCKS = getDefaultArchitectureL1Canon().blocks.map(toArchitectureL1BlockProjection);
|
|
419
|
+
export const ARCHITECTURE_L1_EDGES = getDefaultArchitectureL1Canon().edges.map((edge) => ({ ...edge }));
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ARCHITECTURE_LAYOUT_PROFILE,
|
|
3
|
+
ARCHITECTURE_PIPELINE_LAYOUT_PROFILE,
|
|
4
|
+
GRAPH_CANVAS_VIEW_FULL,
|
|
5
|
+
GRAPH_CANVAS_VIEW_PIPELINE,
|
|
6
|
+
filterGraphForViewMode,
|
|
7
|
+
placeGraphCanvasNodes,
|
|
8
|
+
resolveLayoutProfileForViewMode,
|
|
9
|
+
} from './graphCanvasLayout.mjs';
|
|
10
|
+
|
|
11
|
+
const NODE_WIDTH = 200;
|
|
12
|
+
const NODE_MIN_HEIGHT = 88;
|
|
13
|
+
const COL_GAP = 72;
|
|
14
|
+
const ROW_GAP = 72;
|
|
15
|
+
const OFFSET_X = 24;
|
|
16
|
+
const OFFSET_Y = 24;
|
|
17
|
+
const NODE_PADDING_X = 20;
|
|
18
|
+
const HEADER_BLOCK_HEIGHT = 52;
|
|
19
|
+
const META_BLOCK_HEIGHT = 28;
|
|
20
|
+
const SUMMARY_LINE_HEIGHT = 18;
|
|
21
|
+
const SUMMARY_CHARS_PER_LINE = 32;
|
|
22
|
+
const SUMMARY_MAX_LINES = 2;
|
|
23
|
+
|
|
24
|
+
const GRID_CONFIG = {
|
|
25
|
+
nodeWidth: NODE_WIDTH,
|
|
26
|
+
nodeMinHeight: NODE_MIN_HEIGHT,
|
|
27
|
+
colGap: COL_GAP,
|
|
28
|
+
rowGap: ROW_GAP,
|
|
29
|
+
offsetX: OFFSET_X,
|
|
30
|
+
offsetY: OFFSET_Y,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function estimateSummaryLines(summary, width) {
|
|
34
|
+
const charsPerLine = Math.max(
|
|
35
|
+
16,
|
|
36
|
+
Math.floor((width - NODE_PADDING_X) / (NODE_WIDTH / SUMMARY_CHARS_PER_LINE)),
|
|
37
|
+
);
|
|
38
|
+
const rawLines = Math.max(1, Math.ceil(String(summary ?? '').length / charsPerLine));
|
|
39
|
+
return Math.min(SUMMARY_MAX_LINES, rawLines);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function estimateArchitectureNodeHeight(block, width) {
|
|
43
|
+
const lines = estimateSummaryLines(block.summary, width);
|
|
44
|
+
return Math.max(
|
|
45
|
+
NODE_MIN_HEIGHT,
|
|
46
|
+
HEADER_BLOCK_HEIGHT + lines * SUMMARY_LINE_HEIGHT + META_BLOCK_HEIGHT,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function assignArchitectureEdgeLanes(edges) {
|
|
51
|
+
const outgoing = new Map();
|
|
52
|
+
const incoming = new Map();
|
|
53
|
+
|
|
54
|
+
edges.forEach((edge) => {
|
|
55
|
+
if (!outgoing.has(edge.from)) outgoing.set(edge.from, []);
|
|
56
|
+
if (!incoming.has(edge.to)) incoming.set(edge.to, []);
|
|
57
|
+
outgoing.get(edge.from).push(edge);
|
|
58
|
+
incoming.get(edge.to).push(edge);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return edges.map((edge) => {
|
|
62
|
+
const outList = outgoing.get(edge.from) ?? [];
|
|
63
|
+
const inList = incoming.get(edge.to) ?? [];
|
|
64
|
+
return {
|
|
65
|
+
...edge,
|
|
66
|
+
outLane: outList.indexOf(edge),
|
|
67
|
+
outLaneCount: outList.length,
|
|
68
|
+
inLane: inList.indexOf(edge),
|
|
69
|
+
inLaneCount: inList.length,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function architectureEdgeGeometry(edge) {
|
|
75
|
+
const from = edge.fromNode;
|
|
76
|
+
const to = edge.toNode;
|
|
77
|
+
const sameRow = from.row === to.row;
|
|
78
|
+
const goesDown = to.row > from.row;
|
|
79
|
+
const goesUp = to.row < from.row;
|
|
80
|
+
|
|
81
|
+
let geometry;
|
|
82
|
+
|
|
83
|
+
if (sameRow && to.col > from.col) {
|
|
84
|
+
const laneOffset = (edge.outLane - (edge.outLaneCount - 1) / 2) * 14;
|
|
85
|
+
geometry = {
|
|
86
|
+
axis: 'horizontal',
|
|
87
|
+
upstream: false,
|
|
88
|
+
startX: from.x + from.width,
|
|
89
|
+
startY: from.y + from.height / 2 + laneOffset,
|
|
90
|
+
endX: to.x,
|
|
91
|
+
endY: to.y + to.height / 2 + laneOffset,
|
|
92
|
+
};
|
|
93
|
+
} else if (goesDown) {
|
|
94
|
+
const spread = from.width / (edge.outLaneCount + 1);
|
|
95
|
+
const startX = from.x + spread * (edge.outLane + 1);
|
|
96
|
+
const endSpread = to.width / (edge.inLaneCount + 1);
|
|
97
|
+
const endX = to.x + endSpread * (edge.inLane + 1);
|
|
98
|
+
geometry = {
|
|
99
|
+
axis: 'vertical-down',
|
|
100
|
+
upstream: false,
|
|
101
|
+
startX,
|
|
102
|
+
startY: from.y + from.height,
|
|
103
|
+
endX,
|
|
104
|
+
endY: to.y,
|
|
105
|
+
};
|
|
106
|
+
} else if (goesUp) {
|
|
107
|
+
const spread = from.width / (edge.outLaneCount + 1);
|
|
108
|
+
const startX = from.x + spread * (edge.outLane + 1);
|
|
109
|
+
const endSpread = to.width / (edge.inLaneCount + 1);
|
|
110
|
+
const endX = to.x + endSpread * (edge.inLane + 1);
|
|
111
|
+
geometry = {
|
|
112
|
+
axis: 'vertical-up',
|
|
113
|
+
upstream: true,
|
|
114
|
+
startX,
|
|
115
|
+
startY: from.y,
|
|
116
|
+
endX,
|
|
117
|
+
endY: to.y + to.height,
|
|
118
|
+
};
|
|
119
|
+
} else {
|
|
120
|
+
const laneOffset = (edge.outLane - (edge.outLaneCount - 1) / 2) * 14;
|
|
121
|
+
geometry = {
|
|
122
|
+
axis: 'horizontal-reverse',
|
|
123
|
+
upstream: true,
|
|
124
|
+
startX: from.x,
|
|
125
|
+
startY: from.y + from.height / 2 + laneOffset,
|
|
126
|
+
endX: to.x + to.width,
|
|
127
|
+
endY: to.y + to.height / 2 + laneOffset,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const labelX = (geometry.startX + geometry.endX) / 2;
|
|
132
|
+
const labelY = geometry.axis === 'horizontal' || geometry.axis === 'horizontal-reverse'
|
|
133
|
+
? geometry.startY - 10
|
|
134
|
+
: geometry.startY + (geometry.endY - geometry.startY) / 2;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
...geometry,
|
|
138
|
+
d: architectureEdgePath(geometry),
|
|
139
|
+
labelX,
|
|
140
|
+
labelY,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function architectureEdgePath(geometry) {
|
|
145
|
+
const { startX, startY, endX, endY, axis } = geometry;
|
|
146
|
+
if (axis === 'horizontal' || axis === 'horizontal-reverse') {
|
|
147
|
+
const midX = startX + (endX - startX) / 2;
|
|
148
|
+
return `M ${startX} ${startY} C ${midX} ${startY}, ${midX} ${endY}, ${endX} ${endY}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (axis === 'vertical-down') {
|
|
152
|
+
const midY = startY + Math.max(36, (endY - startY) / 2);
|
|
153
|
+
return `M ${startX} ${startY} C ${startX} ${midY}, ${endX} ${midY}, ${endX} ${endY}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const midY = endY + Math.max(36, (startY - endY) / 2);
|
|
157
|
+
return `M ${startX} ${startY} C ${startX} ${midY}, ${endX} ${midY}, ${endX} ${endY}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function buildArchitectureLayout(architecture, focusBlockId = null, options = {}) {
|
|
161
|
+
const viewMode = options.viewMode ?? GRAPH_CANVAS_VIEW_FULL;
|
|
162
|
+
const layoutProfile = options.layoutProfile
|
|
163
|
+
?? resolveLayoutProfileForViewMode(viewMode, {
|
|
164
|
+
full: ARCHITECTURE_LAYOUT_PROFILE,
|
|
165
|
+
pipeline: ARCHITECTURE_PIPELINE_LAYOUT_PROFILE,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const blocks = architecture.blocks || [];
|
|
169
|
+
const allEdges = (architecture.edges || []).map((edge) => ({
|
|
170
|
+
...edge,
|
|
171
|
+
upstream: edge.type === 'maps_to',
|
|
172
|
+
}));
|
|
173
|
+
|
|
174
|
+
const filtered = filterGraphForViewMode({
|
|
175
|
+
nodes: blocks,
|
|
176
|
+
nodeIds: blocks.map((block) => block.id),
|
|
177
|
+
edges: allEdges,
|
|
178
|
+
viewMode,
|
|
179
|
+
pipelineNodeIds: layoutProfile.pipelineNodeIds ?? [],
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const placed = placeGraphCanvasNodes({
|
|
183
|
+
items: filtered.nodes,
|
|
184
|
+
edges: filtered.edges,
|
|
185
|
+
layoutProfile,
|
|
186
|
+
estimateSize: (block, slot) => {
|
|
187
|
+
const colSpan = slot.colSpan ?? 1;
|
|
188
|
+
const width = NODE_WIDTH * colSpan + COL_GAP * Math.max(0, colSpan - 1);
|
|
189
|
+
return {
|
|
190
|
+
width,
|
|
191
|
+
height: estimateArchitectureNodeHeight(block, width),
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
config: GRID_CONFIG,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const nodes = placed.map((node) => ({
|
|
198
|
+
block: node.item,
|
|
199
|
+
x: node.x,
|
|
200
|
+
y: node.y,
|
|
201
|
+
width: node.width,
|
|
202
|
+
height: node.height,
|
|
203
|
+
row: node.row,
|
|
204
|
+
col: node.col,
|
|
205
|
+
focused: focusBlockId === node.id,
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
const nodeById = new Map(nodes.map((node) => [node.block.id, node]));
|
|
209
|
+
const edges = assignArchitectureEdgeLanes(
|
|
210
|
+
filtered.edges
|
|
211
|
+
.map((edge) => ({ ...edge, fromNode: nodeById.get(edge.from), toNode: nodeById.get(edge.to) }))
|
|
212
|
+
.filter((edge) => edge.fromNode && edge.toNode),
|
|
213
|
+
).map((edge) => ({
|
|
214
|
+
...edge,
|
|
215
|
+
geometry: architectureEdgeGeometry(edge),
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
const maxX = Math.max(...nodes.map((node) => node.x + node.width), NODE_WIDTH + OFFSET_X);
|
|
219
|
+
const maxY = Math.max(...nodes.map((node) => node.y + node.height), NODE_MIN_HEIGHT + OFFSET_Y);
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
nodes,
|
|
223
|
+
edges,
|
|
224
|
+
width: maxX + 64,
|
|
225
|
+
height: maxY + 80,
|
|
226
|
+
viewMode,
|
|
227
|
+
layoutProfile,
|
|
228
|
+
};
|
|
229
|
+
}
|