@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,176 @@
1
+ import {
2
+ attachDerivedWorkItemHierarchy,
3
+ DONE_STATUSES,
4
+ readWorkItemKind,
5
+ } from './workItemHierarchy.mjs';
6
+ import { transitionStatus } from './workGraphRuntime.mjs';
7
+
8
+ const compareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
9
+
10
+ function replaceWorkItem(items, updatedItem) {
11
+ return items.map((entry) => (entry.id === updatedItem.id ? updatedItem : entry));
12
+ }
13
+
14
+ /**
15
+ * @param {Array<{ id: string, status?: string, childIds?: string[] }>} items
16
+ * @param {string} rootId
17
+ */
18
+ export function collectOpenDescendantWorkItems(items, rootId) {
19
+ const enriched = attachDerivedWorkItemHierarchy(items);
20
+ const itemById = new Map(enriched.map((entry) => [entry.id, entry]));
21
+ /** @type {Array<{ id: string, status?: string }>} */
22
+ const open = [];
23
+ /** @type {string[]} */
24
+ const queue = [...(itemById.get(rootId)?.childIds ?? [])];
25
+ const visited = new Set();
26
+
27
+ while (queue.length > 0) {
28
+ const childId = queue.shift();
29
+ if (childId === undefined || visited.has(childId)) {
30
+ continue;
31
+ }
32
+ visited.add(childId);
33
+
34
+ const child = itemById.get(childId);
35
+ if (!child) {
36
+ continue;
37
+ }
38
+
39
+ for (const nestedId of child.childIds ?? []) {
40
+ queue.push(nestedId);
41
+ }
42
+
43
+ if (!DONE_STATUSES.has(String(child.status ?? '').trim())) {
44
+ open.push(child);
45
+ }
46
+ }
47
+
48
+ return open.sort((left, right) => compareText(left.id, right.id));
49
+ }
50
+
51
+ /**
52
+ * @param {Array<import('./workGraphRuntime.mjs').WorkItem>} items
53
+ * @param {import('./workGraphRuntime.mjs').WorkItem} item
54
+ * @param {string} targetStatus
55
+ * @param {{ evidence?: string, cascadeEvidence?: string, reason?: string, blocker?: string }} [options]
56
+ */
57
+ export function transitionWorkItemWithEpicCascade(items, item, targetStatus, options = {}) {
58
+ if (!Array.isArray(items)) {
59
+ throw new TypeError('items must be an array');
60
+ }
61
+ if (!item?.id) {
62
+ throw new TypeError('item.id is required');
63
+ }
64
+
65
+ if (readWorkItemKind(item) !== 'epic' || !DONE_STATUSES.has(targetStatus)) {
66
+ const updated = transitionStatus(item, targetStatus, {
67
+ reason: options.reason,
68
+ blocker: options.blocker,
69
+ evidence: options.evidence,
70
+ allItems: items,
71
+ });
72
+ return {
73
+ items: replaceWorkItem(items, updated),
74
+ updatedItems: [updated],
75
+ cascadedChildIds: [],
76
+ };
77
+ }
78
+
79
+ let working = items.map((entry) => ({ ...entry }));
80
+ /** @type {string[]} */
81
+ const cascadedChildIds = [];
82
+ const cascadeEvidence = String(
83
+ options.cascadeEvidence ?? `cascade: parent epic ${item.id} → ${targetStatus}`,
84
+ ).trim();
85
+ const openDescendants = collectOpenDescendantWorkItems(working, item.id);
86
+
87
+ for (const child of openDescendants) {
88
+ const currentChild = working.find((entry) => entry.id === child.id);
89
+ if (!currentChild) {
90
+ continue;
91
+ }
92
+ const updatedChild = transitionStatus(currentChild, targetStatus, {
93
+ evidence: cascadeEvidence,
94
+ allItems: working,
95
+ });
96
+ working = replaceWorkItem(working, updatedChild);
97
+ cascadedChildIds.push(updatedChild.id);
98
+ }
99
+
100
+ const currentEpic = working.find((entry) => entry.id === item.id) ?? item;
101
+ const updatedEpic = transitionStatus(currentEpic, targetStatus, {
102
+ reason: options.reason,
103
+ blocker: options.blocker,
104
+ evidence: options.evidence,
105
+ allItems: working,
106
+ });
107
+ working = replaceWorkItem(working, updatedEpic);
108
+
109
+ return {
110
+ items: working,
111
+ updatedItems: [
112
+ ...working.filter((entry) => cascadedChildIds.includes(entry.id)),
113
+ updatedEpic,
114
+ ],
115
+ cascadedChildIds,
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Repair drift: epic already done/verified while descendants remain open.
121
+ *
122
+ * @param {Array<import('./workGraphRuntime.mjs').WorkItem>} items
123
+ * @param {string} epicId
124
+ * @param {{ cascadeEvidence?: string, targetStatus?: string }} [options]
125
+ */
126
+ export function closeOpenDescendantsForDoneEpic(items, epicId, options = {}) {
127
+ const epic = items.find((entry) => entry.id === epicId);
128
+ if (!epic || readWorkItemKind(epic) !== 'epic' || !DONE_STATUSES.has(String(epic.status ?? '').trim())) {
129
+ return {
130
+ items,
131
+ updatedItems: [],
132
+ cascadedChildIds: [],
133
+ };
134
+ }
135
+
136
+ const targetStatus = options.targetStatus ?? epic.status;
137
+ let working = items.map((entry) => ({ ...entry }));
138
+ /** @type {string[]} */
139
+ const cascadedChildIds = [];
140
+ const cascadeEvidence = String(
141
+ options.cascadeEvidence ?? `cascade: reconcile epic ${epic.id} already ${targetStatus}`,
142
+ ).trim();
143
+
144
+ for (const child of collectOpenDescendantWorkItems(working, epic.id)) {
145
+ const currentChild = working.find((entry) => entry.id === child.id);
146
+ if (!currentChild) {
147
+ continue;
148
+ }
149
+ const updatedChild = transitionStatus(currentChild, targetStatus, {
150
+ evidence: cascadeEvidence,
151
+ allItems: working,
152
+ });
153
+ working = replaceWorkItem(working, updatedChild);
154
+ cascadedChildIds.push(updatedChild.id);
155
+ }
156
+
157
+ return {
158
+ items: working,
159
+ updatedItems: working.filter((entry) => cascadedChildIds.includes(entry.id)),
160
+ cascadedChildIds,
161
+ };
162
+ }
163
+
164
+ /**
165
+ * @param {Array<import('./workGraphRuntime.mjs').WorkItem>} items
166
+ */
167
+ export function findDoneEpicsWithOpenDescendants(items) {
168
+ return attachDerivedWorkItemHierarchy(items)
169
+ .filter((item) => readWorkItemKind(item) === 'epic' && DONE_STATUSES.has(String(item.status ?? '').trim()))
170
+ .map((epic) => ({
171
+ epicId: epic.id,
172
+ openChildIds: collectOpenDescendantWorkItems(items, epic.id).map((child) => child.id),
173
+ }))
174
+ .filter((entry) => entry.openChildIds.length > 0)
175
+ .sort((left, right) => compareText(left.epicId, right.epicId));
176
+ }
@@ -0,0 +1,78 @@
1
+ import { PIPELINE_VERDICTS } from './workItemDecisionPipeline.mjs';
2
+
3
+ const EXECUTION_TARGET_STATUSES = new Set(['ready', 'claimed', 'doing', 'in_progress']);
4
+
5
+ export function hasWorkItemAnalysis(item) {
6
+ return String(item?.analysis ?? '').trim().length > 0;
7
+ }
8
+
9
+ export function workItemDecisionVerdict(item) {
10
+ const verdict = String(item?.labels?.['work.decision.verdict'] ?? '').trim();
11
+ return PIPELINE_VERDICTS.includes(verdict) ? verdict : null;
12
+ }
13
+
14
+ export function evaluateWorkItemExecutionGate(item) {
15
+ if (!item) {
16
+ return {
17
+ allowed: false,
18
+ code: 'work_item_missing',
19
+ message: 'WorkItem not found',
20
+ };
21
+ }
22
+
23
+ if (!hasWorkItemAnalysis(item)) {
24
+ return {
25
+ allowed: false,
26
+ code: 'missing_analysis',
27
+ message:
28
+ 'Нет анализа в атоме задачи. В Cursor: get_work_item → анализ целесообразности (до исполнения) → record_work_item_analysis.',
29
+ };
30
+ }
31
+
32
+ const verdict = workItemDecisionVerdict(item);
33
+ if (verdict === null) {
34
+ return {
35
+ allowed: false,
36
+ code: 'missing_decision',
37
+ message:
38
+ 'Нет утверждения (useful/harmful/defer). В Cursor: record_work_item_decision после анализа.',
39
+ };
40
+ }
41
+
42
+ if (verdict === 'defer') {
43
+ return {
44
+ allowed: false,
45
+ code: 'verdict_defer',
46
+ message: 'Задача отложена (defer). Исполнение запрещено до пересмотра и нового useful.',
47
+ };
48
+ }
49
+
50
+ if (verdict === 'harmful') {
51
+ return {
52
+ allowed: false,
53
+ code: 'verdict_harmful',
54
+ message: 'Задача отклонена (harmful). Исполнение запрещено.',
55
+ };
56
+ }
57
+
58
+ return {
59
+ allowed: true,
60
+ code: 'ok',
61
+ message: '',
62
+ verdict,
63
+ };
64
+ }
65
+
66
+ export function assertWorkItemExecutionAllowed(item) {
67
+ const gate = evaluateWorkItemExecutionGate(item);
68
+ if (!gate.allowed) {
69
+ const error = new Error(gate.message);
70
+ error.code = gate.code;
71
+ throw error;
72
+ }
73
+ return gate;
74
+ }
75
+
76
+ export function statusChangeRequiresExecutionGate(status) {
77
+ return EXECUTION_TARGET_STATUSES.has(String(status ?? '').trim());
78
+ }
@@ -0,0 +1,226 @@
1
+ const compareText = (left, right) => String(left).localeCompare(String(right), 'en', { sensitivity: 'variant' });
2
+
3
+ export const WORK_ITEM_KINDS = ['epic', 'task', 'subtask'];
4
+ export const DEFAULT_WORK_ITEM_KIND = 'task';
5
+ export const DONE_STATUSES = new Set(['done', 'verified']);
6
+
7
+ export function normalizeWorkItemKind(value) {
8
+ const kind = String(value ?? '').trim().toLowerCase();
9
+ if (kind === '') {
10
+ return DEFAULT_WORK_ITEM_KIND;
11
+ }
12
+ return kind;
13
+ }
14
+
15
+ export function isValidWorkItemKind(value) {
16
+ const kind = String(value ?? '').trim().toLowerCase();
17
+ return kind === '' || WORK_ITEM_KINDS.includes(kind);
18
+ }
19
+
20
+ export function readWorkItemParentId(item) {
21
+ return String(item?.parentId ?? item?.labels?.['work.parent_id'] ?? '').trim();
22
+ }
23
+
24
+ export function readWorkItemKind(item) {
25
+ return normalizeWorkItemKind(item?.itemKind ?? item?.labels?.['work.item_kind']);
26
+ }
27
+
28
+ export function buildChildIdsByParentId(items) {
29
+ const childIdsByParent = new Map();
30
+
31
+ for (const item of items) {
32
+ const parentId = readWorkItemParentId(item);
33
+ if (parentId === '') {
34
+ continue;
35
+ }
36
+
37
+ if (!childIdsByParent.has(parentId)) {
38
+ childIdsByParent.set(parentId, []);
39
+ }
40
+ childIdsByParent.get(parentId).push(item.id);
41
+ }
42
+
43
+ for (const childIds of childIdsByParent.values()) {
44
+ childIds.sort(compareText);
45
+ }
46
+
47
+ return childIdsByParent;
48
+ }
49
+
50
+ export function attachDerivedWorkItemHierarchy(items) {
51
+ const childIdsByParent = buildChildIdsByParentId(items);
52
+
53
+ return items.map((item) => ({
54
+ ...item,
55
+ parentId: readWorkItemParentId(item),
56
+ itemKind: readWorkItemKind(item),
57
+ childIds: [...(childIdsByParent.get(item.id) ?? [])],
58
+ }));
59
+ }
60
+
61
+ export function hasWorkItemParentCycle(itemId, itemById) {
62
+ const visited = new Set();
63
+ let current = itemId;
64
+
65
+ while (current !== '') {
66
+ if (visited.has(current)) {
67
+ return true;
68
+ }
69
+ visited.add(current);
70
+ current = readWorkItemParentId(itemById.get(current));
71
+ }
72
+
73
+ return false;
74
+ }
75
+
76
+ export function lintWorkItemHierarchyIssues(items) {
77
+ if (!Array.isArray(items)) {
78
+ throw new TypeError('items must be an array');
79
+ }
80
+
81
+ const issues = [];
82
+ const itemById = new Map(items.map((item) => [item.id, item]));
83
+ const childIdsByParent = buildChildIdsByParentId(items);
84
+
85
+ for (const item of items) {
86
+ const parentId = readWorkItemParentId(item);
87
+ const rawKind = String(item?.labels?.['work.item_kind'] ?? item?.itemKind ?? '').trim().toLowerCase();
88
+ const itemKind = readWorkItemKind(item);
89
+
90
+ if (rawKind !== '' && !WORK_ITEM_KINDS.includes(rawKind)) {
91
+ issues.push({
92
+ severity: 'error',
93
+ code: 'invalid_item_kind',
94
+ message: `Invalid work.item_kind "${rawKind}" for ${item.id}`,
95
+ workId: item.id,
96
+ itemKind: rawKind,
97
+ });
98
+ }
99
+
100
+ if (parentId === item.id) {
101
+ issues.push({
102
+ severity: 'error',
103
+ code: 'self_parent',
104
+ message: `WorkItem cannot be its own parent: ${item.id}`,
105
+ workId: item.id,
106
+ });
107
+ continue;
108
+ }
109
+
110
+ if (parentId !== '' && !itemById.has(parentId)) {
111
+ issues.push({
112
+ severity: 'error',
113
+ code: 'missing_parent',
114
+ message: `Missing parent "${parentId}" for ${item.id}`,
115
+ workId: item.id,
116
+ parentId,
117
+ });
118
+ }
119
+
120
+ if (parentId !== '' && hasWorkItemParentCycle(item.id, itemById)) {
121
+ issues.push({
122
+ severity: 'error',
123
+ code: 'parent_cycle',
124
+ message: `Parent cycle detected for ${item.id}`,
125
+ workId: item.id,
126
+ parentId,
127
+ });
128
+ }
129
+
130
+ for (const dependencyId of item.dependsOn ?? []) {
131
+ const dependency = itemById.get(dependencyId);
132
+ if (dependency !== undefined && readWorkItemParentId(dependency) === item.id) {
133
+ issues.push({
134
+ severity: 'error',
135
+ code: 'parent_depends_on_child',
136
+ message: `Parent ${item.id} depends_on child ${dependencyId}`,
137
+ workId: item.id,
138
+ dependencyId,
139
+ });
140
+ }
141
+ }
142
+
143
+ if (itemKind === 'epic' && (childIdsByParent.get(item.id) ?? []).length === 0) {
144
+ issues.push({
145
+ severity: 'warning',
146
+ code: 'epic_without_children',
147
+ message: `Epic ${item.id} has no child WorkItems`,
148
+ workId: item.id,
149
+ itemKind,
150
+ });
151
+ }
152
+
153
+ if (itemKind === 'epic' && DONE_STATUSES.has(String(item.status ?? '').trim())) {
154
+ const rollup = summarizeWorkItemHierarchyRollup(item, items);
155
+ if (rollup.openChildIds.length > 0) {
156
+ issues.push({
157
+ severity: 'warning',
158
+ code: 'epic_done_open_children',
159
+ message: `Epic ${item.id} is done but child WorkItems remain open (${rollup.openChildIds.join(', ')})`,
160
+ workId: item.id,
161
+ openChildIds: rollup.openChildIds,
162
+ });
163
+ }
164
+ }
165
+ }
166
+
167
+ return issues;
168
+ }
169
+
170
+ export function summarizeWorkItemHierarchyRollup(item, items) {
171
+ const enriched = attachDerivedWorkItemHierarchy(items);
172
+ const fullItem = enriched.find((candidate) => candidate.id === item.id) ?? item;
173
+ const childIds = fullItem.childIds ?? [];
174
+ if (childIds.length === 0) {
175
+ return {
176
+ childCount: 0,
177
+ doneChildCount: 0,
178
+ openChildIds: [],
179
+ closeBlocked: false,
180
+ };
181
+ }
182
+
183
+ const itemById = new Map(enriched.map((entry) => [entry.id, entry]));
184
+ const openChildIds = childIds.filter((childId) => {
185
+ const child = itemById.get(childId);
186
+ return child !== undefined && !DONE_STATUSES.has(child.status);
187
+ });
188
+
189
+ return {
190
+ childCount: childIds.length,
191
+ doneChildCount: childIds.length - openChildIds.length,
192
+ openChildIds,
193
+ closeBlocked: openChildIds.length > 0,
194
+ };
195
+ }
196
+
197
+ export function evaluateParentCloseGate(items, item, targetStatus) {
198
+ if (!DONE_STATUSES.has(String(targetStatus ?? '').trim())) {
199
+ return { ok: true };
200
+ }
201
+
202
+ const rollup = summarizeWorkItemHierarchyRollup(item, items);
203
+ if (!rollup.closeBlocked) {
204
+ return { ok: true, rollup };
205
+ }
206
+
207
+ return {
208
+ ok: false,
209
+ code: 'parent_close_blocked_open_children',
210
+ message: `Cannot close ${item.id}: open child WorkItems remain (${rollup.openChildIds.join(', ')})`,
211
+ workId: item.id,
212
+ openChildIds: rollup.openChildIds,
213
+ rollup,
214
+ };
215
+ }
216
+
217
+ export function assertParentCloseAllowed(items, item, targetStatus) {
218
+ const gate = evaluateParentCloseGate(items, item, targetStatus);
219
+ if (!gate.ok) {
220
+ const error = new Error(gate.message);
221
+ error.code = gate.code;
222
+ error.openChildIds = gate.openChildIds;
223
+ throw error;
224
+ }
225
+ return gate;
226
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Lint human prose in WorkItem sections — робототекст и англоязычный жаргон.
3
+ */
4
+
5
+ const PROSE_FIELDS = ['basis', 'vector', 'goal', 'checks', 'analysis', 'decision'];
6
+
7
+ /** @type {Array<{ code: string, pattern: RegExp, hint: string }>} */
8
+ export const WORK_ITEM_JARGON_PATTERNS = [
9
+ { code: 'jargon_closing_analysis', pattern: /\bclosing analysis\b/ui, hint: '«итоговый разбор после эпика»' },
10
+ { code: 'jargon_canon_colon', pattern: /\bCanon:/ui, hint: '«канон:» или опиши по-русски' },
11
+ { code: 'jargon_evidence_word', pattern: /\bevidence\b/ui, hint: '«свидетельства» / «подтверждение»' },
12
+ { code: 'jargon_upstream', pattern: /\bupstream\b/ui, hint: '«вышестоящие зависимости»' },
13
+ { code: 'jargon_feeds_epics', pattern: /\bfeeds_epics\b/ui, hint: '«питает эпики» или «связан с эпиком»' },
14
+ { code: 'jargon_track_letter', pattern: /\bTrack [A-D]\b/ui, hint: 'нумерованные шаги по-русски' },
15
+ { code: 'jargon_rationale', pattern: /\brationale\b/ui, hint: '«обоснование»' },
16
+ { code: 'jargon_published', pattern: /\bpublished\b/ui, hint: '«опубликован»' },
17
+ { code: 'jargon_scope_drift', pattern: /\bscope drift\b/ui, hint: '«размывание границ»' },
18
+ { code: 'jargon_actionable', pattern: /\bactionable\b/ui, hint: '«можно брать в работу»' },
19
+ { code: 'jargon_outcomes', pattern: /\bOutcomes\b/ui, hint: '«результаты»' },
20
+ { code: 'template_stoit_zavesti', pattern: /Стоит завести «/u, hint: 'пиши Зачем:, не шаблон create_work_item' },
21
+ { code: 'template_tselesoobraznost', pattern: /Целесообразность:/u, hint: 'используй Зачем: / Когда: / Риск:' },
22
+ { code: 'template_depends_on_eq', pattern: /depends_on=/u, hint: '«зависит от: …» по-русски' },
23
+ { code: 'template_upstream_deps', pattern: /upstream-зависимостей/u, hint: '«зависимостей выше по цепочке»' },
24
+ { code: 'template_intake_razbor', pattern: /разбор материалов приёма/u, hint: 'конкретная формулировка вместо шаблона' },
25
+ { code: 'jargon_doc_published', pattern: /\bdoc published\b/ui, hint: '«документ опубликован»' },
26
+ { code: 'jargon_epic_closed', pattern: /\bepic closed with evidence\b/ui, hint: '«эпик закрыт, свидетельства приложены»' },
27
+ { code: 'jargon_journal_entry', pattern: /\bjournal entry\b/ui, hint: '«запись в журнале»' },
28
+ { code: 'jargon_closing_entry', pattern: /\bclosing entry\b/ui, hint: '«итоговая запись»' },
29
+ { code: 'title_an_closing_epic', pattern: /AN-\d+\s+closing:/ui, hint: '«Закрыть разбор AN-n после эпика …»' },
30
+ ];
31
+
32
+ const TITLE_ENGLISH_LEAD = /^(?:UI|CLI|ADR|Runbook|Tests?|Implement|Write|Decide|Docs|Track)\b/u;
33
+
34
+ function proseBlob(item) {
35
+ const parts = [];
36
+ for (const field of PROSE_FIELDS) {
37
+ parts.push(...sectionToLines(item[field]));
38
+ }
39
+ const title = String(item.title ?? item.labels?.['work.title'] ?? '').trim();
40
+ if (title) {
41
+ parts.push(title);
42
+ }
43
+ return parts.join('\n');
44
+ }
45
+
46
+ function sectionToLines(value) {
47
+ if (Array.isArray(value)) {
48
+ return value.map((line) => String(line).trim()).filter(Boolean);
49
+ }
50
+ return String(value ?? '')
51
+ .split(/\n+/u)
52
+ .map((line) => line.trim())
53
+ .filter(Boolean);
54
+ }
55
+
56
+ function checkLines(field, lines, workId, issues) {
57
+ const normalized = sectionToLines(lines);
58
+
59
+ if (field === 'checks' && normalized.length > 0 && normalized.length < 2) {
60
+ issues.push({
61
+ severity: 'warning',
62
+ code: 'short_checks_count',
63
+ message: `Проверки для ${workId}: нужно минимум 2 пункта, сейчас ${normalized.length}`,
64
+ workId,
65
+ field,
66
+ });
67
+ }
68
+
69
+ for (const line of normalized) {
70
+ if (field === 'checks' && line.length > 0 && line.length < 20) {
71
+ issues.push({
72
+ severity: 'warning',
73
+ code: 'short_check_line',
74
+ message: `Проверки для ${workId}: пункт слишком короткий (${line.length} симв.): «${line.slice(0, 40)}»`,
75
+ workId,
76
+ field,
77
+ });
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * @param {{ id?: string, title?: string, basis?: string[], vector?: string[], goal?: string[], checks?: string[], analysis?: string[], decision?: string[], labels?: Record<string, string> }} item
84
+ */
85
+ export function evaluateWorkItemProseLint(item) {
86
+ const issues = [];
87
+ const workId = item.id ?? item.labels?.['work.id'] ?? '';
88
+
89
+ for (const field of PROSE_FIELDS) {
90
+ checkLines(field, item[field], workId, issues);
91
+ }
92
+
93
+ const title = String(item.title ?? item.labels?.['work.title'] ?? '').trim();
94
+ if (TITLE_ENGLISH_LEAD.test(title)) {
95
+ issues.push({
96
+ severity: 'warning',
97
+ code: 'english_lead_title',
98
+ message: `work.title для ${workId} начинается с английского шаблона: «${title}»`,
99
+ workId,
100
+ field: 'title',
101
+ });
102
+ }
103
+
104
+ for (const { code, pattern, hint } of WORK_ITEM_JARGON_PATTERNS) {
105
+ if (code.startsWith('title_') && title !== '' && pattern.test(title)) {
106
+ issues.push({
107
+ severity: 'warning',
108
+ code: `work_item_prose_jargon_${code}`,
109
+ message: `work.title для ${workId}: ${hint}`,
110
+ workId,
111
+ field: 'title',
112
+ });
113
+ }
114
+ }
115
+
116
+ const blob = proseBlob(item);
117
+ for (const { code, pattern, hint } of WORK_ITEM_JARGON_PATTERNS) {
118
+ if (code.startsWith('title_')) {
119
+ continue;
120
+ }
121
+ if (pattern.test(blob)) {
122
+ issues.push({
123
+ severity: 'warning',
124
+ code: `work_item_prose_jargon_${code}`,
125
+ message: `Робототекст/жаргон в ${workId}: ${hint} (pattern ${code})`,
126
+ workId,
127
+ field: 'prose',
128
+ });
129
+ }
130
+ }
131
+
132
+ return issues;
133
+ }