@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,158 @@
1
+ const CODE_FILE_PATTERN = /\.(?:js|mjs|cjs|ts|tsx|jsx|step|json|yaml|yml|os)$/iu;
2
+ const TRACE_REF_LABELS = ['trace.code_refs', 'trace.artifact_refs', 'trace.evidence_refs', 'trace.links'];
3
+ const DONE_TRACE_STATUSES = new Set(['linked', 'verified']);
4
+ const VERIFY_TRACE_STATUSES = new Set(['linked', 'verified', 'pending', 'needs_review']);
5
+
6
+ const compareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
7
+
8
+ function parseList(value) {
9
+ if (value === undefined || value === null || String(value).trim() === '') {
10
+ return [];
11
+ }
12
+
13
+ return String(value)
14
+ .split(/\s*,\s*/u)
15
+ .map((item) => item.trim())
16
+ .filter(Boolean);
17
+ }
18
+
19
+ export function isCodeFacingWorkItem(item) {
20
+ if (item === undefined || item === null) {
21
+ throw new TypeError('item is required');
22
+ }
23
+
24
+ const sourceStep = item.labels?.['trace.source_step'] ?? item.labels?.['work.source_step'] ?? '';
25
+ if (String(sourceStep).trim() !== '') {
26
+ return true;
27
+ }
28
+
29
+ if (TRACE_REF_LABELS.some((labelKey) => parseList(item.labels?.[labelKey]).length > 0)) {
30
+ return true;
31
+ }
32
+
33
+ return (item.targetFiles ?? []).some((path) => CODE_FILE_PATTERN.test(String(path)));
34
+ }
35
+
36
+ export function buildWorkItemTraceEnvelope(item) {
37
+ if (item === undefined || item === null) {
38
+ throw new TypeError('item is required');
39
+ }
40
+
41
+ const traceRefs = Object.fromEntries(
42
+ TRACE_REF_LABELS.map((labelKey) => [labelKey, parseList(item.labels?.[labelKey]).sort(compareText)]),
43
+ );
44
+ const sourceStep = String(item.labels?.['trace.source_step'] ?? item.labels?.['work.source_step'] ?? '').trim();
45
+
46
+ return {
47
+ schema: 'workitem.trace-envelope.v1',
48
+ workId: item.id,
49
+ codeFacing: isCodeFacingWorkItem(item),
50
+ sourceStep,
51
+ targetFiles: [...(item.targetFiles ?? [])].sort(compareText),
52
+ traceRefs,
53
+ traceStatus: String(item.traceStatus ?? item.labels?.['trace.status'] ?? 'pending').trim() || 'pending',
54
+ evidence: [...(item.evidence ?? [])].sort(compareText),
55
+ intentPath: String(item.labels?.['intent.path'] ?? '').trim(),
56
+ };
57
+ }
58
+
59
+ export function evaluateTraceVerifyGate(item, options = {}) {
60
+ const envelope = buildWorkItemTraceEnvelope(item);
61
+ const targetStatus = options.targetStatus ?? item.status;
62
+ const diagnostics = [];
63
+
64
+ if (!envelope.codeFacing) {
65
+ return { ok: true, envelope, diagnostics };
66
+ }
67
+
68
+ const hasTraceRefs = TRACE_REF_LABELS.some((labelKey) => envelope.traceRefs[labelKey].length > 0);
69
+ const hasSourceStep = envelope.sourceStep !== '';
70
+
71
+ if (!hasTraceRefs && !hasSourceStep) {
72
+ const onlyTargetFiles = envelope.targetFiles.length > 0 && targetStatus === 'done';
73
+ if (!onlyTargetFiles) {
74
+ diagnostics.push({
75
+ severity: envelope.targetFiles.length > 0 ? 'warning' : 'error',
76
+ code: 'trace.envelope.missing_refs',
77
+ message: `WorkItem ${envelope.workId} is code-facing but has no trace refs or source step.`,
78
+ actionable: 'Add trace.code_refs, trace.artifact_refs or trace.source_step.',
79
+ });
80
+ }
81
+ }
82
+
83
+ if (envelope.traceStatus === 'missing' || envelope.traceStatus === 'orphaned') {
84
+ diagnostics.push({
85
+ severity: 'error',
86
+ code: 'trace.envelope.invalid_status',
87
+ message: `WorkItem ${envelope.workId} has trace.status=${envelope.traceStatus}.`,
88
+ actionable: 'Fix trace links or downgrade task scope before verify.',
89
+ });
90
+ }
91
+
92
+ if (targetStatus === 'verify' && envelope.traceStatus === 'missing') {
93
+ diagnostics.push({
94
+ severity: 'error',
95
+ code: 'trace.envelope.verify_blocked',
96
+ message: `WorkItem ${envelope.workId} cannot enter verify with trace.status=missing.`,
97
+ actionable: 'Author trace refs and set trace.status to pending or linked.',
98
+ });
99
+ }
100
+
101
+ if (targetStatus === 'done') {
102
+ if (envelope.evidence.length === 0) {
103
+ diagnostics.push({
104
+ severity: 'error',
105
+ code: 'trace.envelope.done_without_evidence',
106
+ message: `WorkItem ${envelope.workId} cannot be done without evidence.`,
107
+ actionable: 'Add Свидетельства section entries.',
108
+ });
109
+ }
110
+
111
+ if (!DONE_TRACE_STATUSES.has(envelope.traceStatus) && hasTraceRefs) {
112
+ diagnostics.push({
113
+ severity: 'error',
114
+ code: 'trace.envelope.done_without_verified_trace',
115
+ message: `WorkItem ${envelope.workId} has trace refs but trace.status=${envelope.traceStatus}.`,
116
+ actionable: 'Set trace.status to linked or verified after validator pass.',
117
+ });
118
+ }
119
+
120
+ if (envelope.targetFiles.length > 0 && !hasTraceRefs) {
121
+ diagnostics.push({
122
+ severity: 'warning',
123
+ code: 'trace.weak_target_files_only',
124
+ message: `WorkItem ${envelope.workId} has target_files but no Trace Links v1 labels.`,
125
+ actionable: `Add trace.code_refs or trace.artifact_refs for ${envelope.targetFiles[0]}.`,
126
+ });
127
+ }
128
+ }
129
+
130
+ if (targetStatus === 'verify' && hasTraceRefs && !VERIFY_TRACE_STATUSES.has(envelope.traceStatus)) {
131
+ diagnostics.push({
132
+ severity: 'error',
133
+ code: 'trace.envelope.verify_status',
134
+ message: `WorkItem ${envelope.workId} cannot enter verify with trace.status=${envelope.traceStatus}.`,
135
+ actionable: 'Set trace.status to pending, linked or needs_review.',
136
+ });
137
+ }
138
+
139
+ const ok = diagnostics.every((diagnostic) => diagnostic.severity !== 'error');
140
+ return { ok, envelope, diagnostics: diagnostics.sort((left, right) => compareText(`${left.severity}\0${left.code}`, `${right.severity}\0${right.code}`)) };
141
+ }
142
+
143
+ export function buildTraceEnvelopeSnapshot(items) {
144
+ if (!Array.isArray(items)) {
145
+ throw new TypeError('items must be an array');
146
+ }
147
+
148
+ const envelopes = items
149
+ .map((item) => buildWorkItemTraceEnvelope(item))
150
+ .filter((envelope) => envelope.codeFacing)
151
+ .sort((left, right) => compareText(left.workId, right.workId));
152
+
153
+ return {
154
+ schema: 'workitem.trace-envelope.snapshot.v1',
155
+ count: envelopes.length,
156
+ envelopes,
157
+ };
158
+ }
@@ -0,0 +1,272 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
+ import { dirname, join, resolve } from 'node:path';
4
+
5
+ import {
6
+ applyAtomInspectorProposalToBacklogFile,
7
+ importStepAtomDraftForWorkItem,
8
+ } from './atomInspector.mjs';
9
+ import { readWorkItemAtomFromRepo, readWorkItemsFromRepo } from './intentTreeWorkItems.mjs';
10
+ export const UI_REFERENCES_MANIFEST_SCHEMA = 'workitem.ui-references.v1';
11
+ export const DEFAULT_UI_REFERENCES_DIR = 'work/ui-references';
12
+ export const UI_REFERENCE_MAX_BYTES = 5 * 1024 * 1024;
13
+ export const UI_REFERENCE_ALLOWED_EXT = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
14
+
15
+ const compareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
16
+
17
+ export function isUiFacingWorkItem(item) {
18
+ if (!item) {
19
+ return false;
20
+ }
21
+
22
+ if (String(item.labels?.['work.ui_task'] ?? '').trim().toLowerCase() === 'true') {
23
+ return true;
24
+ }
25
+
26
+ const haystack = [
27
+ item.title,
28
+ item.basis,
29
+ item.vector,
30
+ item.goal,
31
+ item.department,
32
+ item.ownerRole,
33
+ ...(item.targetFiles ?? []),
34
+ ].join('\n');
35
+
36
+ if (/dashboard|operator|интерфейс|экран|макет|wireframe|figma|скрин|ui panel|ui task|ui-dashboard|backlog ui|ui server|ui-refs|ui refs/iu.test(haystack)) {
37
+ return true;
38
+ }
39
+
40
+ if (String(item.department ?? '').toLowerCase().includes('ui')) {
41
+ return true;
42
+ }
43
+
44
+ return (item.targetFiles ?? []).some((path) =>
45
+ /BacklogUi|dashboard|\.css|\.html|operator|panel|view/i.test(path),
46
+ );
47
+ }
48
+
49
+ export function resolveUiReferencesDir(cwd, workId, rootDir = DEFAULT_UI_REFERENCES_DIR) {
50
+ const normalizedWorkId = String(workId ?? '').trim();
51
+ if (normalizedWorkId === '' || normalizedWorkId.includes('..') || normalizedWorkId.includes('/')) {
52
+ throw new Error('invalid workId for ui references');
53
+ }
54
+
55
+ return resolve(cwd, rootDir, normalizedWorkId);
56
+ }
57
+
58
+ export function manifestPathForWorkItem(cwd, workId, rootDir = DEFAULT_UI_REFERENCES_DIR) {
59
+ return join(resolveUiReferencesDir(cwd, workId, rootDir), 'manifest.v1.json');
60
+ }
61
+
62
+ export function emptyUiReferencesManifest(workId) {
63
+ return {
64
+ schema: UI_REFERENCES_MANIFEST_SCHEMA,
65
+ workId,
66
+ items: [],
67
+ };
68
+ }
69
+
70
+ export async function readUiReferencesManifest(cwd, workId, rootDir = DEFAULT_UI_REFERENCES_DIR) {
71
+ const path = manifestPathForWorkItem(cwd, workId, rootDir);
72
+
73
+ try {
74
+ const raw = await readFile(path, 'utf8');
75
+ const parsed = JSON.parse(raw);
76
+ if (parsed?.schema !== UI_REFERENCES_MANIFEST_SCHEMA || !Array.isArray(parsed.items)) {
77
+ return emptyUiReferencesManifest(workId);
78
+ }
79
+
80
+ return {
81
+ ...parsed,
82
+ workId,
83
+ items: parsed.items.sort((left, right) => compareText(left.file ?? '', right.file ?? '')),
84
+ };
85
+ } catch (error) {
86
+ if (error && typeof error === 'object' && error.code === 'ENOENT') {
87
+ return emptyUiReferencesManifest(workId);
88
+ }
89
+
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ async function writeUiReferencesManifest(cwd, workId, manifest, rootDir = DEFAULT_UI_REFERENCES_DIR) {
95
+ const path = manifestPathForWorkItem(cwd, workId, rootDir);
96
+ await mkdir(dirname(path), { recursive: true });
97
+ const tempPath = `${path}.tmp`;
98
+ await writeFile(tempPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
99
+ await rename(tempPath, path);
100
+ return path;
101
+ }
102
+
103
+ export function validateUiReferenceUpload({ filename, buffer }) {
104
+ const name = String(filename ?? '').trim();
105
+ if (name === '' || name.includes('..') || name.includes('/') || name.includes('\\')) {
106
+ return { ok: false, error: 'invalid_filename' };
107
+ }
108
+
109
+ const ext = name.slice(name.lastIndexOf('.')).toLowerCase();
110
+ if (!UI_REFERENCE_ALLOWED_EXT.has(ext)) {
111
+ return { ok: false, error: 'unsupported_image_type' };
112
+ }
113
+
114
+ const bytes = buffer instanceof Buffer ? buffer : Buffer.from(buffer ?? []);
115
+ if (bytes.length === 0) {
116
+ return { ok: false, error: 'empty_file' };
117
+ }
118
+
119
+ if (bytes.length > UI_REFERENCE_MAX_BYTES) {
120
+ return { ok: false, error: 'file_too_large' };
121
+ }
122
+
123
+ return { ok: true, filename: name, buffer: bytes, ext };
124
+ }
125
+
126
+ function stableReferenceFileName(originalName, buffer) {
127
+ const ext = originalName.slice(originalName.lastIndexOf('.')).toLowerCase();
128
+ const hash = createHash('sha256').update(buffer).digest('hex').slice(0, 12);
129
+ const stem = originalName.slice(0, originalName.lastIndexOf('.')).replace(/[^a-zA-Z0-9._-]+/gu, '-').slice(0, 40);
130
+ return `${stem || 'ref'}-${hash}${ext}`;
131
+ }
132
+
133
+ export async function syncUiRefsLabelOnWorkItemAtom(workId, manifest, options = {}) {
134
+ const paths = manifest.items.map((entry) =>
135
+ join(DEFAULT_UI_REFERENCES_DIR, workId, entry.file).replace(/\\/g, '/'),
136
+ );
137
+
138
+ const sourceAtom = await readWorkItemAtomFromRepo(workId, options);
139
+ const source = importStepAtomDraftForWorkItem(sourceAtom.atomText, workId);
140
+ const draft = {
141
+ ...source.draft,
142
+ labels: {
143
+ ...(source.draft.labels ?? {}),
144
+ 'work.ui_refs': paths.join(', '),
145
+ 'work.ui_refs.count': String(paths.length),
146
+ },
147
+ };
148
+
149
+ if (paths.length > 0) {
150
+ draft.uiRefs = manifest.items.map((entry) =>
151
+ `${entry.file}${entry.caption ? ` | ${entry.caption}` : ''}`,
152
+ );
153
+ }
154
+
155
+ return applyAtomInspectorProposalToBacklogFile({
156
+ ...options,
157
+ workId,
158
+ draft,
159
+ });
160
+ }
161
+
162
+ export async function attachUiReference(options = {}) {
163
+ const cwd = options.cwd ?? process.cwd();
164
+ const workId = String(options.workId ?? '').trim();
165
+ if (workId === '') {
166
+ return { ok: false, error: 'work_id_required' };
167
+ }
168
+
169
+ const items = await readWorkItemsFromRepo({ ...options, cwd });
170
+ const item = items.find((entry) => entry.id === workId);
171
+ if (!item) {
172
+ return { ok: false, error: 'work_item_not_found', workId };
173
+ }
174
+
175
+ if (!isUiFacingWorkItem(item) && options.force !== true) {
176
+ return { ok: false, error: 'not_ui_facing_task', workId, hint: 'Set work.ui_task: true to allow refs on non-UI tasks' };
177
+ }
178
+
179
+ let buffer = options.buffer;
180
+ if (options.contentBase64 !== undefined) {
181
+ buffer = Buffer.from(String(options.contentBase64), 'base64');
182
+ }
183
+
184
+ const validated = validateUiReferenceUpload({ filename: options.filename, buffer });
185
+ if (!validated.ok) {
186
+ return { ok: false, error: validated.error, workId };
187
+ }
188
+
189
+ const storedName = stableReferenceFileName(validated.filename, validated.buffer);
190
+ const dir = resolveUiReferencesDir(cwd, workId, options.rootDir);
191
+ await mkdir(dir, { recursive: true });
192
+ const filePath = join(dir, storedName);
193
+ await writeFile(filePath, validated.buffer);
194
+
195
+ const manifest = await readUiReferencesManifest(cwd, workId, options.rootDir);
196
+ const entry = {
197
+ file: storedName,
198
+ caption: String(options.caption ?? '').trim(),
199
+ uploadedAt: options.uploadedAt ?? new Date().toISOString(),
200
+ originalName: validated.filename,
201
+ bytes: validated.buffer.length,
202
+ mime: mimeForExt(validated.ext),
203
+ };
204
+
205
+ manifest.items = [...manifest.items.filter((candidate) => candidate.file !== storedName), entry]
206
+ .sort((left, right) => compareText(left.file, right.file));
207
+
208
+ await writeUiReferencesManifest(cwd, workId, manifest, options.rootDir);
209
+ const labelSync = await syncUiRefsLabelOnWorkItemAtom(workId, manifest, { ...options, cwd });
210
+
211
+ return {
212
+ ok: true,
213
+ workId,
214
+ entry,
215
+ relativePath: join(DEFAULT_UI_REFERENCES_DIR, workId, storedName).replace(/\\/g, '/'),
216
+ manifest,
217
+ labelSyncOk: labelSync.ok === true,
218
+ };
219
+ }
220
+
221
+ export async function listUiReferences(options = {}) {
222
+ const cwd = options.cwd ?? process.cwd();
223
+ const workId = String(options.workId ?? '').trim();
224
+ if (workId === '') {
225
+ return { ok: false, error: 'work_id_required' };
226
+ }
227
+
228
+ const items = await readWorkItemsFromRepo({ ...options, cwd });
229
+ const item = items.find((entry) => entry.id === workId) ?? null;
230
+ const manifest = await readUiReferencesManifest(cwd, workId, options.rootDir);
231
+
232
+ return {
233
+ ok: true,
234
+ schema: UI_REFERENCES_MANIFEST_SCHEMA,
235
+ workId,
236
+ uiFacing: item ? isUiFacingWorkItem(item) : false,
237
+ items: manifest.items.map((entry) => ({
238
+ ...entry,
239
+ url: `/api/work-item/ui-refs/file?workId=${encodeURIComponent(workId)}&file=${encodeURIComponent(entry.file)}`,
240
+ })),
241
+ };
242
+ }
243
+
244
+ export function resolveUiReferenceFilePath(cwd, workId, fileName, rootDir = DEFAULT_UI_REFERENCES_DIR) {
245
+ const normalizedFile = String(fileName ?? '').trim();
246
+ if (normalizedFile === '' || normalizedFile.includes('..') || normalizedFile.includes('/') || normalizedFile.includes('\\')) {
247
+ throw new Error('invalid ui reference file name');
248
+ }
249
+
250
+ return join(resolveUiReferencesDir(cwd, workId, rootDir), normalizedFile);
251
+ }
252
+
253
+ function mimeForExt(ext) {
254
+ switch (ext) {
255
+ case '.png':
256
+ return 'image/png';
257
+ case '.jpg':
258
+ case '.jpeg':
259
+ return 'image/jpeg';
260
+ case '.webp':
261
+ return 'image/webp';
262
+ case '.gif':
263
+ return 'image/gif';
264
+ default:
265
+ return 'application/octet-stream';
266
+ }
267
+ }
268
+
269
+ export function mimeTypeForUiReferenceFileName(fileName) {
270
+ const ext = String(fileName ?? '').slice(String(fileName).lastIndexOf('.')).toLowerCase();
271
+ return mimeForExt(ext);
272
+ }
@@ -0,0 +1,67 @@
1
+ const workflowEpicCompareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
2
+ const WORKFLOW_EPIC_DONE_STATUSES = new Set(['done', 'verified']);
3
+
4
+ /**
5
+ * @param {Array<{ id: string, title?: string, status?: string, parentId?: string, itemKind?: string }>} items
6
+ */
7
+ export function buildWorkflowEpicGroups(items) {
8
+ if (!Array.isArray(items)) {
9
+ return { epicGroups: [], orphans: [] };
10
+ }
11
+
12
+ const epics = items
13
+ .filter((item) => item.itemKind === 'epic')
14
+ .sort((left, right) => workflowEpicCompareText(left.title ?? left.id, right.title ?? right.id));
15
+ const epicIds = new Set(epics.map((epic) => epic.id));
16
+ /** @type {Map<string, Array<object>>} */
17
+ const childrenByEpicId = new Map();
18
+ /** @type {Set<string>} */
19
+ const assignedChildIds = new Set();
20
+
21
+ for (const item of items) {
22
+ if (item.itemKind === 'epic') {
23
+ continue;
24
+ }
25
+ const parentId = String(item.parentId ?? '').trim();
26
+ if (parentId === '' || !epicIds.has(parentId)) {
27
+ continue;
28
+ }
29
+ if (!childrenByEpicId.has(parentId)) {
30
+ childrenByEpicId.set(parentId, []);
31
+ }
32
+ childrenByEpicId.get(parentId).push(item);
33
+ assignedChildIds.add(item.id);
34
+ }
35
+
36
+ for (const children of childrenByEpicId.values()) {
37
+ children.sort((left, right) => workflowEpicCompareText(left.title ?? left.id, right.title ?? right.id));
38
+ }
39
+
40
+ const epicGroups = epics.map((epic) => {
41
+ const children = childrenByEpicId.get(epic.id) ?? [];
42
+ const doneChildCount = children.filter((child) => WORKFLOW_EPIC_DONE_STATUSES.has(String(child.status ?? ''))).length;
43
+ return {
44
+ epic,
45
+ children,
46
+ childCount: children.length,
47
+ doneChildCount,
48
+ };
49
+ });
50
+
51
+ const orphans = items
52
+ .filter((item) => item.itemKind !== 'epic' && !assignedChildIds.has(item.id))
53
+ .sort((left, right) => workflowEpicCompareText(left.title ?? left.id, right.title ?? right.id));
54
+
55
+ return { epicGroups, orphans };
56
+ }
57
+
58
+ /**
59
+ * @param {Array<object>} items
60
+ */
61
+ export function buildWorkflowDisplayUnits(items) {
62
+ const { epicGroups, orphans } = buildWorkflowEpicGroups(items);
63
+ return [
64
+ ...epicGroups.map((group) => ({ type: 'epic', group })),
65
+ ...orphans.map((item) => ({ type: 'orphan', item })),
66
+ ];
67
+ }
@@ -0,0 +1,53 @@
1
+ const workflowTreeCompareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
2
+
3
+ /**
4
+ * @param {Array<{ id: string, title?: string, parentId?: string, itemKind?: string }>} items
5
+ */
6
+ export function buildWorkflowTreeForest(items) {
7
+ if (!Array.isArray(items)) {
8
+ return [];
9
+ }
10
+
11
+ const visibleIds = new Set(items.map((item) => item.id));
12
+ /** @type {Map<string, Array<object>>} */
13
+ const childrenByParentId = new Map();
14
+
15
+ for (const item of items) {
16
+ let parentId = String(item.parentId ?? '').trim();
17
+ if (parentId !== '' && !visibleIds.has(parentId)) {
18
+ parentId = '';
19
+ }
20
+ if (!childrenByParentId.has(parentId)) {
21
+ childrenByParentId.set(parentId, []);
22
+ }
23
+ childrenByParentId.get(parentId).push(item);
24
+ }
25
+
26
+ for (const children of childrenByParentId.values()) {
27
+ children.sort((left, right) => workflowTreeCompareText(left.title ?? left.id, right.title ?? right.id));
28
+ }
29
+
30
+ /**
31
+ * @param {object} item
32
+ * @param {number} depth
33
+ */
34
+ function buildNode(item, depth) {
35
+ const children = (childrenByParentId.get(item.id) ?? []).map((child) => buildNode(child, depth + 1));
36
+ return {
37
+ item,
38
+ depth,
39
+ childCount: children.length,
40
+ children,
41
+ };
42
+ }
43
+
44
+ const roots = childrenByParentId.get('') ?? [];
45
+ return roots.map((root) => buildNode(root, 0));
46
+ }
47
+
48
+ /**
49
+ * @param {Array<object>} forest
50
+ */
51
+ export function buildWorkflowTreeDisplayUnits(forest) {
52
+ return forest.map((root) => ({ type: 'tree-root', root }));
53
+ }
@@ -0,0 +1,150 @@
1
+ import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { basename, dirname, resolve } from 'node:path';
4
+
5
+ export const WORKSPACES_SCHEMA = 'workspaces.v1';
6
+ export const DEFAULT_REGISTRY_DIR = resolve(homedir(), '.work-graph');
7
+ export const DEFAULT_REGISTRY_PATH = resolve(DEFAULT_REGISTRY_DIR, 'workspaces.json');
8
+
9
+ function compareText(left, right) {
10
+ return String(left).localeCompare(String(right), 'ru', { sensitivity: 'variant' });
11
+ }
12
+
13
+ export function defaultRegistryPath(options = {}) {
14
+ return resolve(options.registryPath ?? DEFAULT_REGISTRY_PATH);
15
+ }
16
+
17
+ export function normalizeWorkspaceRoot(root) {
18
+ return resolve(String(root ?? '').trim());
19
+ }
20
+
21
+ export function slugFromRoot(root) {
22
+ const base = basename(normalizeWorkspaceRoot(root));
23
+ return base
24
+ .toLowerCase()
25
+ .replace(/[^a-z0-9а-яё]+/giu, '-')
26
+ .replace(/^-+|-+$/gu, '')
27
+ || 'project';
28
+ }
29
+
30
+ export function emptyRegistry() {
31
+ return {
32
+ schema: WORKSPACES_SCHEMA,
33
+ activeProjectId: null,
34
+ workspaces: [],
35
+ };
36
+ }
37
+
38
+ export function normalizeRegistry(raw) {
39
+ const registry = raw && typeof raw === 'object' ? raw : emptyRegistry();
40
+ const workspaces = Array.isArray(registry.workspaces)
41
+ ? registry.workspaces.map((entry) => ({
42
+ id: String(entry.id ?? '').trim(),
43
+ root: normalizeWorkspaceRoot(entry.root),
44
+ label: String(entry.label ?? entry.id ?? '').trim(),
45
+ lastOpenedAt: entry.lastOpenedAt ? String(entry.lastOpenedAt) : null,
46
+ })).filter((entry) => entry.id !== '' && entry.root !== '')
47
+ : [];
48
+
49
+ workspaces.sort((left, right) => compareText(left.label || left.id, right.label || right.id));
50
+
51
+ const activeProjectId = String(registry.activeProjectId ?? '').trim() || null;
52
+ const activeExists = activeProjectId && workspaces.some((entry) => entry.id === activeProjectId);
53
+
54
+ return {
55
+ schema: WORKSPACES_SCHEMA,
56
+ activeProjectId: activeExists ? activeProjectId : (workspaces[0]?.id ?? null),
57
+ workspaces,
58
+ };
59
+ }
60
+
61
+ export async function readWorkspaceRegistry(options = {}) {
62
+ const registryPath = defaultRegistryPath(options);
63
+
64
+ try {
65
+ const text = await readFile(registryPath, 'utf8');
66
+ return normalizeRegistry(JSON.parse(text));
67
+ } catch (error) {
68
+ if (error && typeof error === 'object' && error.code === 'ENOENT') {
69
+ return emptyRegistry();
70
+ }
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ export async function writeWorkspaceRegistry(registry, options = {}) {
76
+ const registryPath = defaultRegistryPath(options);
77
+ await mkdir(dirname(registryPath), { recursive: true });
78
+ const normalized = normalizeRegistry(registry);
79
+ const tempPath = `${registryPath}.tmp`;
80
+ await writeFile(tempPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
81
+ await rename(tempPath, registryPath);
82
+ return normalized;
83
+ }
84
+
85
+ export function findWorkspace(registry, projectId) {
86
+ const id = String(projectId ?? '').trim();
87
+ return registry.workspaces.find((entry) => entry.id === id) ?? null;
88
+ }
89
+
90
+ export function resolveWorkspaceRoot(registry, projectId) {
91
+ const workspace = findWorkspace(registry, projectId);
92
+ return workspace?.root ?? null;
93
+ }
94
+
95
+ export async function registerWorkspace(input = {}, options = {}) {
96
+ const root = normalizeWorkspaceRoot(input.root ?? input.path ?? options.cwd);
97
+ const id = String(input.id ?? input.projectId ?? slugFromRoot(root)).trim();
98
+ const label = String(input.label ?? input.name ?? basename(root)).trim() || id;
99
+ const registry = await readWorkspaceRegistry(options);
100
+ const now = new Date().toISOString();
101
+ const existing = findWorkspace(registry, id);
102
+
103
+ const nextEntry = {
104
+ id,
105
+ root,
106
+ label,
107
+ lastOpenedAt: now,
108
+ };
109
+
110
+ const workspaces = existing
111
+ ? registry.workspaces.map((entry) => (entry.id === id ? { ...entry, ...nextEntry } : entry))
112
+ : [...registry.workspaces, nextEntry];
113
+
114
+ return writeWorkspaceRegistry({
115
+ ...registry,
116
+ workspaces,
117
+ activeProjectId: registry.activeProjectId ?? id,
118
+ }, options);
119
+ }
120
+
121
+ export async function setActiveWorkspace(projectId, options = {}) {
122
+ const id = String(projectId ?? '').trim();
123
+ if (id === '') {
124
+ throw new Error('projectId is required');
125
+ }
126
+
127
+ const registry = await readWorkspaceRegistry(options);
128
+ const workspace = findWorkspace(registry, id);
129
+ if (!workspace) {
130
+ throw new Error(`unknown projectId: ${id}`);
131
+ }
132
+
133
+ const now = new Date().toISOString();
134
+ return writeWorkspaceRegistry({
135
+ ...registry,
136
+ activeProjectId: id,
137
+ workspaces: registry.workspaces.map((entry) => (
138
+ entry.id === id ? { ...entry, lastOpenedAt: now } : entry
139
+ )),
140
+ }, options);
141
+ }
142
+
143
+ export async function listWorkspaces(options = {}) {
144
+ const registry = await readWorkspaceRegistry(options);
145
+ return {
146
+ schema: 'workgraph.workspaces.list.v1',
147
+ activeProjectId: registry.activeProjectId,
148
+ workspaces: registry.workspaces,
149
+ };
150
+ }