@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,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
INTENT_NODE_KINDS,
|
|
3
|
+
attachDerivedIntentNodeChildren,
|
|
4
|
+
readIntentNodeLink,
|
|
5
|
+
} from './intentNodeRuntime.mjs';
|
|
6
|
+
|
|
7
|
+
export function lintIntentNodeGraph(intentNodes, workItems = []) {
|
|
8
|
+
if (!Array.isArray(intentNodes)) {
|
|
9
|
+
throw new TypeError('intentNodes must be an array');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const issues = [];
|
|
13
|
+
const nodeById = new Map(intentNodes.map((node) => [node.id, node]));
|
|
14
|
+
const workIds = new Set((workItems ?? []).map((item) => item.id));
|
|
15
|
+
|
|
16
|
+
for (const node of intentNodes) {
|
|
17
|
+
if (!INTENT_NODE_KINDS.includes(node.nodeKind)) {
|
|
18
|
+
issues.push({
|
|
19
|
+
severity: 'error',
|
|
20
|
+
code: 'invalid_intent_node_kind',
|
|
21
|
+
message: `Invalid intent.node_kind "${node.nodeKind}" for ${node.id}`,
|
|
22
|
+
intentId: node.id,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (node.parentId === node.id) {
|
|
27
|
+
issues.push({
|
|
28
|
+
severity: 'error',
|
|
29
|
+
code: 'intent_self_parent',
|
|
30
|
+
message: `Intent node cannot be its own parent: ${node.id}`,
|
|
31
|
+
intentId: node.id,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (node.parentId !== '' && !nodeById.has(node.parentId)) {
|
|
36
|
+
issues.push({
|
|
37
|
+
severity: 'error',
|
|
38
|
+
code: 'missing_intent_parent',
|
|
39
|
+
message: `Missing intent.parent_id "${node.parentId}" for ${node.id}`,
|
|
40
|
+
intentId: node.id,
|
|
41
|
+
parentId: node.parentId,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (node.parentId !== '') {
|
|
46
|
+
const visited = new Set();
|
|
47
|
+
let current = node.id;
|
|
48
|
+
while (current !== '') {
|
|
49
|
+
if (visited.has(current)) {
|
|
50
|
+
issues.push({
|
|
51
|
+
severity: 'error',
|
|
52
|
+
code: 'intent_parent_cycle',
|
|
53
|
+
message: `Intent parent cycle detected for ${node.id}`,
|
|
54
|
+
intentId: node.id,
|
|
55
|
+
});
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
visited.add(current);
|
|
59
|
+
current = nodeById.get(current)?.parentId ?? '';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (node.nodeKind === 'decision' && node.selected) {
|
|
64
|
+
const optionId = readIntentNodeLink(node, 'option_id');
|
|
65
|
+
if (optionId === '' || !nodeById.has(optionId)) {
|
|
66
|
+
issues.push({
|
|
67
|
+
severity: 'error',
|
|
68
|
+
code: 'selected_decision_missing_option',
|
|
69
|
+
message: `Selected decision ${node.id} missing valid intent.link.option_id`,
|
|
70
|
+
intentId: node.id,
|
|
71
|
+
optionId,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (node.nodeKind === 'work_ref') {
|
|
77
|
+
const workId = readIntentNodeLink(node, 'work_id') || String(node.labels?.['intent.link.work_id'] ?? '').trim();
|
|
78
|
+
if (workId !== '' && workIds.size > 0 && !workIds.has(workId)) {
|
|
79
|
+
issues.push({
|
|
80
|
+
severity: 'error',
|
|
81
|
+
code: 'work_ref_missing_work_item',
|
|
82
|
+
message: `work_ref ${node.id} points to missing work.id ${workId}`,
|
|
83
|
+
intentId: node.id,
|
|
84
|
+
workId,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return issues;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function lintIntentNodeGraphReport(intentNodes, workItems = []) {
|
|
94
|
+
const issues = lintIntentNodeGraph(intentNodes, workItems);
|
|
95
|
+
const errors = issues.filter((issue) => issue.severity === 'error');
|
|
96
|
+
return {
|
|
97
|
+
schema: 'workgraph.intent-node.lint.v1',
|
|
98
|
+
ok: errors.length === 0,
|
|
99
|
+
nodeCount: intentNodes.length,
|
|
100
|
+
errorCount: errors.length,
|
|
101
|
+
issues,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function summarizeIntentNodeLint(intentNodes, workItems = []) {
|
|
106
|
+
return lintIntentNodeGraphReport(attachDerivedIntentNodeChildren(intentNodes), workItems);
|
|
107
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const STEP_ATOM_PATTERN = /^#([^\n<]+)<\[\n([\s\S]*?)\n\]>/gmu;
|
|
5
|
+
const LIST_SEPARATOR_PATTERN = /\s*,\s*/u;
|
|
6
|
+
|
|
7
|
+
export const INTENT_NODE_KINDS = ['question', 'option', 'decision', 'work_ref', 'evidence_ref'];
|
|
8
|
+
export const INTENT_NODE_PROFILE = 'intent_node';
|
|
9
|
+
|
|
10
|
+
const compareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
|
|
11
|
+
|
|
12
|
+
function parseList(value) {
|
|
13
|
+
if (value === undefined || value === null) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
return String(value)
|
|
17
|
+
.split(LIST_SEPARATOR_PATTERN)
|
|
18
|
+
.map((entry) => entry.trim())
|
|
19
|
+
.filter(Boolean);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseSections(body) {
|
|
23
|
+
const sections = {
|
|
24
|
+
basis: '',
|
|
25
|
+
vector: '',
|
|
26
|
+
goal: '',
|
|
27
|
+
labels: {},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const lines = body.split('\n');
|
|
31
|
+
let current = null;
|
|
32
|
+
let buffer = [];
|
|
33
|
+
|
|
34
|
+
const flush = () => {
|
|
35
|
+
if (current === null) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const text = buffer.join('\n').trim();
|
|
39
|
+
if (current === 'labels') {
|
|
40
|
+
for (const line of buffer) {
|
|
41
|
+
const match = line.match(/^([^:]+):\s*(.*)$/u);
|
|
42
|
+
if (match) {
|
|
43
|
+
sections.labels[match[1].trim()] = match[2].trim();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
sections[current] = text;
|
|
48
|
+
}
|
|
49
|
+
buffer = [];
|
|
50
|
+
current = null;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
if (line === 'Базис:') {
|
|
55
|
+
flush();
|
|
56
|
+
current = 'basis';
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (line === 'Вектор:') {
|
|
60
|
+
flush();
|
|
61
|
+
current = 'vector';
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (line === 'Цель:') {
|
|
65
|
+
flush();
|
|
66
|
+
current = 'goal';
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (line === 'Метки:') {
|
|
70
|
+
flush();
|
|
71
|
+
current = 'labels';
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (current !== null) {
|
|
75
|
+
buffer.push(line);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
flush();
|
|
79
|
+
return sections;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function parseIntentNodes(text) {
|
|
83
|
+
if (typeof text !== 'string') {
|
|
84
|
+
throw new TypeError('text must be a string');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const nodes = [];
|
|
88
|
+
for (const match of text.matchAll(STEP_ATOM_PATTERN)) {
|
|
89
|
+
const [, atomName, body] = match;
|
|
90
|
+
const sections = parseSections(body);
|
|
91
|
+
const labels = sections.labels;
|
|
92
|
+
if (labels['atom.profile'] !== INTENT_NODE_PROFILE) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const id = String(labels['intent.id'] ?? '').trim();
|
|
97
|
+
if (id === '') {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
nodes.push({
|
|
102
|
+
atomName: atomName.trim(),
|
|
103
|
+
id,
|
|
104
|
+
nodeKind: String(labels['intent.node_kind'] ?? '').trim(),
|
|
105
|
+
parentId: String(labels['intent.parent_id'] ?? '').trim(),
|
|
106
|
+
title: String(labels['intent.title'] ?? labels['intent.id'] ?? id).trim(),
|
|
107
|
+
selected: String(labels['intent.selected'] ?? '').trim().toLowerCase() === 'true',
|
|
108
|
+
basis: sections.basis,
|
|
109
|
+
vector: sections.vector,
|
|
110
|
+
goal: sections.goal,
|
|
111
|
+
links: Object.fromEntries(
|
|
112
|
+
Object.entries(labels)
|
|
113
|
+
.filter(([key]) => key.startsWith('intent.link.'))
|
|
114
|
+
.map(([key, value]) => [key.slice('intent.link.'.length), value]),
|
|
115
|
+
),
|
|
116
|
+
labels,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return nodes;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function listStepFiles(rootDir) {
|
|
124
|
+
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
125
|
+
const nested = await Promise.all(
|
|
126
|
+
entries.map(async (entry) => {
|
|
127
|
+
const entryPath = join(rootDir, entry.name);
|
|
128
|
+
if (entry.isDirectory()) {
|
|
129
|
+
return listStepFiles(entryPath);
|
|
130
|
+
}
|
|
131
|
+
return entry.isFile() && entry.name.endsWith('.bvc') ? [entryPath] : [];
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
return nested.flat();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function readIntentNodesFromRepo(options = {}) {
|
|
138
|
+
const cwd = options.cwd ?? process.cwd();
|
|
139
|
+
const intentRoot = resolve(cwd, options.intentRoot ?? 'intent');
|
|
140
|
+
try {
|
|
141
|
+
const files = await listStepFiles(intentRoot);
|
|
142
|
+
const nodes = [];
|
|
143
|
+
|
|
144
|
+
for (const filePath of files.sort(compareText)) {
|
|
145
|
+
const text = await readFile(filePath, 'utf8');
|
|
146
|
+
nodes.push(...parseIntentNodes(text));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return nodes.sort((left, right) => compareText(left.id, right.id));
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error && typeof error === 'object' && error.code === 'ENOENT') {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function buildChildIdsByIntentParent(nodes) {
|
|
159
|
+
const childIdsByParent = new Map();
|
|
160
|
+
for (const node of nodes) {
|
|
161
|
+
if (node.parentId === '') {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (!childIdsByParent.has(node.parentId)) {
|
|
165
|
+
childIdsByParent.set(node.parentId, []);
|
|
166
|
+
}
|
|
167
|
+
childIdsByParent.get(node.parentId).push(node.id);
|
|
168
|
+
}
|
|
169
|
+
for (const childIds of childIdsByParent.values()) {
|
|
170
|
+
childIds.sort(compareText);
|
|
171
|
+
}
|
|
172
|
+
return childIdsByParent;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function attachDerivedIntentNodeChildren(nodes) {
|
|
176
|
+
const childIdsByParent = buildChildIdsByIntentParent(nodes);
|
|
177
|
+
return nodes.map((node) => ({
|
|
178
|
+
...node,
|
|
179
|
+
childIds: [...(childIdsByParent.get(node.id) ?? [])],
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function readIntentNodeLink(node, key) {
|
|
184
|
+
return String(node?.links?.[key] ?? node?.labels?.[`intent.link.${key}`] ?? '').trim();
|
|
185
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { layoutGraphWithDagre } from './dagreGraphLayout.mjs';
|
|
2
|
+
import { buildGraphCanvasEdgeGeometry } from './graphCanvasLitFlow/graphCanvasEdgeRouter.mjs';
|
|
3
|
+
import { N8N_INSPIRED_DAGRE_LR } from './graphCanvasLitFlow/graphCanvasLayoutProfile.mjs';
|
|
4
|
+
import {
|
|
5
|
+
GRAPH_CARD_MIN_HEIGHT,
|
|
6
|
+
GRAPH_CARD_WIDTH,
|
|
7
|
+
estimateGraphCardHeight,
|
|
8
|
+
} from './graphCanvasLitFlow/graphCanvasNodeMetrics.mjs';
|
|
9
|
+
import {
|
|
10
|
+
isIntentRoadmapIntentKind,
|
|
11
|
+
layoutIntentRoadmapWorkStack,
|
|
12
|
+
} from './graphCanvasLitFlow/layoutIntentRoadmapWorkStack.mjs';
|
|
13
|
+
import { findAllOptionsForQuestion } from './intentRoadmapMermaid.mjs';
|
|
14
|
+
|
|
15
|
+
export const INTENT_ROADMAP_CANVAS_SCHEMA = 'workgraph.intent-roadmap.canvas.v1';
|
|
16
|
+
|
|
17
|
+
const NODE_WIDTH = GRAPH_CARD_WIDTH;
|
|
18
|
+
const NODE_MIN_HEIGHT = GRAPH_CARD_MIN_HEIGHT;
|
|
19
|
+
const OFFSET_PADDING = 40;
|
|
20
|
+
|
|
21
|
+
function estimateCanvasNodeHeight(title, options = {}) {
|
|
22
|
+
return estimateGraphCardHeight({
|
|
23
|
+
title,
|
|
24
|
+
status: options.status,
|
|
25
|
+
summary: options.summary,
|
|
26
|
+
layer: options.layer !== false,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const KIND_LABELS = {
|
|
31
|
+
intent_question: 'вопрос',
|
|
32
|
+
intent_analysis: 'анализ',
|
|
33
|
+
intent_option: 'вариант',
|
|
34
|
+
intent_decision: 'решение',
|
|
35
|
+
work_item: 'задача',
|
|
36
|
+
work_epic: 'эпик',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function readOptionId(decision) {
|
|
40
|
+
return String(decision?.links?.option_id ?? decision?.links?.['intent.link.option_id'] ?? '').trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findQuestionForDecision(decision, intentNodes) {
|
|
44
|
+
const parentId = String(decision?.parentId ?? '').trim();
|
|
45
|
+
const parent = intentNodes.find((node) => node.id === parentId);
|
|
46
|
+
if (parent?.nodeKind === 'question') {
|
|
47
|
+
return parent;
|
|
48
|
+
}
|
|
49
|
+
return intentNodes.find((node) =>
|
|
50
|
+
node.nodeKind === 'question'
|
|
51
|
+
&& (node.childIds ?? []).includes(decision.id),
|
|
52
|
+
) ?? null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function findSelectedOption(decision, intentNodes) {
|
|
56
|
+
const optionId = readOptionId(decision);
|
|
57
|
+
if (optionId === '') {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return intentNodes.find((node) => node.id === optionId && node.nodeKind === 'option') ?? null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function analyticsNodeId(analyticsRef) {
|
|
64
|
+
return `analysis:${String(analyticsRef).replace(/^analytics:/u, '')}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function analyticsTitle(analyticsRef) {
|
|
68
|
+
const ref = String(analyticsRef ?? '').trim();
|
|
69
|
+
if (ref === '') {
|
|
70
|
+
return 'Анализ';
|
|
71
|
+
}
|
|
72
|
+
return `Анализ ${ref.replace(/^analytics:/u, '')}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function flattenWorkTree(roots, options = {}) {
|
|
76
|
+
const collapsedEpicIds = options.collapsedEpicIds instanceof Set ? options.collapsedEpicIds : new Set();
|
|
77
|
+
/** @type {Array<{ id: string, kind: 'work_item'|'work_epic', title: string, status: string, workId: string, doneChildCount?: number, childCount?: number, collapsible?: boolean, collapsed?: boolean }>} */
|
|
78
|
+
const nodes = [];
|
|
79
|
+
/** @type {Array<{ from: string, to: string, label: string }>} */
|
|
80
|
+
const edges = [];
|
|
81
|
+
|
|
82
|
+
function walk(node, parentWorkId = '') {
|
|
83
|
+
const isEpic = node.itemKind === 'epic';
|
|
84
|
+
const kind = isEpic ? 'work_epic' : 'work_item';
|
|
85
|
+
const collapsed = isEpic && collapsedEpicIds.has(node.workId);
|
|
86
|
+
|
|
87
|
+
nodes.push({
|
|
88
|
+
id: node.workId,
|
|
89
|
+
kind,
|
|
90
|
+
title: node.title ?? node.workId,
|
|
91
|
+
status: node.status ?? '',
|
|
92
|
+
workId: node.workId,
|
|
93
|
+
doneChildCount: node.doneChildCount ?? 0,
|
|
94
|
+
childCount: node.childCount ?? 0,
|
|
95
|
+
collapsible: isEpic && (node.childCount ?? 0) > 0,
|
|
96
|
+
collapsed,
|
|
97
|
+
});
|
|
98
|
+
if (parentWorkId !== '') {
|
|
99
|
+
edges.push({ from: parentWorkId, to: node.workId, label: 'подзадача' });
|
|
100
|
+
}
|
|
101
|
+
if (!collapsed) {
|
|
102
|
+
for (const child of node.children ?? []) {
|
|
103
|
+
walk(child, node.workId);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const root of roots) {
|
|
109
|
+
walk(root);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { nodes, edges };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {{
|
|
117
|
+
* decisionId: string,
|
|
118
|
+
* decisionTitle?: string,
|
|
119
|
+
* analyticsRef?: string,
|
|
120
|
+
* question?: object | null,
|
|
121
|
+
* selectedOption?: object | null,
|
|
122
|
+
* allOptions?: Array<object>,
|
|
123
|
+
* decision?: object,
|
|
124
|
+
* roots?: Array<object>,
|
|
125
|
+
* }} branch
|
|
126
|
+
*/
|
|
127
|
+
export function buildIntentRoadmapCanvasModel(branch, options = {}) {
|
|
128
|
+
const decision = branch.decision ?? { id: branch.decisionId, title: branch.decisionTitle };
|
|
129
|
+
const decisionId = decision.id ?? branch.decisionId;
|
|
130
|
+
const selectedOptionId = branch.selectedOption?.id ?? readOptionId(decision);
|
|
131
|
+
const allOptions = branch.allOptions ?? (branch.selectedOption ? [branch.selectedOption] : []);
|
|
132
|
+
|
|
133
|
+
/** @type {Array<{ id: string, kind: string, title: string, width: number, height: number, selected?: boolean, status?: string, workId?: string, doneChildCount?: number, childCount?: number }>} */
|
|
134
|
+
const specNodes = [];
|
|
135
|
+
/** @type {Array<{ from: string, to: string, label: string, rejected?: boolean }>} */
|
|
136
|
+
const specEdges = [];
|
|
137
|
+
|
|
138
|
+
let optionAnchorId = null;
|
|
139
|
+
|
|
140
|
+
if (branch.question) {
|
|
141
|
+
const title = branch.question.title ?? branch.question.id;
|
|
142
|
+
specNodes.push({
|
|
143
|
+
id: branch.question.id,
|
|
144
|
+
kind: 'intent_question',
|
|
145
|
+
title,
|
|
146
|
+
width: NODE_WIDTH,
|
|
147
|
+
height: estimateCanvasNodeHeight(title),
|
|
148
|
+
});
|
|
149
|
+
optionAnchorId = branch.question.id;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (branch.analyticsRef) {
|
|
153
|
+
const analysisId = analyticsNodeId(branch.analyticsRef);
|
|
154
|
+
const title = analyticsTitle(branch.analyticsRef);
|
|
155
|
+
specNodes.push({
|
|
156
|
+
id: analysisId,
|
|
157
|
+
kind: 'intent_analysis',
|
|
158
|
+
title,
|
|
159
|
+
width: NODE_WIDTH,
|
|
160
|
+
height: estimateCanvasNodeHeight(title),
|
|
161
|
+
});
|
|
162
|
+
if (optionAnchorId) {
|
|
163
|
+
specEdges.push({ from: optionAnchorId, to: analysisId, label: 'разбор' });
|
|
164
|
+
}
|
|
165
|
+
optionAnchorId = analysisId;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const option of allOptions) {
|
|
169
|
+
const title = option.title ?? option.id;
|
|
170
|
+
const selected = option.id === selectedOptionId || option.selected === true;
|
|
171
|
+
specNodes.push({
|
|
172
|
+
id: option.id,
|
|
173
|
+
kind: 'intent_option',
|
|
174
|
+
title,
|
|
175
|
+
width: NODE_WIDTH,
|
|
176
|
+
height: estimateCanvasNodeHeight(title),
|
|
177
|
+
selected,
|
|
178
|
+
});
|
|
179
|
+
if (optionAnchorId) {
|
|
180
|
+
specEdges.push({
|
|
181
|
+
from: optionAnchorId,
|
|
182
|
+
to: option.id,
|
|
183
|
+
label: 'вариант',
|
|
184
|
+
rejected: !selected,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (selected) {
|
|
188
|
+
specEdges.push({ from: option.id, to: decisionId, label: 'выбрано' });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const decisionTitle = decision.title ?? branch.decisionTitle ?? branch.decisionId;
|
|
193
|
+
specNodes.push({
|
|
194
|
+
id: decisionId,
|
|
195
|
+
kind: 'intent_decision',
|
|
196
|
+
title: decisionTitle,
|
|
197
|
+
width: NODE_WIDTH,
|
|
198
|
+
height: estimateCanvasNodeHeight(decisionTitle),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (!allOptions.some((option) => option.id === selectedOptionId) && optionAnchorId) {
|
|
202
|
+
specEdges.push({ from: optionAnchorId, to: decisionId, label: 'решение' });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const workTree = flattenWorkTree(branch.roots ?? [], options);
|
|
206
|
+
for (const workNode of workTree.nodes) {
|
|
207
|
+
specNodes.push({
|
|
208
|
+
...workNode,
|
|
209
|
+
width: NODE_WIDTH,
|
|
210
|
+
height: estimateCanvasNodeHeight(workNode.title, { status: workNode.status }),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const root of branch.roots ?? []) {
|
|
215
|
+
specEdges.push({ from: decisionId, to: root.workId, label: 'порождает' });
|
|
216
|
+
}
|
|
217
|
+
specEdges.push(...workTree.edges);
|
|
218
|
+
|
|
219
|
+
const layoutDirection = 'LR';
|
|
220
|
+
const intentSpecNodes = specNodes.filter((node) => isIntentRoadmapIntentKind(node.kind));
|
|
221
|
+
const workSpecNodes = specNodes.filter((node) => node.kind === 'work_item' || node.kind === 'work_epic');
|
|
222
|
+
const intentSpecEdges = specEdges.filter((edge) => {
|
|
223
|
+
const fromKind = specNodes.find((node) => node.id === edge.from)?.kind;
|
|
224
|
+
const toKind = specNodes.find((node) => node.id === edge.to)?.kind;
|
|
225
|
+
return isIntentRoadmapIntentKind(fromKind) && isIntentRoadmapIntentKind(toKind);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const placedIntent = layoutGraphWithDagre(intentSpecNodes, intentSpecEdges, {
|
|
229
|
+
...N8N_INSPIRED_DAGRE_LR,
|
|
230
|
+
rankdir: layoutDirection,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const decisionPlaced = placedIntent.find((node) => node.id === decisionId);
|
|
234
|
+
const placedWork = decisionPlaced
|
|
235
|
+
? layoutIntentRoadmapWorkStack(workSpecNodes, workTree.edges, decisionPlaced, {
|
|
236
|
+
ranksep: N8N_INSPIRED_DAGRE_LR.ranksep,
|
|
237
|
+
gap: N8N_INSPIRED_DAGRE_LR.overlapGap,
|
|
238
|
+
})
|
|
239
|
+
: new Map();
|
|
240
|
+
|
|
241
|
+
const placed = [
|
|
242
|
+
...placedIntent,
|
|
243
|
+
...workSpecNodes.map((node) => placedWork.get(node.id) ?? node),
|
|
244
|
+
];
|
|
245
|
+
const nodeById = new Map(placed.map((node) => [node.id, {
|
|
246
|
+
...node,
|
|
247
|
+
layer: KIND_LABELS[node.kind] ?? node.kind,
|
|
248
|
+
}]));
|
|
249
|
+
|
|
250
|
+
const edges = specEdges
|
|
251
|
+
.map((edge) => ({
|
|
252
|
+
...edge,
|
|
253
|
+
fromNode: nodeById.get(edge.from),
|
|
254
|
+
toNode: nodeById.get(edge.to),
|
|
255
|
+
}))
|
|
256
|
+
.filter((edge) => edge.fromNode && edge.toNode)
|
|
257
|
+
.map((edge) => ({
|
|
258
|
+
...edge,
|
|
259
|
+
geometry: intentRoadmapEdgeGeometry(edge, layoutDirection),
|
|
260
|
+
}));
|
|
261
|
+
|
|
262
|
+
const layoutNodes = [...nodeById.values()];
|
|
263
|
+
const maxX = Math.max(...layoutNodes.map((node) => node.x + node.width), NODE_WIDTH);
|
|
264
|
+
const maxY = Math.max(...layoutNodes.map((node) => node.y + node.height), NODE_MIN_HEIGHT);
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
schema: INTENT_ROADMAP_CANVAS_SCHEMA,
|
|
268
|
+
layoutEngine: 'dagre+work-stack',
|
|
269
|
+
layoutDirection,
|
|
270
|
+
nodes: layoutNodes,
|
|
271
|
+
edges,
|
|
272
|
+
width: maxX + OFFSET_PADDING,
|
|
273
|
+
height: maxY + OFFSET_PADDING,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function buildEpicRoadmapCanvasModel(epicEntry, options = {}) {
|
|
278
|
+
const collapsedEpicIds = options.collapsedEpicIds instanceof Set ? options.collapsedEpicIds : new Set();
|
|
279
|
+
const collapsed = collapsedEpicIds.has(epicEntry.epicId);
|
|
280
|
+
const layoutDirection = 'TB';
|
|
281
|
+
|
|
282
|
+
/** @type {Array<{ id: string, kind: string, title: string, width: number, height: number, status?: string, workId?: string, doneChildCount?: number, childCount?: number, collapsible?: boolean, collapsed?: boolean }>} */
|
|
283
|
+
const specNodes = [];
|
|
284
|
+
/** @type {Array<{ from: string, to: string, label: string }>} */
|
|
285
|
+
const specEdges = [];
|
|
286
|
+
|
|
287
|
+
specNodes.push({
|
|
288
|
+
id: epicEntry.epicId,
|
|
289
|
+
kind: 'work_epic',
|
|
290
|
+
title: epicEntry.title ?? epicEntry.epicId,
|
|
291
|
+
status: epicEntry.status ?? '',
|
|
292
|
+
workId: epicEntry.epicId,
|
|
293
|
+
doneChildCount: epicEntry.doneChildCount ?? 0,
|
|
294
|
+
childCount: epicEntry.childCount ?? 0,
|
|
295
|
+
collapsible: (epicEntry.childCount ?? 0) > 0,
|
|
296
|
+
collapsed,
|
|
297
|
+
width: NODE_WIDTH,
|
|
298
|
+
height: estimateCanvasNodeHeight(epicEntry.title ?? epicEntry.epicId, {
|
|
299
|
+
status: epicEntry.status,
|
|
300
|
+
}),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (!collapsed) {
|
|
304
|
+
for (const child of epicEntry.children ?? []) {
|
|
305
|
+
specNodes.push({
|
|
306
|
+
id: child.workId,
|
|
307
|
+
kind: 'work_item',
|
|
308
|
+
title: child.title ?? child.workId,
|
|
309
|
+
status: child.status ?? '',
|
|
310
|
+
workId: child.workId,
|
|
311
|
+
width: NODE_WIDTH,
|
|
312
|
+
height: estimateCanvasNodeHeight(child.title ?? child.workId, { status: child.status }),
|
|
313
|
+
});
|
|
314
|
+
specEdges.push({ from: epicEntry.epicId, to: child.workId, label: 'подзадача' });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
let y = OFFSET_PADDING;
|
|
319
|
+
const x = OFFSET_PADDING;
|
|
320
|
+
const gap = 36;
|
|
321
|
+
const layoutNodes = specNodes.map((node) => {
|
|
322
|
+
const placed = {
|
|
323
|
+
...node,
|
|
324
|
+
x,
|
|
325
|
+
y,
|
|
326
|
+
layer: KIND_LABELS[node.kind] ?? node.kind,
|
|
327
|
+
};
|
|
328
|
+
y += node.height + gap;
|
|
329
|
+
return placed;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const nodeById = new Map(layoutNodes.map((node) => [node.id, node]));
|
|
333
|
+
const edges = specEdges
|
|
334
|
+
.map((edge) => ({
|
|
335
|
+
...edge,
|
|
336
|
+
fromNode: nodeById.get(edge.from),
|
|
337
|
+
toNode: nodeById.get(edge.to),
|
|
338
|
+
}))
|
|
339
|
+
.filter((edge) => edge.fromNode && edge.toNode)
|
|
340
|
+
.map((edge) => ({
|
|
341
|
+
...edge,
|
|
342
|
+
geometry: intentRoadmapEdgeGeometry(edge, layoutDirection),
|
|
343
|
+
}));
|
|
344
|
+
|
|
345
|
+
const maxX = Math.max(...layoutNodes.map((node) => node.x + node.width), NODE_WIDTH);
|
|
346
|
+
const maxY = Math.max(...layoutNodes.map((node) => node.y + node.height), NODE_MIN_HEIGHT);
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
schema: INTENT_ROADMAP_CANVAS_SCHEMA,
|
|
350
|
+
layoutEngine: 'epic-stack',
|
|
351
|
+
layoutDirection,
|
|
352
|
+
nodes: layoutNodes,
|
|
353
|
+
edges,
|
|
354
|
+
width: maxX + OFFSET_PADDING,
|
|
355
|
+
height: maxY + OFFSET_PADDING,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function enrichIntentRoadmapBranchWithCanvas(branch, intentNodes, options = {}) {
|
|
360
|
+
const decisionNode = intentNodes.find((node) => node.id === branch.decisionId && node.nodeKind === 'decision')
|
|
361
|
+
?? {
|
|
362
|
+
id: branch.decisionId,
|
|
363
|
+
nodeKind: 'decision',
|
|
364
|
+
title: branch.decisionTitle,
|
|
365
|
+
parentId: '',
|
|
366
|
+
links: {},
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const question = findQuestionForDecision(decisionNode, intentNodes);
|
|
370
|
+
const enriched = {
|
|
371
|
+
...branch,
|
|
372
|
+
question,
|
|
373
|
+
selectedOption: findSelectedOption(decisionNode, intentNodes),
|
|
374
|
+
allOptions: findAllOptionsForQuestion(question, intentNodes).map((option) => ({
|
|
375
|
+
id: option.id,
|
|
376
|
+
title: option.title,
|
|
377
|
+
selected: option.selected === true || option.id === readOptionId(decisionNode),
|
|
378
|
+
})),
|
|
379
|
+
decision: decisionNode,
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
enriched.canvas = buildIntentRoadmapCanvasModel(enriched, options);
|
|
383
|
+
return enriched;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function intentRoadmapEdgeGeometry(edge, layoutDirection = 'LR') {
|
|
387
|
+
return buildGraphCanvasEdgeGeometry(edge, layoutDirection);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export {
|
|
391
|
+
findQuestionForDecision,
|
|
392
|
+
findSelectedOption,
|
|
393
|
+
};
|