@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.
Files changed (194) hide show
  1. package/README.md +31 -0
  2. package/bin/work-graph.mjs +238 -0
  3. package/package.json +38 -0
  4. package/vendor/packages/design-tokens/generated/gripe-dark-default.css +67 -0
  5. package/vendor/packages/design-tokens/generated/marketplace-default.css +67 -0
  6. package/vendor/packages/design-tokens/generated/workgraph-dark.css +67 -0
  7. package/vendor/packages/workgraph-mcp/README.md +28 -0
  8. package/vendor/packages/workgraph-mcp/bin/workgraph-mcp.mjs +21 -0
  9. package/vendor/packages/workgraph-mcp/package.json +37 -0
  10. package/vendor/packages/workgraph-mcp/src/handlers.mjs +761 -0
  11. package/vendor/packages/workgraph-mcp/src/index.mjs +638 -0
  12. package/vendor/packages/workgraph-mcp/src/prompts.mjs +162 -0
  13. package/vendor/public/assets/workgraph-logo.svg +11 -0
  14. package/vendor/public/fonts/GraphikLCG/GraphikLCG-Medium.woff2 +0 -0
  15. package/vendor/public/fonts/GraphikLCG/GraphikLCG-Regular.woff2 +0 -0
  16. package/vendor/public/fonts/GraphikLCG/GraphikLCG-Semibold.woff2 +0 -0
  17. package/vendor/public/fonts/GraphikLCG/stylesheet.css +25 -0
  18. package/vendor/public/graph-canvas-lit-flow.css +154 -0
  19. package/vendor/public/graph-canvas-lit-flow.css.map +7 -0
  20. package/vendor/public/graph-canvas-lit-flow.js +8530 -0
  21. package/vendor/public/graph-canvas-lit-flow.js.map +7 -0
  22. package/vendor/src/agentBehaviorRulesAudit.mjs +168 -0
  23. package/vendor/src/agentBehaviorRulesBundle.mjs +144 -0
  24. package/vendor/src/agentRunApi.mjs +136 -0
  25. package/vendor/src/agentToolLoopGuard.mjs +88 -0
  26. package/vendor/src/agentWorkerClaudeProvider.mjs +288 -0
  27. package/vendor/src/agentWorkerCursorSdkProvider.mjs +156 -0
  28. package/vendor/src/agentWorkerLiveLoop.mjs +455 -0
  29. package/vendor/src/agentWorkerLocalCliProvider.mjs +217 -0
  30. package/vendor/src/agentWorkerLocalRunner.mjs +246 -0
  31. package/vendor/src/agentWorkerOpenAiProvider.mjs +459 -0
  32. package/vendor/src/analyticsPanelProjection.mjs +212 -0
  33. package/vendor/src/analyticsRecordStore.mjs +165 -0
  34. package/vendor/src/analyticsRecordWorkItems.mjs +104 -0
  35. package/vendor/src/architectureL1Canon.mjs +419 -0
  36. package/vendor/src/architectureLayout.mjs +229 -0
  37. package/vendor/src/architectureSnapshot.mjs +490 -0
  38. package/vendor/src/architectureViewsProjection.mjs +116 -0
  39. package/vendor/src/atomInspector.mjs +253 -0
  40. package/vendor/src/atomInspectorApi.mjs +130 -0
  41. package/vendor/src/auditGapMatrixRefresh.mjs +121 -0
  42. package/vendor/src/backlogSchemaLint.mjs +176 -0
  43. package/vendor/src/blockedOnebaseGoPreflightEval.mjs +100 -0
  44. package/vendor/src/bracketIrTraceSignal.mjs +93 -0
  45. package/vendor/src/bvcAtomParser.mjs +210 -0
  46. package/vendor/src/bvcDialectRegistry.mjs +86 -0
  47. package/vendor/src/bvcFileFormat.mjs +218 -0
  48. package/vendor/src/bvcFormatCli.mjs +55 -0
  49. package/vendor/src/bvcLintCli.mjs +48 -0
  50. package/vendor/src/bvcNewWritePolicy.mjs +70 -0
  51. package/vendor/src/charterPreflightPromoteGate.mjs +194 -0
  52. package/vendor/src/claimNoEligibleEval.mjs +205 -0
  53. package/vendor/src/closingAnalysisSuggest.mjs +59 -0
  54. package/vendor/src/codeGapAnalyzer.mjs +308 -0
  55. package/vendor/src/codeGapBacklogFeeder.mjs +82 -0
  56. package/vendor/src/codeGapDraftIntakeApi.mjs +307 -0
  57. package/vendor/src/codeGapOperatorProjection.mjs +60 -0
  58. package/vendor/src/codeSyntaxHighlight.mjs +123 -0
  59. package/vendor/src/codegenEvidence.mjs +187 -0
  60. package/vendor/src/compilerRoundTripCli.mjs +164 -0
  61. package/vendor/src/dagreGraphLayout.mjs +78 -0
  62. package/vendor/src/draftIntakePromotionRules.mjs +205 -0
  63. package/vendor/src/epicWorkScope.mjs +85 -0
  64. package/vendor/src/evalLiveLlmEnv.mjs +63 -0
  65. package/vendor/src/evidenceReadModel.mjs +167 -0
  66. package/vendor/src/gfsOverlayProjectPassport.mjs +235 -0
  67. package/vendor/src/globalStepPathToBvcReferences.mjs +196 -0
  68. package/vendor/src/goldenPath.mjs +69 -0
  69. package/vendor/src/graphCanvasLayout.mjs +464 -0
  70. package/vendor/src/graphCanvasLitFlow/client/graphCanvasMinimap.ts +261 -0
  71. package/vendor/src/graphCanvasLitFlow/client/graphCanvasSvgEdges.ts +259 -0
  72. package/vendor/src/graphCanvasLitFlow/client/graphCanvasTheme.css +152 -0
  73. package/vendor/src/graphCanvasLitFlow/client/graphCardNode.ts +328 -0
  74. package/vendor/src/graphCanvasLitFlow/client/mountGraphCanvasLitFlow.ts +322 -0
  75. package/vendor/src/graphCanvasLitFlow/graphCanvasEdgeLabels.mjs +58 -0
  76. package/vendor/src/graphCanvasLitFlow/graphCanvasEdgeRouter.mjs +142 -0
  77. package/vendor/src/graphCanvasLitFlow/graphCanvasLayoutProfile.mjs +32 -0
  78. package/vendor/src/graphCanvasLitFlow/graphCanvasNodeMetrics.mjs +45 -0
  79. package/vendor/src/graphCanvasLitFlow/graphCanvasProjection.mjs +115 -0
  80. package/vendor/src/graphCanvasLitFlow/graphCanvasProjectionToFlow.mjs +133 -0
  81. package/vendor/src/graphCanvasLitFlow/graphCanvasTraversal.mjs +77 -0
  82. package/vendor/src/graphCanvasLitFlow/layoutIntentRoadmapWorkStack.mjs +73 -0
  83. package/vendor/src/graphCanvasLitFlow/resolveGraphCanvasOverlaps.mjs +77 -0
  84. package/vendor/src/graphRagContextSlice.mjs +461 -0
  85. package/vendor/src/gvmVerifyWorkerGate.mjs +95 -0
  86. package/vendor/src/homeSnapshotApi.mjs +131 -0
  87. package/vendor/src/homeSnapshotProjection.mjs +275 -0
  88. package/vendor/src/inboxEventStream.mjs +140 -0
  89. package/vendor/src/intentComposerApi.mjs +245 -0
  90. package/vendor/src/intentGraphGbcSliceBoundary.mjs +258 -0
  91. package/vendor/src/intentGraphProjection.mjs +208 -0
  92. package/vendor/src/intentHierarchy.mjs +241 -0
  93. package/vendor/src/intentNodeLint.mjs +107 -0
  94. package/vendor/src/intentNodeRuntime.mjs +185 -0
  95. package/vendor/src/intentRoadmapCanvas.mjs +393 -0
  96. package/vendor/src/intentRoadmapEpicProjection.mjs +122 -0
  97. package/vendor/src/intentRoadmapMermaid.mjs +165 -0
  98. package/vendor/src/intentRoadmapProjection.mjs +85 -0
  99. package/vendor/src/intentTreeLint.mjs +114 -0
  100. package/vendor/src/intentTreeMigration.mjs +150 -0
  101. package/vendor/src/intentTreeWorkItems.mjs +227 -0
  102. package/vendor/src/kanbanBoardProjection.mjs +58 -0
  103. package/vendor/src/languageAdapterRegistry.mjs +180 -0
  104. package/vendor/src/languageAdapters/goAdapter.mjs +62 -0
  105. package/vendor/src/languageAdapters/jsTsAdapter.mjs +60 -0
  106. package/vendor/src/languageAdapters/jsonYamlAdapter.mjs +103 -0
  107. package/vendor/src/languageAdapters/onebaseOsAdapter.mjs +55 -0
  108. package/vendor/src/languageAdapters/plaintextAdapter.mjs +36 -0
  109. package/vendor/src/languageAdapters/shared.mjs +68 -0
  110. package/vendor/src/languageAdapters/stepAdapter.mjs +81 -0
  111. package/vendor/src/lintPlanWorkAlignment.mjs +136 -0
  112. package/vendor/src/loopHintRepeatToolEval.mjs +153 -0
  113. package/vendor/src/lowcodeScaffoldCli.mjs +386 -0
  114. package/vendor/src/markdownDocumentRender.mjs +208 -0
  115. package/vendor/src/memoryPanelProjection.mjs +116 -0
  116. package/vendor/src/memoryRecordWriter.mjs +243 -0
  117. package/vendor/src/memoryWorkerSlice.mjs +238 -0
  118. package/vendor/src/migrateStepToBvc.mjs +133 -0
  119. package/vendor/src/missionControlServerHandlers.mjs +195 -0
  120. package/vendor/src/missionControlUiClient.mjs +278 -0
  121. package/vendor/src/onebaseCliCapabilityProbe.mjs +107 -0
  122. package/vendor/src/onebaseCliRunner.mjs +145 -0
  123. package/vendor/src/onebaseGrossProfitStaticVerify.mjs +98 -0
  124. package/vendor/src/onebaseParityEvidenceSync.mjs +88 -0
  125. package/vendor/src/onebasePvrgGraphNodes.mjs +257 -0
  126. package/vendor/src/onebaseRestEvidenceAdapter.mjs +216 -0
  127. package/vendor/src/onebaseVectorDslCodegenReadiness.mjs +137 -0
  128. package/vendor/src/onebaseWorkItemTemplate.mjs +154 -0
  129. package/vendor/src/onebaseWorkerTools.mjs +586 -0
  130. package/vendor/src/operatorShellProjection.mjs +102 -0
  131. package/vendor/src/pipelineProseRender.mjs +180 -0
  132. package/vendor/src/pipelineStageLint.mjs +118 -0
  133. package/vendor/src/promptRulesEditorApi.mjs +174 -0
  134. package/vendor/src/promptRulesProjection.mjs +134 -0
  135. package/vendor/src/pvrg/bladeAdapter.mjs +40 -0
  136. package/vendor/src/pvrgTaskScope.mjs +152 -0
  137. package/vendor/src/releaseGateMatrix.mjs +188 -0
  138. package/vendor/src/schematicView.mjs +305 -0
  139. package/vendor/src/seedAnalyticsRecord.mjs +217 -0
  140. package/vendor/src/semanticSearchBm25.mjs +103 -0
  141. package/vendor/src/semanticSearchExcerpts.mjs +68 -0
  142. package/vendor/src/semanticSearchTfidfVector.mjs +86 -0
  143. package/vendor/src/semanticSearchWorkflow.mjs +366 -0
  144. package/vendor/src/stepAtomFormatter.mjs +413 -0
  145. package/vendor/src/stepGraphSlice.mjs +318 -0
  146. package/vendor/src/ui/atoms/badge.mjs +40 -0
  147. package/vendor/src/ui/atoms/badgeClient.mjs +32 -0
  148. package/vendor/src/ui/atoms/button.mjs +114 -0
  149. package/vendor/src/ui/atoms/buttonClient.mjs +49 -0
  150. package/vendor/src/ui/atoms/icon.mjs +23 -0
  151. package/vendor/src/ui/atoms/input.mjs +38 -0
  152. package/vendor/src/ui/atoms/modal.mjs +44 -0
  153. package/vendor/src/ui/atoms/select.mjs +98 -0
  154. package/vendor/src/ui/backlogShellButtons.mjs +238 -0
  155. package/vendor/src/ui/htmlEscape.mjs +11 -0
  156. package/vendor/src/ui/molecules/rating.mjs +48 -0
  157. package/vendor/src/ui/molecules/tabs.mjs +70 -0
  158. package/vendor/src/ui/organisms/modal.mjs +1 -0
  159. package/vendor/src/ui/pages/uiKitPage.mjs +147 -0
  160. package/vendor/src/ui/workItemStatusTone.mjs +36 -0
  161. package/vendor/src/unifiedLinkageProjection.mjs +264 -0
  162. package/vendor/src/verificationLoop.mjs +206 -0
  163. package/vendor/src/workGraphBacklogPersist.mjs +234 -0
  164. package/vendor/src/workGraphBacklogUiServer.mjs +9192 -0
  165. package/vendor/src/workGraphBoundedTargetFileRead.mjs +178 -0
  166. package/vendor/src/workGraphCycleSlice.mjs +184 -0
  167. package/vendor/src/workGraphDaemonTick.mjs +307 -0
  168. package/vendor/src/workGraphDaemonWatch.mjs +157 -0
  169. package/vendor/src/workGraphEngineRoot.mjs +136 -0
  170. package/vendor/src/workGraphInstallLayout.mjs +65 -0
  171. package/vendor/src/workGraphLlmUsefulnessEval.mjs +611 -0
  172. package/vendor/src/workGraphPhasePromoteReadyQueue.mjs +159 -0
  173. package/vendor/src/workGraphProjectHost.mjs +149 -0
  174. package/vendor/src/workGraphProjectInit.mjs +392 -0
  175. package/vendor/src/workGraphPromoteReadyApi.mjs +115 -0
  176. package/vendor/src/workGraphRecoveryPolicy.mjs +124 -0
  177. package/vendor/src/workGraphRunnerQueueProjection.mjs +187 -0
  178. package/vendor/src/workGraphRuntime.mjs +1008 -0
  179. package/vendor/src/workGraphToolSurfaceAudit.mjs +372 -0
  180. package/vendor/src/workGraphToolTransportRuntime.mjs +195 -0
  181. package/vendor/src/workGraphWorkerProvider.mjs +600 -0
  182. package/vendor/src/workItemBvcQuality.mjs +262 -0
  183. package/vendor/src/workItemCreateAnalysis.mjs +157 -0
  184. package/vendor/src/workItemDecisionPipeline.mjs +278 -0
  185. package/vendor/src/workItemEpicCascade.mjs +176 -0
  186. package/vendor/src/workItemExecutionGate.mjs +78 -0
  187. package/vendor/src/workItemHierarchy.mjs +226 -0
  188. package/vendor/src/workItemProseLint.mjs +133 -0
  189. package/vendor/src/workItemTextRusify.mjs +794 -0
  190. package/vendor/src/workItemTraceEnvelope.mjs +158 -0
  191. package/vendor/src/workItemUiReferences.mjs +272 -0
  192. package/vendor/src/workflowEpicGrouping.mjs +67 -0
  193. package/vendor/src/workflowTreeProjection.mjs +53 -0
  194. 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
+ }