@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,264 @@
1
+ import { parseTraceLinksV1, scanReverseTraceMarkers } from './workGraphRuntime.mjs';
2
+ import { buildWorkItemTraceEnvelope } from './workItemTraceEnvelope.mjs';
3
+
4
+ const compareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
5
+ const DEFAULT_MAX_LINKS = 48;
6
+ const DEFAULT_MAX_REFS = 64;
7
+
8
+ function inferLinkageRefKind(ref, sourceLabel = '') {
9
+ const value = String(ref ?? '').trim();
10
+ if (value === '') {
11
+ return 'file';
12
+ }
13
+
14
+ if (sourceLabel === 'trace.source_step' || sourceLabel === 'work.source_step' || /\.bvc$/u.test(value)) {
15
+ return 'step';
16
+ }
17
+
18
+ if (value.startsWith('work:')) {
19
+ return 'work';
20
+ }
21
+
22
+ return 'file';
23
+ }
24
+
25
+ function pushLinkageRef(refs, seen, entry) {
26
+ const ref = String(entry.ref ?? '').trim();
27
+ if (ref === '') {
28
+ return;
29
+ }
30
+
31
+ const key = `${entry.kind}\0${ref}\0${entry.source ?? ''}`;
32
+ if (seen.has(key)) {
33
+ return;
34
+ }
35
+
36
+ seen.add(key);
37
+ refs.push({
38
+ kind: entry.kind,
39
+ ref,
40
+ label: entry.label ?? ref,
41
+ source: entry.source ?? '',
42
+ relation: entry.relation ?? '',
43
+ });
44
+ }
45
+
46
+ export function buildWorkItemLinkageDrilldown(workId, items, options = {}) {
47
+ if (!Array.isArray(items)) {
48
+ throw new TypeError('items must be an array');
49
+ }
50
+
51
+ const normalizedWorkId = String(workId ?? '').trim();
52
+ if (normalizedWorkId === '') {
53
+ throw new TypeError('workId is required');
54
+ }
55
+
56
+ const itemById = new Map(items.map((item) => [item.id, item]));
57
+ const task = itemById.get(normalizedWorkId);
58
+ if (task === undefined) {
59
+ throw new Error(`unknown task id: ${normalizedWorkId}`);
60
+ }
61
+
62
+ const maxLinks = Number.isInteger(options.maxLinks) && options.maxLinks > 0
63
+ ? options.maxLinks
64
+ : DEFAULT_MAX_LINKS;
65
+ const maxRefs = Number.isInteger(options.maxRefs) && options.maxRefs > 0
66
+ ? options.maxRefs
67
+ : DEFAULT_MAX_REFS;
68
+
69
+ const linkage = options.linkage ?? buildUnifiedLinkageProjectionV1(items, options);
70
+ const envelope = buildWorkItemTraceEnvelope(task);
71
+ const links = linkage.links.filter((link) =>
72
+ link.sourceWorkId === normalizedWorkId
73
+ || (link.from.kind === 'work' && link.from.id === normalizedWorkId)
74
+ || (link.to.kind === 'work' && link.to.id === normalizedWorkId),
75
+ );
76
+
77
+ const refs = [];
78
+ const seen = new Set();
79
+
80
+ for (const path of envelope.targetFiles) {
81
+ pushLinkageRef(refs, seen, {
82
+ kind: 'file',
83
+ ref: path,
84
+ label: path,
85
+ source: 'work.target_files',
86
+ relation: 'targets',
87
+ });
88
+ }
89
+
90
+ if (envelope.sourceStep !== '') {
91
+ pushLinkageRef(refs, seen, {
92
+ kind: 'step',
93
+ ref: envelope.sourceStep,
94
+ label: envelope.sourceStep,
95
+ source: 'trace.source_step',
96
+ relation: 'references',
97
+ });
98
+ }
99
+
100
+ for (const [labelKey, values] of Object.entries(envelope.traceRefs)) {
101
+ for (const value of values) {
102
+ pushLinkageRef(refs, seen, {
103
+ kind: inferLinkageRefKind(value, labelKey),
104
+ ref: value,
105
+ label: value,
106
+ source: labelKey,
107
+ relation: 'references',
108
+ });
109
+ }
110
+ }
111
+
112
+ for (const link of links) {
113
+ if (link.to.kind === 'work' && link.to.id !== normalizedWorkId && itemById.has(link.to.id)) {
114
+ const related = itemById.get(link.to.id);
115
+ pushLinkageRef(refs, seen, {
116
+ kind: 'work',
117
+ ref: link.to.id,
118
+ label: related?.title ? `${related.title} (${link.to.id})` : link.to.id,
119
+ source: link.sourceLabel ?? link.relation,
120
+ relation: link.relation,
121
+ });
122
+ }
123
+
124
+ if (link.to.kind === 'file') {
125
+ const fileRef = link.to.locator?.path ?? link.to.id;
126
+ pushLinkageRef(refs, seen, {
127
+ kind: 'file',
128
+ ref: fileRef,
129
+ label: fileRef,
130
+ source: link.sourceLabel ?? link.relation,
131
+ relation: link.relation,
132
+ });
133
+ }
134
+
135
+ if (link.from.kind === 'work' && link.from.id !== normalizedWorkId && itemById.has(link.from.id)) {
136
+ const related = itemById.get(link.from.id);
137
+ pushLinkageRef(refs, seen, {
138
+ kind: 'work',
139
+ ref: link.from.id,
140
+ label: related?.title ? `${related.title} (${link.from.id})` : link.from.id,
141
+ source: link.sourceLabel ?? link.relation,
142
+ relation: link.relation,
143
+ });
144
+ }
145
+ }
146
+
147
+ for (const marker of linkage.markers ?? []) {
148
+ if (marker.endpoint?.kind === 'work' && marker.endpoint.id === normalizedWorkId) {
149
+ pushLinkageRef(refs, seen, {
150
+ kind: 'file',
151
+ ref: `${marker.sourcePath}:${marker.line}`,
152
+ label: `${marker.sourcePath}:${marker.line}`,
153
+ source: 'reverse_marker',
154
+ relation: 'references',
155
+ });
156
+ }
157
+ }
158
+
159
+ const sortedRefs = refs
160
+ .sort((left, right) => compareText(`${left.kind}\0${left.ref}`, `${right.kind}\0${right.ref}`))
161
+ .slice(0, maxRefs);
162
+ const sortedLinks = links
163
+ .sort((left, right) => compareText(left.id, right.id))
164
+ .slice(0, maxLinks);
165
+
166
+ return {
167
+ schema: 'workgraph.work-item-linkage-drilldown.v1',
168
+ workId: normalizedWorkId,
169
+ envelope,
170
+ linkCount: sortedLinks.length,
171
+ refCount: sortedRefs.length,
172
+ truncated: links.length > sortedLinks.length || refs.length > sortedRefs.length,
173
+ links: sortedLinks,
174
+ refs: sortedRefs,
175
+ };
176
+ }
177
+
178
+ export function buildUnifiedLinkageProjectionV1(items, options = {}) {
179
+ if (!Array.isArray(items)) {
180
+ throw new TypeError('items must be an array');
181
+ }
182
+
183
+ const traceLinks = (options.traceLinks ?? parseTraceLinksV1(items)).map((link) => ({
184
+ id: link.id,
185
+ from: link.from,
186
+ to: link.to,
187
+ relation: link.relation,
188
+ evidence: [...(link.evidence ?? [])].sort(compareText),
189
+ status: link.status,
190
+ source: link.source,
191
+ confidence: link.confidence,
192
+ sourceWorkId: link.sourceWorkId,
193
+ sourceLabel: link.sourceLabel,
194
+ sourceRef: link.sourceRef,
195
+ }));
196
+
197
+ const reverseMarkers = (options.reverseMarkers ?? scanReverseTraceMarkers(options.fileContentsByPath ?? {})).map((marker) => ({
198
+ ref: marker.ref,
199
+ endpoint: marker.endpoint,
200
+ sourcePath: marker.sourcePath,
201
+ line: marker.line,
202
+ }));
203
+
204
+ const planningEdges = items.flatMap((item) => {
205
+ const targetEdges = (item.targetFiles ?? []).map((path) => ({
206
+ id: `plan:${item.id}:targets:${path}`,
207
+ from: { kind: 'work', id: item.id },
208
+ to: { kind: 'file', id: path, locator: { path } },
209
+ relation: 'targets',
210
+ evidence: [],
211
+ status: 'pending',
212
+ source: 'derived_projection',
213
+ confidence: 'low',
214
+ sourceWorkId: item.id,
215
+ sourceLabel: 'work.target_files',
216
+ sourceRef: path,
217
+ }));
218
+
219
+ const dependencyEdges = (item.dependsOn ?? []).map((dependencyId) => ({
220
+ id: `plan:${item.id}:depends_on:${dependencyId}`,
221
+ from: { kind: 'work', id: item.id },
222
+ to: { kind: 'work', id: dependencyId },
223
+ relation: 'depends_on',
224
+ evidence: [],
225
+ status: 'linked',
226
+ source: 'derived_projection',
227
+ confidence: 'high',
228
+ sourceWorkId: item.id,
229
+ sourceLabel: 'work.depends_on',
230
+ sourceRef: dependencyId,
231
+ }));
232
+
233
+ const parentId = String(item.parentId ?? item.labels?.['work.parent_id'] ?? '').trim();
234
+ const parentEdges = parentId === '' ? [] : [{
235
+ id: `plan:${item.id}:parent_of:${parentId}`,
236
+ from: { kind: 'work', id: parentId },
237
+ to: { kind: 'work', id: item.id },
238
+ relation: 'parent_of',
239
+ evidence: [],
240
+ status: 'linked',
241
+ source: 'derived_projection',
242
+ confidence: 'high',
243
+ sourceWorkId: item.id,
244
+ sourceLabel: 'work.parent_id',
245
+ sourceRef: parentId,
246
+ }];
247
+
248
+ return [...targetEdges, ...dependencyEdges, ...parentEdges];
249
+ });
250
+
251
+ const links = [...traceLinks, ...planningEdges].sort((left, right) => compareText(left.id, right.id));
252
+ const markers = [...reverseMarkers].sort((left, right) => compareText(`${left.sourcePath}\0${left.line}\0${left.ref}`, `${right.sourcePath}\0${right.line}\0${right.ref}`));
253
+
254
+ return {
255
+ schema: 'unified-linkage.projection.v1',
256
+ generatedFrom: options.generatedFrom ?? 'workgraph.snapshot.v1',
257
+ generatedAt: options.generatedAt ?? null,
258
+ linkCount: links.length,
259
+ markerCount: markers.length,
260
+ links,
261
+ markers,
262
+ consumers: ['architectureSnapshot', 'verificationLoop', 'pvrgTaskScope', 'backlogUiDerivedGraph'],
263
+ };
264
+ }
@@ -0,0 +1,206 @@
1
+ import { buildCodegenVerificationGate } from './codegenEvidence.mjs';
2
+
3
+ /** @typedef {'deterministic' | 'optional-env' | 'optional-llm'} VerificationTier */
4
+ /** @typedef {'passed' | 'pending' | 'blocked' | 'not_run' | 'failed'} VerificationStatus */
5
+
6
+ /** @type {Array<{ id: string, tier: VerificationTier, title: string, command: string, gateTaskIds: string[], evidenceHints: string[] }>} */
7
+ export const VERIFICATION_MATRIX = [
8
+ {
9
+ id: 'formatter-roundtrip',
10
+ tier: 'deterministic',
11
+ title: 'Roundtrip форматтера Step Atom',
12
+ command: 'npm run test:deterministic',
13
+ gateTaskIds: ['implement-step-atom-formatter'],
14
+ evidenceHints: ['formatter', 'roundtrip', 'npm test'],
15
+ },
16
+ {
17
+ id: 'workgraph-runtime',
18
+ tier: 'deterministic',
19
+ title: 'Политика runtime Work Graph',
20
+ command: 'npm run test:deterministic',
21
+ gateTaskIds: ['implement-workgraph-minimal-runtime'],
22
+ evidenceHints: ['claimNext', 'transitionStatus', 'npm test'],
23
+ },
24
+ {
25
+ id: 'trace-links-v1',
26
+ tier: 'deterministic',
27
+ title: 'Валидатор Trace Links v1',
28
+ command: 'npm run test:deterministic',
29
+ gateTaskIds: ['implement-step-code-trace-link-validator'],
30
+ evidenceHints: ['Trace Links', 'validateTraceLinksV1', 'npm test'],
31
+ },
32
+ {
33
+ id: 'ui-server-smoke',
34
+ tier: 'deterministic',
35
+ title: 'Smoke UI-сервера backlog',
36
+ command: 'npm run test:deterministic',
37
+ gateTaskIds: ['implement-workgraph-minimal-runtime', 'implement-schematic-visualization-view-mvp'],
38
+ evidenceHints: ['workGraphBacklogUiServer', 'renderBacklogHtml', 'npm test'],
39
+ },
40
+ {
41
+ id: 'golden-path-runtime',
42
+ tier: 'deterministic',
43
+ title: 'Сценарий golden path runtime',
44
+ command: 'npm run test:deterministic',
45
+ gateTaskIds: ['golden-path-test'],
46
+ evidenceHints: ['goldenPath', 'runDeterministicGoldenPath', 'npm test'],
47
+ },
48
+ {
49
+ id: 'onebase-gross-profit-static',
50
+ tier: 'deterministic',
51
+ title: 'OneBase: статическая проверка артефакта валовой прибыли',
52
+ command: 'npm run test:deterministic',
53
+ gateTaskIds: ['onebase-implement-gross-profit-warehouse-dimension'],
54
+ evidenceHints: ['verifyOnebaseGrossProfitWarehouseArtifacts', 'валовая_прибыль', 'ДвВП.Склад'],
55
+ },
56
+ {
57
+ id: 'onebase-go-test',
58
+ tier: 'optional-env',
59
+ title: 'OneBase golden path (go test)',
60
+ command: 'npm run test:optional:onebase',
61
+ gateTaskIds: ['onebase-verification-command', 'onebase-implement-gross-profit-warehouse-dimension'],
62
+ evidenceHints: ['go test', 'go version', 'CommandNotFoundException', 'blocked evidence'],
63
+ },
64
+ {
65
+ id: 'onebase-config-check',
66
+ tier: 'optional-env',
67
+ title: 'OneBase config check (CLI)',
68
+ command: 'npm run test:optional:onebase-check',
69
+ gateTaskIds: ['wire-onebase-cli-tools-parity', 'implement-onebase-check-verification-gate'],
70
+ evidenceHints: ['onebase check', 'config-check', 'cli_command_unavailable', 'skipped'],
71
+ },
72
+ {
73
+ id: 'lowcode-arch-scaffold',
74
+ tier: 'optional-env',
75
+ title: 'Low-code: verify scaffold arch-rules',
76
+ command: 'npm run verify:lowcode',
77
+ gateTaskIds: ['implement-lowcode-scaffold-cli-mvp'],
78
+ evidenceHints: ['scaffold:arch-rules', 'arch-rules.bvc', 'manifest.json'],
79
+ },
80
+ {
81
+ id: 'onebase-llm-scenario',
82
+ tier: 'optional-llm',
83
+ title: 'OneBase: сценарий с реальной LLM',
84
+ command: 'вручную / будущий optional eval',
85
+ gateTaskIds: ['onebase-posting-rule-golden-path'],
86
+ evidenceHints: ['real LLM', 'tool-call', 'optional'],
87
+ },
88
+ ];
89
+
90
+ /**
91
+ * @param {{ items?: Array<{ id: string, status: string, evidence?: string[], blocker?: string, title?: string, nextAction?: string }> }} workGraphSnapshot
92
+ */
93
+ export function buildVerificationSummary(workGraphSnapshot, options = {}) {
94
+ const items = Array.isArray(options.items) ? options.items : (workGraphSnapshot?.items ?? []);
95
+ const itemById = new Map(items.map((item) => [item.id, item]));
96
+
97
+ const matrix = VERIFICATION_MATRIX.map((row) => ({
98
+ ...row,
99
+ status: deriveMatrixRowStatus(row, itemById),
100
+ gateTasks: row.gateTaskIds.map((taskId) => summarizeGateTask(itemById.get(taskId))),
101
+ }));
102
+
103
+ const tierCounts = summarizeTierCounts(matrix);
104
+ const onebaseGate = matrix.find((row) => row.id === 'onebase-go-test') ?? null;
105
+ const codegenGate = buildCodegenVerificationGate(items);
106
+
107
+ return {
108
+ schema: 'verification.summary.v1',
109
+ tierCounts,
110
+ matrix,
111
+ onebaseGate: onebaseGate
112
+ ? {
113
+ status: onebaseGate.status,
114
+ preflightCommand: 'go version',
115
+ primaryCommand: 'go test ./...',
116
+ cwd: '../onebase',
117
+ blockedTaskId: items.find((item) => item.id === 'onebase-implement-gross-profit-warehouse-dimension' && item.status === 'blocked')?.id ?? null,
118
+ }
119
+ : null,
120
+ codegenGate,
121
+ policy: {
122
+ deterministicCommand: 'npm run test:deterministic',
123
+ optionalOnebaseCommand: 'npm run test:optional:onebase',
124
+ optionalOnebaseCheckCommand: 'npm run test:optional:onebase-check',
125
+ protocolPath: 'protocols/rebuild-verification-loop.bvc',
126
+ },
127
+ };
128
+ }
129
+
130
+ /**
131
+ * @param {typeof VERIFICATION_MATRIX[number]} row
132
+ * @param {Map<string, object>} itemById
133
+ * @returns {VerificationStatus}
134
+ */
135
+ function deriveMatrixRowStatus(row, itemById) {
136
+ const gateTasks = row.gateTaskIds.map((id) => itemById.get(id)).filter(Boolean);
137
+ if (gateTasks.length === 0) return 'pending';
138
+
139
+ if (row.tier === 'optional-llm') {
140
+ return gateTasks.some((task) => task.status === 'done' && hasEvidenceMatch(task, row.evidenceHints))
141
+ ? 'passed'
142
+ : 'not_run';
143
+ }
144
+
145
+ if (row.tier === 'optional-env') {
146
+ const blocked = gateTasks.find((task) => task.status === 'blocked');
147
+ if (blocked && mentionsAny(blocked, ['go', 'PATH', 'CommandNotFound', 'blocked evidence'])) {
148
+ return 'blocked';
149
+ }
150
+ if (gateTasks.some((task) => task.status === 'done' && hasEvidenceMatch(task, row.evidenceHints))) {
151
+ return 'passed';
152
+ }
153
+ return gateTasks.some((task) => task.status === 'done') ? 'pending' : 'not_run';
154
+ }
155
+
156
+ const allDone = row.gateTaskIds.every((taskId) => {
157
+ const task = itemById.get(taskId);
158
+ return task && ['done', 'verified'].includes(task.status);
159
+ });
160
+ if (!allDone) return 'pending';
161
+
162
+ const evidenceOk = gateTasks.some((task) => hasEvidenceMatch(task, row.evidenceHints) || (task.evidence || []).length > 0);
163
+ return evidenceOk ? 'passed' : 'pending';
164
+ }
165
+
166
+ /** @param {object | undefined} task */
167
+ function summarizeGateTask(task) {
168
+ if (!task) {
169
+ return { id: null, status: 'missing', evidenceCount: 0 };
170
+ }
171
+ return {
172
+ id: task.id,
173
+ status: task.status,
174
+ title: task.title,
175
+ evidenceCount: (task.evidence || []).length,
176
+ };
177
+ }
178
+
179
+ /** @param {object} task @param {string[]} hints */
180
+ function hasEvidenceMatch(task, hints) {
181
+ const haystack = [(task.evidence || []).join('\n'), task.blocker, task.nextAction].filter(Boolean).join('\n').toLowerCase();
182
+ return hints.some((hint) => haystack.includes(hint.toLowerCase()));
183
+ }
184
+
185
+ /** @param {object} task @param {string[]} needles */
186
+ function mentionsAny(task, needles) {
187
+ const haystack = [(task.evidence || []).join('\n'), task.blocker, task.nextAction, task.title].filter(Boolean).join('\n').toLowerCase();
188
+ return needles.some((needle) => haystack.includes(needle.toLowerCase()));
189
+ }
190
+
191
+ /** @param {Array<{ tier: VerificationTier, status: VerificationStatus }>} matrix */
192
+ function summarizeTierCounts(matrix) {
193
+ const countTier = (tier) => ({
194
+ passed: matrix.filter((row) => row.tier === tier && row.status === 'passed').length,
195
+ pending: matrix.filter((row) => row.tier === tier && row.status === 'pending').length,
196
+ blocked: matrix.filter((row) => row.tier === tier && row.status === 'blocked').length,
197
+ notRun: matrix.filter((row) => row.tier === tier && row.status === 'not_run').length,
198
+ total: matrix.filter((row) => row.tier === tier).length,
199
+ });
200
+
201
+ return {
202
+ deterministic: countTier('deterministic'),
203
+ optionalEnv: countTier('optional-env'),
204
+ optionalLlm: countTier('optional-llm'),
205
+ };
206
+ }
@@ -0,0 +1,234 @@
1
+ import { readFile, rename, writeFile } from 'node:fs/promises';
2
+
3
+ const STEP_ATOM_PATTERN = /^#([^\n<]+)<\[\n([\s\S]*?)\n\]>/gmu;
4
+
5
+ function escapeRegex(value) {
6
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
+ }
8
+
9
+ export function findWorkItemAtomSpan(backlogText, workId) {
10
+ if (typeof backlogText !== 'string') {
11
+ throw new TypeError('backlogText must be a string');
12
+ }
13
+
14
+ const normalizedWorkId = String(workId ?? '').trim();
15
+ if (normalizedWorkId === '') {
16
+ throw new TypeError('workId must be a non-empty string');
17
+ }
18
+
19
+ for (const match of backlogText.matchAll(STEP_ATOM_PATTERN)) {
20
+ const body = match[2];
21
+ const idMatch = body.match(/^\s*work\.id:\s*(.+)$/m);
22
+ if (idMatch?.[1]?.trim() === normalizedWorkId) {
23
+ return {
24
+ start: match.index,
25
+ end: match.index + match[0].length,
26
+ atomName: match[1],
27
+ body,
28
+ fullMatch: match[0],
29
+ };
30
+ }
31
+ }
32
+
33
+ return null;
34
+ }
35
+
36
+ function upsertLabelLine(body, key, value) {
37
+ const pattern = new RegExp(`^(\\s*)${escapeRegex(key)}:\\s*.+$`, 'm');
38
+ const line = ` ${key}: ${value}`;
39
+
40
+ if (pattern.test(body)) {
41
+ return body.replace(pattern, line);
42
+ }
43
+
44
+ const labelsMatch = body.match(/^(\s*)Метки:\s*$/m);
45
+ if (labelsMatch) {
46
+ const insertAt = labelsMatch.index + labelsMatch[0].length;
47
+ return `${body.slice(0, insertAt)}\n${line}${body.slice(insertAt)}`;
48
+ }
49
+
50
+ return `${body.trimEnd()}\n\nМетки:\n${line}`;
51
+ }
52
+
53
+ function removeLabelLine(body, key) {
54
+ const pattern = new RegExp(`^\\s*${escapeRegex(key)}:\\s*.+\\n?`, 'm');
55
+ return body.replace(pattern, '');
56
+ }
57
+
58
+ function parseEvidenceLinesFromBody(body) {
59
+ const lines = [];
60
+ let inEvidence = false;
61
+
62
+ for (const rawLine of body.split(/\r?\n/u)) {
63
+ const trimmed = rawLine.trim();
64
+
65
+ if (trimmed === 'Свидетельства:') {
66
+ inEvidence = true;
67
+ continue;
68
+ }
69
+
70
+ if (!inEvidence) {
71
+ continue;
72
+ }
73
+
74
+ if (trimmed === '') {
75
+ continue;
76
+ }
77
+
78
+ if (/^[A-Za-zА-Яа-яЁё_]+:$/u.test(trimmed) && trimmed !== 'критерии_готовности:') {
79
+ inEvidence = false;
80
+ continue;
81
+ }
82
+
83
+ if (/^-\s*/u.test(trimmed) || rawLine.startsWith(' ')) {
84
+ lines.push(trimmed.replace(/^-\s*/u, '').trim());
85
+ continue;
86
+ }
87
+
88
+ inEvidence = false;
89
+ }
90
+
91
+ return lines;
92
+ }
93
+
94
+ function appendEvidenceLines(body, newLines) {
95
+ const existing = new Set(parseEvidenceLinesFromBody(body));
96
+ const toAdd = newLines
97
+ .map((line) => String(line ?? '').trim())
98
+ .filter((line) => line !== '' && !existing.has(line));
99
+
100
+ if (toAdd.length === 0) {
101
+ return body;
102
+ }
103
+
104
+ const formatted = toAdd.map((line) => ` - ${line}`);
105
+ const evidenceHeader = body.match(/^(\s*)Свидетельства:\s*$/m);
106
+
107
+ if (!evidenceHeader) {
108
+ return `${body.trimEnd()}\n\nСвидетельства:\n${formatted.join('\n')}`;
109
+ }
110
+
111
+ const bodyLines = body.split(/\r?\n/u);
112
+ let evidenceStartIdx = -1;
113
+ let insertIdx = -1;
114
+
115
+ for (let index = 0; index < bodyLines.length; index += 1) {
116
+ if (bodyLines[index].trim() === 'Свидетельства:') {
117
+ evidenceStartIdx = index;
118
+ insertIdx = index + 1;
119
+ continue;
120
+ }
121
+
122
+ if (evidenceStartIdx < 0 || index <= evidenceStartIdx) {
123
+ continue;
124
+ }
125
+
126
+ const trimmed = bodyLines[index].trim();
127
+ if (trimmed === '') {
128
+ continue;
129
+ }
130
+
131
+ if (/^[A-Za-zА-Яа-яЁё_]+:$/u.test(trimmed) && trimmed !== 'критерии_готовности:') {
132
+ break;
133
+ }
134
+
135
+ if (/^-\s*/u.test(trimmed) || bodyLines[index].startsWith(' ')) {
136
+ insertIdx = index + 1;
137
+ continue;
138
+ }
139
+
140
+ break;
141
+ }
142
+
143
+ bodyLines.splice(insertIdx, 0, ...formatted);
144
+ return bodyLines.join('\n');
145
+ }
146
+
147
+ export function patchWorkItemAtomBody(body, item) {
148
+ if (!item || typeof item !== 'object') {
149
+ throw new TypeError('item must be an object');
150
+ }
151
+
152
+ let next = upsertLabelLine(body, 'work.status', item.status);
153
+
154
+ if (item.status === 'blocked' && item.blocker) {
155
+ next = upsertLabelLine(next, 'work.blocker', item.blocker);
156
+ } else {
157
+ next = removeLabelLine(next, 'work.blocker');
158
+ next = removeLabelLine(next, 'work.blocked_reason');
159
+ }
160
+
161
+ if (Array.isArray(item.evidence) && item.evidence.length > 0) {
162
+ next = appendEvidenceLines(next, item.evidence);
163
+ }
164
+
165
+ const activeClaimStatuses = new Set(['claimed', 'doing', 'in_progress']);
166
+ if (activeClaimStatuses.has(item.status)) {
167
+ const claimedBy = String(item.labels?.['work.claimed_by'] ?? '').trim();
168
+ const leaseUntil = String(item.labels?.['work.claim_lease_until'] ?? '').trim();
169
+ if (claimedBy !== '') {
170
+ next = upsertLabelLine(next, 'work.claimed_by', claimedBy);
171
+ }
172
+ if (leaseUntil !== '') {
173
+ next = upsertLabelLine(next, 'work.claim_lease_until', leaseUntil);
174
+ }
175
+ } else {
176
+ next = removeLabelLine(next, 'work.claimed_by');
177
+ next = removeLabelLine(next, 'work.claim_lease_until');
178
+ }
179
+
180
+ return next;
181
+ }
182
+
183
+ export function patchWorkItemInBacklogText(backlogText, item) {
184
+ const span = findWorkItemAtomSpan(backlogText, item.id);
185
+ if (!span) {
186
+ throw new Error(`work item atom not found: ${item.id}`);
187
+ }
188
+
189
+ const patchedBody = patchWorkItemAtomBody(span.body, item);
190
+ const patchedAtom = `#${span.atomName}<[\n${patchedBody}\n]>`;
191
+ return `${backlogText.slice(0, span.start)}${patchedAtom}${backlogText.slice(span.end)}`;
192
+ }
193
+
194
+ export async function writeBacklogTextAtomically(backlogPath, backlogText) {
195
+ const tempPath = `${backlogPath}.tmp`;
196
+ await writeFile(tempPath, backlogText, 'utf8');
197
+ await rename(tempPath, backlogPath);
198
+ }
199
+
200
+ export function appendWorkItemAtomToBacklogText(backlogText, atomText) {
201
+ if (typeof backlogText !== 'string') {
202
+ throw new TypeError('backlogText must be a string');
203
+ }
204
+
205
+ const atom = String(atomText ?? '').trim();
206
+ if (atom === '') {
207
+ throw new TypeError('atomText must be a non-empty string');
208
+ }
209
+
210
+ const trimmedBacklog = backlogText.trimEnd();
211
+ return `${trimmedBacklog}\n\n${atom}\n`;
212
+ }
213
+
214
+ export async function persistWorkItemUpdateToBacklogFile(options = {}) {
215
+ const backlogPath = options.backlogPath;
216
+ if (!backlogPath) {
217
+ throw new TypeError('backlogPath is required');
218
+ }
219
+
220
+ const item = options.item;
221
+ if (!item?.id) {
222
+ throw new TypeError('item.id is required');
223
+ }
224
+
225
+ const sourceText = options.backlogText ?? await readFile(backlogPath, 'utf8');
226
+ const newText = patchWorkItemInBacklogText(sourceText, item);
227
+ await writeBacklogTextAtomically(backlogPath, newText);
228
+
229
+ return {
230
+ path: backlogPath,
231
+ workId: item.id,
232
+ status: item.status,
233
+ };
234
+ }