@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,1008 @@
1
+ import {
2
+ attachDerivedWorkItemHierarchy,
3
+ assertParentCloseAllowed,
4
+ readWorkItemKind,
5
+ readWorkItemParentId,
6
+ } from './workItemHierarchy.mjs';
7
+
8
+ const STEP_ATOM_PATTERN = /^#([^\n<]+)<\[\n([\s\S]*?)\n\]>/gmu;
9
+ const LIST_SEPARATOR_PATTERN = /\s*,\s*/u;
10
+ const TRACE_LABEL_KEYS = ['trace.code_refs', 'trace.artifact_refs', 'trace.evidence_refs', 'trace.links'];
11
+ const REVERSE_MARKER_PATTERN = /(?:@iohasc-id:\s*([^\s`"'<>]+)|iohasc-ref:\s*([^\s`"'<>]+))/gu;
12
+ const RANGE_LOCATOR_PATTERN = /^(.+)#L(\d+)(?:-L(\d+))?$/u;
13
+
14
+ const READY_STATUSES = new Set(['ready']);
15
+ const DONE_STATUSES = new Set(['done', 'verified']);
16
+ const ACTIVE_CLAIM_STATUSES = new Set(['claimed', 'doing', 'in_progress']);
17
+ const ALLOWED_STATUSES = new Set(['backlog', 'ready', 'claimed', 'doing', 'in_progress', 'verify', 'done', 'blocked']);
18
+ export const DEFAULT_CLAIM_LEASE_MS = 15 * 60 * 1000;
19
+
20
+ export class WorkGraphPolicyError extends Error {
21
+ constructor(message) {
22
+ super(message);
23
+ this.name = 'WorkGraphPolicyError';
24
+ }
25
+ }
26
+
27
+ export function parseWorkItems(text) {
28
+ if (typeof text !== 'string') {
29
+ throw new TypeError('text must be a string');
30
+ }
31
+
32
+ const items = [];
33
+
34
+ for (const match of text.matchAll(STEP_ATOM_PATTERN)) {
35
+ const [, atomName, body] = match;
36
+ const sections = parseSections(body);
37
+ const labels = sections.labels;
38
+
39
+ if (labels['atom.profile'] !== 'work_item' && labels['work.id'] === undefined) {
40
+ continue;
41
+ }
42
+
43
+ const id = labels['work.id'];
44
+ if (id === undefined || id.trim() === '') {
45
+ continue;
46
+ }
47
+
48
+ items.push({
49
+ atomName: atomName.trim(),
50
+ id: id.trim(),
51
+ title: labels['work.title'] ?? id.trim(),
52
+ status: normalizeStatus(labels['work.status']),
53
+ ownerRole: labels['work.owner_role'] ?? '',
54
+ department: labels['work.department'] ?? '',
55
+ priority: labels['work.priority'] ?? '',
56
+ risk: labels['work.risk'] ?? '',
57
+ dependsOn: parseList(labels['work.depends_on']),
58
+ targetFiles: parseList(labels['work.target_files']),
59
+ traceStatus: labels['trace.status'] ?? '',
60
+ nextAction: labels['work.next_action'] ?? '',
61
+ evidence: sections.evidence,
62
+ checks: sections.checks,
63
+ blocker: labels['work.blocker'] ?? labels['work.blocked_reason'] ?? '',
64
+ basis: normalizeSectionText(sections.basis),
65
+ vector: normalizeSectionText(sections.vector),
66
+ goal: normalizeSectionText(sections.goal),
67
+ analysis: normalizeSectionText(sections.analysis),
68
+ decision: normalizeSectionText(sections.decision),
69
+ uiRefs: normalizeSectionText(sections.uiRefs),
70
+ parentId: String(labels['work.parent_id'] ?? '').trim(),
71
+ itemKind: readWorkItemKind({ labels }),
72
+ labels,
73
+ });
74
+ }
75
+
76
+ return items;
77
+ }
78
+
79
+ export function claimNext(items) {
80
+ assertItems(items);
81
+
82
+ const doneIds = new Set(items.filter((item) => DONE_STATUSES.has(item.status)).map((item) => item.id));
83
+
84
+ return stableItems(items).find((item) => {
85
+ if (!READY_STATUSES.has(item.status)) {
86
+ return false;
87
+ }
88
+
89
+ return item.dependsOn.every((dependencyId) => doneIds.has(dependencyId));
90
+ }) ?? null;
91
+ }
92
+
93
+ export function getDoneWorkItemIds(items) {
94
+ assertItems(items);
95
+ return new Set(items.filter((item) => DONE_STATUSES.has(item.status)).map((item) => item.id));
96
+ }
97
+
98
+ export function areWorkItemDependenciesSatisfied(items, item) {
99
+ assertItems(items);
100
+
101
+ if (item === undefined || item === null) {
102
+ return false;
103
+ }
104
+
105
+ const doneIds = getDoneWorkItemIds(items);
106
+ return (item.dependsOn ?? []).every((dependencyId) => doneIds.has(dependencyId));
107
+ }
108
+
109
+ export function isPromotableBacklogItem(items, item) {
110
+ return item?.status === 'backlog' && areWorkItemDependenciesSatisfied(items, item);
111
+ }
112
+
113
+ export function evaluatePromoteReadyEligibility(items, workId, options = {}) {
114
+ assertItems(items);
115
+
116
+ const normalizedWorkId = String(workId ?? '').trim();
117
+ if (normalizedWorkId === '') {
118
+ return { ok: false, error: 'work_id_required' };
119
+ }
120
+
121
+ const item = items.find((entry) => entry.id === normalizedWorkId);
122
+ if (item === undefined) {
123
+ return { ok: false, error: 'task_not_found', workId: normalizedWorkId };
124
+ }
125
+
126
+ if (item.status !== 'backlog') {
127
+ return {
128
+ ok: false,
129
+ error: 'task_not_backlog',
130
+ workId: normalizedWorkId,
131
+ currentStatus: item.status,
132
+ };
133
+ }
134
+
135
+ const doneIds = getDoneWorkItemIds(items);
136
+ const unsatisfiedDependencies = (item.dependsOn ?? []).filter((dependencyId) => !doneIds.has(dependencyId));
137
+
138
+ if (unsatisfiedDependencies.length > 0) {
139
+ return {
140
+ ok: false,
141
+ error: 'dependencies_unsatisfied',
142
+ workId: normalizedWorkId,
143
+ unsatisfiedDependencies: [...unsatisfiedDependencies].sort(compareText),
144
+ };
145
+ }
146
+
147
+ const charterPreflight = options.charterPreflight;
148
+ if (charterPreflight && charterPreflight.ok === false) {
149
+ return {
150
+ ok: false,
151
+ error: 'charter_preflight_blocked',
152
+ workId: normalizedWorkId,
153
+ charterPreflight,
154
+ charterViolations: charterPreflight.violations ?? [],
155
+ };
156
+ }
157
+
158
+ return { ok: true, workId: normalizedWorkId, item };
159
+ }
160
+
161
+ export function transitionStatus(item, targetStatus, options = {}) {
162
+ assertItem(item);
163
+
164
+ if (!ALLOWED_STATUSES.has(targetStatus)) {
165
+ throw new WorkGraphPolicyError(`unsupported target status: ${targetStatus}`);
166
+ }
167
+
168
+ if (targetStatus === 'done' && !hasEvidence(item, options)) {
169
+ throw new WorkGraphPolicyError('cannot mark done without evidence');
170
+ }
171
+
172
+ if (Array.isArray(options.allItems) && (targetStatus === 'done' || targetStatus === 'verified')) {
173
+ assertParentCloseAllowed(options.allItems, item, targetStatus);
174
+ }
175
+
176
+ if (targetStatus === 'blocked' && !hasBlockerReason(options)) {
177
+ throw new WorkGraphPolicyError('cannot mark blocked without reason');
178
+ }
179
+
180
+ const nextItem = {
181
+ ...item,
182
+ status: targetStatus,
183
+ labels: {
184
+ ...item.labels,
185
+ 'work.status': targetStatus,
186
+ },
187
+ };
188
+
189
+ if (options.reason !== undefined || options.blocker !== undefined) {
190
+ const blocker = String(options.reason ?? options.blocker).trim();
191
+ nextItem.blocker = blocker;
192
+ nextItem.labels['work.blocker'] = blocker;
193
+ }
194
+
195
+ if (options.evidence !== undefined) {
196
+ return recordEvidence(nextItem, options.evidence);
197
+ }
198
+
199
+ return nextItem;
200
+ }
201
+
202
+ export function getClaimLeaseUntil(item) {
203
+ const raw = item?.labels?.['work.claim_lease_until'] ?? '';
204
+ if (raw === '') {
205
+ return null;
206
+ }
207
+
208
+ const timestamp = Date.parse(String(raw));
209
+ return Number.isNaN(timestamp) ? null : timestamp;
210
+ }
211
+
212
+ export function isActiveClaimLease(item, nowMs = Date.now()) {
213
+ const leaseUntil = getClaimLeaseUntil(item);
214
+ return leaseUntil !== null && leaseUntil > nowMs;
215
+ }
216
+
217
+ export function evaluateWorkItemClaimEligibility(item, options = {}) {
218
+ assertItem(item);
219
+
220
+ const nowMs = options.nowMs ?? Date.now();
221
+ const claimRunId = String(options.claimRunId ?? '').trim();
222
+ const claimedBy = String(item.labels?.['work.claimed_by'] ?? '').trim();
223
+ const leaseActive = isActiveClaimLease(item, nowMs);
224
+
225
+ if (item.status === 'ready') {
226
+ return { ok: true, workId: item.id, reclaim: false, idempotent: false };
227
+ }
228
+
229
+ if (ACTIVE_CLAIM_STATUSES.has(item.status)) {
230
+ if (leaseActive) {
231
+ if (claimRunId !== '' && claimRunId === claimedBy) {
232
+ return { ok: true, workId: item.id, reclaim: false, idempotent: true, claimedBy, leaseUntil: item.labels['work.claim_lease_until'] };
233
+ }
234
+
235
+ return {
236
+ ok: false,
237
+ error: 'claim_lease_active',
238
+ workId: item.id,
239
+ currentStatus: item.status,
240
+ claimedBy: claimedBy || null,
241
+ leaseUntil: item.labels?.['work.claim_lease_until'] ?? null,
242
+ };
243
+ }
244
+
245
+ if (claimedBy !== '') {
246
+ if (claimRunId !== '' && claimRunId === claimedBy) {
247
+ return { ok: true, workId: item.id, reclaim: false, idempotent: true, claimedBy, leaseUntil: item.labels?.['work.claim_lease_until'] ?? null };
248
+ }
249
+
250
+ return { ok: true, workId: item.id, reclaim: true, idempotent: false };
251
+ }
252
+
253
+ return {
254
+ ok: false,
255
+ error: 'claim_lease_active',
256
+ workId: item.id,
257
+ currentStatus: item.status,
258
+ claimedBy: null,
259
+ leaseUntil: null,
260
+ };
261
+ }
262
+
263
+ return {
264
+ ok: false,
265
+ error: 'task_not_claimable',
266
+ workId: item.id,
267
+ currentStatus: item.status,
268
+ };
269
+ }
270
+
271
+ export function claimWorkItemWithLease(item, options = {}) {
272
+ const eligibility = evaluateWorkItemClaimEligibility(item, options);
273
+ if (!eligibility.ok) {
274
+ return {
275
+ ok: false,
276
+ ...eligibility,
277
+ item,
278
+ newStatus: item.status,
279
+ previousStatus: item.status,
280
+ };
281
+ }
282
+
283
+ if (eligibility.idempotent) {
284
+ return {
285
+ ok: true,
286
+ idempotent: true,
287
+ workId: item.id,
288
+ item,
289
+ previousStatus: item.status,
290
+ newStatus: item.status,
291
+ claimRunId: options.claimRunId ?? item.labels?.['work.claimed_by'] ?? null,
292
+ leaseUntil: item.labels?.['work.claim_lease_until'] ?? null,
293
+ };
294
+ }
295
+
296
+ const nowMs = options.nowMs ?? Date.now();
297
+ const leaseMs = Number.isInteger(options.leaseMs) && options.leaseMs > 0 ? options.leaseMs : DEFAULT_CLAIM_LEASE_MS;
298
+ const claimRunId = String(options.claimRunId ?? `claim-${item.id}-${nowMs}`).trim();
299
+ const leaseUntil = new Date(nowMs + leaseMs).toISOString();
300
+ const targetStatus = options.targetStatus ?? 'claimed';
301
+ const previousStatus = item.status;
302
+
303
+ let current = item;
304
+ if (previousStatus === 'ready' || eligibility.reclaim) {
305
+ current = transitionStatus(item, targetStatus, {
306
+ evidence: options.evidence ?? `claim: ${item.id} lease until ${leaseUntil}`,
307
+ });
308
+ }
309
+
310
+ current = {
311
+ ...current,
312
+ labels: {
313
+ ...current.labels,
314
+ 'work.claimed_by': claimRunId,
315
+ 'work.claim_lease_until': leaseUntil,
316
+ },
317
+ };
318
+
319
+ return {
320
+ ok: true,
321
+ idempotent: false,
322
+ reclaim: eligibility.reclaim === true,
323
+ workId: item.id,
324
+ item: current,
325
+ previousStatus,
326
+ newStatus: current.status,
327
+ claimRunId,
328
+ leaseUntil,
329
+ };
330
+ }
331
+
332
+ export function recordEvidence(item, evidence) {
333
+ assertItem(item);
334
+ const normalizedEvidence = normalizeEvidence(evidence);
335
+
336
+ return {
337
+ ...item,
338
+ evidence: [...item.evidence, normalizedEvidence],
339
+ };
340
+ }
341
+
342
+ export function buildSnapshot(items) {
343
+ assertItems(items);
344
+
345
+ const enrichedItems = attachDerivedWorkItemHierarchy(items);
346
+ const snapshotItems = stableItems(enrichedItems).map((item) => ({
347
+ key: '',
348
+ id: item.id,
349
+ title: item.title,
350
+ status: item.status,
351
+ ownerRole: item.ownerRole,
352
+ department: item.department,
353
+ priority: item.priority,
354
+ risk: item.risk,
355
+ dependsOn: [...item.dependsOn].sort(compareText),
356
+ targetFiles: [...item.targetFiles].sort(compareText),
357
+ traceStatus: item.traceStatus,
358
+ nextAction: item.nextAction,
359
+ evidence: [...item.evidence].sort(compareText),
360
+ checks: [...item.checks].sort(compareText),
361
+ blocker: item.blocker,
362
+ basis: item.basis,
363
+ vector: item.vector,
364
+ goal: item.goal,
365
+ analysis: item.analysis ?? '',
366
+ decision: item.decision ?? '',
367
+ uiRefs: item.uiRefs ?? '',
368
+ parentId: readWorkItemParentId(item),
369
+ itemKind: readWorkItemKind(item),
370
+ childIds: [...(item.childIds ?? [])],
371
+ labels: { ...(item.labels ?? {}) },
372
+ }));
373
+
374
+ snapshotItems.forEach((item, index) => {
375
+ item.key = `WG-${String(index + 1).padStart(3, '0')}`;
376
+ });
377
+
378
+ return {
379
+ schema: 'workgraph.snapshot.v1',
380
+ source: '.bvc',
381
+ items: snapshotItems,
382
+ edges: buildEdges(snapshotItems),
383
+ statusCounts: buildStatusCounts(snapshotItems),
384
+ readyQueue: snapshotItems
385
+ .filter((item) => item.status === 'ready')
386
+ .map((item) => item.id),
387
+ };
388
+ }
389
+
390
+ export function buildOperatorDashboardSnapshot(workGraphSnapshot, options = {}) {
391
+ assertWorkGraphSnapshot(workGraphSnapshot);
392
+
393
+ const items = stableItems(workGraphSnapshot.items);
394
+ const itemById = new Map(items.map((item) => [item.id, item]));
395
+ const doneIds = new Set(items.filter((item) => DONE_STATUSES.has(item.status)).map((item) => item.id));
396
+ const readyIds = Array.isArray(workGraphSnapshot.readyQueue)
397
+ ? workGraphSnapshot.readyQueue
398
+ : items.filter((item) => item.status === 'ready').map((item) => item.id);
399
+
400
+ const currentTasks = items
401
+ .filter((item) => ['claimed', 'doing', 'in_progress', 'verify'].includes(item.status))
402
+ .map(toDashboardTaskSummary);
403
+
404
+ const readyQueue = readyIds
405
+ .map((id) => itemById.get(id))
406
+ .filter(Boolean)
407
+ .map((item) => ({
408
+ ...toDashboardTaskSummary(item),
409
+ dependencies: item.dependsOn.map((dependencyId) => ({
410
+ id: dependencyId,
411
+ status: itemById.get(dependencyId)?.status ?? 'missing',
412
+ satisfied: doneIds.has(dependencyId),
413
+ })),
414
+ claimable: item.dependsOn.every((dependencyId) => doneIds.has(dependencyId)),
415
+ }));
416
+
417
+ const blocked = items
418
+ .filter((item) => item.status === 'blocked' || item.blocker)
419
+ .map((item) => ({
420
+ ...toDashboardTaskSummary(item),
421
+ blocker: item.blocker,
422
+ nextUnblockAction: item.nextAction,
423
+ }));
424
+
425
+ return {
426
+ schema: 'operator-dashboard.snapshot.v1',
427
+ sourceSchema: workGraphSnapshot.schema,
428
+ source: workGraphSnapshot.source,
429
+ currentTasks,
430
+ currentTask: currentTasks[0] ?? null,
431
+ readyQueue,
432
+ blocked,
433
+ statusCounts: buildDashboardStatusCounts(items),
434
+ recentEvidence: buildRecentEvidence(items, options.evidenceLimit),
435
+ workerRunSummaries: normalizeProjectionList(options.workerRunSummaries ?? options.workerRuns),
436
+ memoryUpdates: normalizeProjectionList(options.memoryUpdates),
437
+ actionFeed: normalizeProjectionList(options.actionFeed),
438
+ viewCounts: buildViewCounts(items),
439
+ };
440
+ }
441
+
442
+ export function parseTraceLinksV1(items) {
443
+ assertItems(items);
444
+
445
+ return stableItems(items).flatMap((item) =>
446
+ TRACE_LABEL_KEYS.flatMap((labelKey) =>
447
+ parseList(item.labels?.[labelKey]).map((rawRef) => ({
448
+ id: `trace:${item.id}:${labelKey}:${rawRef}`,
449
+ from: { kind: 'work', id: item.id },
450
+ to: parseTraceEndpoint(labelKey, rawRef),
451
+ relation: traceRelationForLabel(labelKey),
452
+ evidence: labelKey === 'trace.evidence_refs' ? [rawRef] : [],
453
+ status: item.traceStatus || 'pending',
454
+ source: 'author',
455
+ confidence: rawRef.includes('#') ? 'medium' : 'high',
456
+ sourceWorkId: item.id,
457
+ sourceLabel: labelKey,
458
+ sourceRef: rawRef,
459
+ })),
460
+ ),
461
+ );
462
+ }
463
+
464
+ export function scanReverseTraceMarkers(fileContentsByPath) {
465
+ const files = normalizeFileMap(fileContentsByPath);
466
+ const markers = [];
467
+
468
+ for (const [path, content] of files) {
469
+ let match;
470
+ REVERSE_MARKER_PATTERN.lastIndex = 0;
471
+ while ((match = REVERSE_MARKER_PATTERN.exec(content)) !== null) {
472
+ const rawRef = match[2] ?? legacyIohascIdToRef(match[1]);
473
+ if (rawRef === '') {
474
+ continue;
475
+ }
476
+
477
+ markers.push({
478
+ ref: rawRef,
479
+ endpoint: parseReverseMarkerEndpoint(rawRef),
480
+ sourcePath: path,
481
+ line: lineNumberAt(content, match.index),
482
+ });
483
+ }
484
+ }
485
+
486
+ return markers.sort((left, right) => compareText(`${left.sourcePath}\0${left.line}\0${left.ref}`, `${right.sourcePath}\0${right.line}\0${right.ref}`));
487
+ }
488
+
489
+ export function validateTraceLinksV1(items, options = {}) {
490
+ assertItems(items);
491
+
492
+ const workIds = new Set(items.map((item) => item.id));
493
+ const atomIds = new Set(items.map((item) => item.labels.guid).filter(Boolean));
494
+ const evidenceIds = new Set([
495
+ ...items.flatMap((item) => item.evidence.map((_, index) => `${item.id}:legacy-evidence:${index + 1}`)),
496
+ ...normalizeStringList(options.evidenceIds),
497
+ ]);
498
+ const filePaths = new Set([
499
+ ...normalizeStringList(options.filePaths).map(normalizeTracePath),
500
+ ...normalizeFileMap(options.fileContentsByPath).map(([path]) => path),
501
+ ]);
502
+ const links = options.traceLinks ?? parseTraceLinksV1(items);
503
+ const markers = options.reverseMarkers ?? scanReverseTraceMarkers(options.fileContentsByPath ?? {});
504
+ const diagnostics = [];
505
+
506
+ for (const link of links) {
507
+ const diagnostic = validateTraceEndpoint(link, { workIds, atomIds, evidenceIds, filePaths });
508
+ if (diagnostic !== null) {
509
+ diagnostics.push(diagnostic);
510
+ }
511
+ }
512
+
513
+ for (const marker of markers) {
514
+ const diagnostic = validateReverseMarker(marker, { workIds, atomIds, evidenceIds, traceIds: new Set(links.map((link) => link.id)) });
515
+ if (diagnostic !== null) {
516
+ diagnostics.push(diagnostic);
517
+ }
518
+ }
519
+
520
+ for (const item of stableItems(items)) {
521
+ if (DONE_STATUSES.has(item.status) && item.targetFiles.length > 0 && !hasAuthoredTraceLinks(item)) {
522
+ diagnostics.push(traceDiagnostic({
523
+ severity: 'warning',
524
+ code: 'trace.weak_target_files_only',
525
+ message: `WorkItem ${item.id} has target_files but no Trace Links v1 labels.`,
526
+ source: { workId: item.id, label: 'work.target_files' },
527
+ actionable: `Add trace.code_refs or trace.artifact_refs for ${item.targetFiles[0]}.`,
528
+ }));
529
+ }
530
+ }
531
+
532
+ return diagnostics.sort((left, right) => compareText(`${left.severity}\0${left.code}\0${left.message}`, `${right.severity}\0${right.code}\0${right.message}`));
533
+ }
534
+
535
+ function isInlineSectionHeading(line) {
536
+ return /^[A-Za-zА-Яа-яЁё0-9][^:\n]{0,80}:$/u.test(line)
537
+ || /^[A-Za-zА-Яа-яЁё0-9][^:\n]{0,40}\s\/\s[^:\n]{0,40}:$/u.test(line);
538
+ }
539
+
540
+ function parseSections(body) {
541
+ const result = {
542
+ labels: {},
543
+ basis: [],
544
+ vector: [],
545
+ goal: [],
546
+ analysis: [],
547
+ decision: [],
548
+ uiRefs: [],
549
+ evidence: [],
550
+ checks: [],
551
+ };
552
+ let section = '';
553
+
554
+ for (const rawLine of body.split(/\r?\n/u)) {
555
+ const line = rawLine.trim();
556
+ if (line === '') {
557
+ continue;
558
+ }
559
+
560
+ if (line === 'Базис:') {
561
+ if (section === 'analysis' || section === 'decision') {
562
+ result[section].push(stripListMarker(line));
563
+ } else {
564
+ section = 'basis';
565
+ }
566
+ continue;
567
+ }
568
+
569
+ if (line === 'Вектор:') {
570
+ if (section === 'analysis' || section === 'decision') {
571
+ result[section].push(stripListMarker(line));
572
+ } else {
573
+ section = 'vector';
574
+ }
575
+ continue;
576
+ }
577
+
578
+ if (line === 'Цель:') {
579
+ if (section === 'analysis' || section === 'decision') {
580
+ result[section].push(stripListMarker(line));
581
+ } else {
582
+ section = 'goal';
583
+ }
584
+ continue;
585
+ }
586
+
587
+ if (line === 'Анализ:') {
588
+ section = 'analysis';
589
+ continue;
590
+ }
591
+
592
+ if (line === 'Решение:') {
593
+ section = 'decision';
594
+ continue;
595
+ }
596
+
597
+ if (line === 'Референсы_UI:') {
598
+ section = 'uiRefs';
599
+ continue;
600
+ }
601
+
602
+ if (line === 'Метки:') {
603
+ section = 'labels';
604
+ continue;
605
+ }
606
+
607
+ if (line === 'Свидетельства:') {
608
+ section = 'evidence';
609
+ continue;
610
+ }
611
+
612
+ if (line === 'критерии_готовности:' || line === 'Проверки:') {
613
+ section = 'checks';
614
+ continue;
615
+ }
616
+
617
+ if (section === 'basis' || section === 'vector' || section === 'goal') {
618
+ if (isInlineSectionHeading(line)) {
619
+ section = '';
620
+ continue;
621
+ }
622
+ result[section].push(stripListMarker(line));
623
+ continue;
624
+ }
625
+
626
+ if (section === 'analysis' || section === 'decision' || section === 'uiRefs') {
627
+ result[section].push(stripListMarker(line));
628
+ continue;
629
+ }
630
+
631
+ if (isInlineSectionHeading(line)) {
632
+ section = '';
633
+ continue;
634
+ }
635
+
636
+ if (section === 'labels') {
637
+ const separatorIndex = line.indexOf(':');
638
+ if (separatorIndex === -1) {
639
+ continue;
640
+ }
641
+
642
+ const key = line.slice(0, separatorIndex).trim();
643
+ const value = line.slice(separatorIndex + 1).trim();
644
+ result.labels[key] = value;
645
+ continue;
646
+ }
647
+
648
+ if (section === 'evidence') {
649
+ result.evidence.push(stripListMarker(line));
650
+ continue;
651
+ }
652
+
653
+ if (section === 'checks') {
654
+ result.checks.push(stripListMarker(line));
655
+ }
656
+ }
657
+
658
+ return result;
659
+ }
660
+
661
+ function buildEdges(items) {
662
+ return items
663
+ .flatMap((item) =>
664
+ item.dependsOn.map((dependencyId) => ({
665
+ from: dependencyId,
666
+ to: item.id,
667
+ type: 'depends_on',
668
+ })),
669
+ )
670
+ .sort((left, right) => compareText(`${left.from}\0${left.to}`, `${right.from}\0${right.to}`));
671
+ }
672
+
673
+ function buildStatusCounts(items) {
674
+ const entries = [];
675
+ for (const status of [...ALLOWED_STATUSES].sort(compareText)) {
676
+ const count = items.filter((item) => item.status === status).length;
677
+ if (count > 0) {
678
+ entries.push([status, count]);
679
+ }
680
+ }
681
+
682
+ return Object.fromEntries(entries);
683
+ }
684
+
685
+ function buildDashboardStatusCounts(items) {
686
+ return Object.fromEntries(
687
+ [...items.reduce((counts, item) => {
688
+ counts.set(item.status, (counts.get(item.status) ?? 0) + 1);
689
+ return counts;
690
+ }, new Map())].sort(([left], [right]) => compareText(left, right)),
691
+ );
692
+ }
693
+
694
+ function buildRecentEvidence(items, limit = 10) {
695
+ return items
696
+ .flatMap((item) =>
697
+ item.evidence.map((summary, index) => ({
698
+ id: `${item.id}:legacy-evidence:${index + 1}`,
699
+ taskId: item.id,
700
+ taskTitle: item.title,
701
+ type: 'legacy-evidence',
702
+ source: 'workgraph.snapshot.v1',
703
+ status: item.traceStatus === 'verified' ? 'succeeded' : 'pending',
704
+ summary,
705
+ })),
706
+ )
707
+ .slice(0, Number.isInteger(limit) && limit >= 0 ? limit : 10);
708
+ }
709
+
710
+ function parseTraceEndpoint(labelKey, rawRef) {
711
+ if (labelKey === 'trace.evidence_refs') {
712
+ return { kind: 'evidence', id: rawRef };
713
+ }
714
+
715
+ if (labelKey === 'trace.links') {
716
+ return parseReverseMarkerEndpoint(rawRef);
717
+ }
718
+
719
+ const rangeMatch = RANGE_LOCATOR_PATTERN.exec(rawRef);
720
+ if (rangeMatch !== null) {
721
+ const [, path, startLine, endLine] = rangeMatch;
722
+ return {
723
+ kind: 'range',
724
+ id: rawRef,
725
+ locator: {
726
+ path: normalizeTracePath(path),
727
+ startLine: Number(startLine),
728
+ endLine: Number(endLine ?? startLine),
729
+ },
730
+ };
731
+ }
732
+
733
+ const hashIndex = rawRef.indexOf('#');
734
+ if (hashIndex !== -1) {
735
+ const path = normalizeTracePath(rawRef.slice(0, hashIndex));
736
+ const symbol = rawRef.slice(hashIndex + 1).trim();
737
+ return { kind: 'symbol', id: rawRef, locator: { path, symbol } };
738
+ }
739
+
740
+ return { kind: 'file', id: normalizeTracePath(rawRef), locator: { path: normalizeTracePath(rawRef) } };
741
+ }
742
+
743
+ function parseReverseMarkerEndpoint(ref) {
744
+ const separatorIndex = ref.indexOf(':');
745
+ if (separatorIndex === -1) {
746
+ return { kind: 'marker', id: ref };
747
+ }
748
+
749
+ return {
750
+ kind: ref.slice(0, separatorIndex).trim(),
751
+ id: ref.slice(separatorIndex + 1).trim(),
752
+ };
753
+ }
754
+
755
+ function traceRelationForLabel(labelKey) {
756
+ if (labelKey === 'trace.evidence_refs') {
757
+ return 'verifies';
758
+ }
759
+
760
+ if (labelKey === 'trace.links') {
761
+ return 'references';
762
+ }
763
+
764
+ return 'references';
765
+ }
766
+
767
+ function validateTraceEndpoint(link, context) {
768
+ const endpoint = link.to;
769
+
770
+ if (endpoint.kind === 'file' && !context.filePaths.has(endpoint.id)) {
771
+ return traceDiagnostic({
772
+ severity: 'error',
773
+ code: 'trace.broken_file_ref',
774
+ message: `Trace link from ${link.sourceWorkId} references missing file ${endpoint.id}.`,
775
+ source: { workId: link.sourceWorkId, label: link.sourceLabel, ref: link.sourceRef },
776
+ actionable: `Update ${link.sourceLabel} or add ${endpoint.id} to the provided file list.`,
777
+ });
778
+ }
779
+
780
+ if ((endpoint.kind === 'symbol' || endpoint.kind === 'range') && !context.filePaths.has(endpoint.locator.path)) {
781
+ return traceDiagnostic({
782
+ severity: 'error',
783
+ code: 'trace.broken_file_ref',
784
+ message: `Trace link from ${link.sourceWorkId} references missing file ${endpoint.locator.path}.`,
785
+ source: { workId: link.sourceWorkId, label: link.sourceLabel, ref: link.sourceRef },
786
+ actionable: `Update ${link.sourceLabel} or add ${endpoint.locator.path} to the provided file list.`,
787
+ });
788
+ }
789
+
790
+ if (endpoint.kind === 'evidence' && context.evidenceIds.size > 0 && !context.evidenceIds.has(endpoint.id)) {
791
+ return traceDiagnostic({
792
+ severity: 'warning',
793
+ code: 'trace.broken_evidence_ref',
794
+ message: `Trace link from ${link.sourceWorkId} references unknown evidence ${endpoint.id}.`,
795
+ source: { workId: link.sourceWorkId, label: link.sourceLabel, ref: link.sourceRef },
796
+ actionable: `Record evidence ${endpoint.id} or update ${link.sourceLabel}.`,
797
+ });
798
+ }
799
+
800
+ return null;
801
+ }
802
+
803
+ function validateReverseMarker(marker, context) {
804
+ const { endpoint } = marker;
805
+ const knownByKind = {
806
+ work: context.workIds,
807
+ atom: context.atomIds,
808
+ evidence: context.evidenceIds,
809
+ trace: context.traceIds,
810
+ };
811
+ const knownIds = knownByKind[endpoint.kind];
812
+
813
+ if (knownIds === undefined || knownIds.has(endpoint.id)) {
814
+ return null;
815
+ }
816
+
817
+ return traceDiagnostic({
818
+ severity: 'error',
819
+ code: 'trace.orphan_reverse_marker',
820
+ message: `Reverse marker ${marker.ref} in ${marker.sourcePath} points to unknown ${endpoint.kind} id ${endpoint.id}.`,
821
+ source: { path: marker.sourcePath, line: marker.line, ref: marker.ref },
822
+ actionable: `Update or remove the marker in ${marker.sourcePath}.`,
823
+ });
824
+ }
825
+
826
+ function traceDiagnostic({ severity, code, message, source, actionable }) {
827
+ return { severity, code, message, source, actionable };
828
+ }
829
+
830
+ function hasAuthoredTraceLinks(item) {
831
+ return TRACE_LABEL_KEYS.some((labelKey) => parseList(item.labels[labelKey]).length > 0);
832
+ }
833
+
834
+ function normalizeFileMap(value) {
835
+ if (value === undefined || value === null) {
836
+ return [];
837
+ }
838
+
839
+ if (value instanceof Map) {
840
+ return [...value.entries()].map(([path, content]) => [normalizeTracePath(path), String(content ?? '')]);
841
+ }
842
+
843
+ if (Array.isArray(value)) {
844
+ return value.map(([path, content]) => [normalizeTracePath(path), String(content ?? '')]);
845
+ }
846
+
847
+ if (typeof value === 'object') {
848
+ return Object.entries(value).map(([path, content]) => [normalizeTracePath(path), String(content ?? '')]);
849
+ }
850
+
851
+ throw new TypeError('fileContentsByPath must be an object, Map, or entry array');
852
+ }
853
+
854
+ function normalizeTracePath(path) {
855
+ return String(path ?? '').replace(/\\/gu, '/').replace(/^\.\//u, '').trim();
856
+ }
857
+
858
+ function normalizeStringList(value) {
859
+ if (value === undefined || value === null) {
860
+ return [];
861
+ }
862
+
863
+ if (!Array.isArray(value)) {
864
+ throw new TypeError('value must be an array');
865
+ }
866
+
867
+ return value.map((item) => String(item).trim()).filter(Boolean);
868
+ }
869
+
870
+ function legacyIohascIdToRef(ref) {
871
+ const normalized = String(ref ?? '').trim();
872
+ return normalized.startsWith('step:') ? `atom:${normalized.slice('step:'.length)}` : normalized;
873
+ }
874
+
875
+ function lineNumberAt(text, index) {
876
+ return text.slice(0, index).split(/\r?\n/u).length;
877
+ }
878
+
879
+ function buildViewCounts(items) {
880
+ return {
881
+ board: items.filter((item) => item.status !== 'backlog').length,
882
+ backlog: items.filter((item) => item.status === 'backlog').length,
883
+ current: items.filter((item) => ['claimed', 'doing', 'in_progress', 'verify'].includes(item.status)).length,
884
+ blocked: items.filter((item) => item.status === 'blocked' || item.blocker).length,
885
+ };
886
+ }
887
+
888
+ function toDashboardTaskSummary(item) {
889
+ return {
890
+ id: item.id,
891
+ key: item.key,
892
+ title: item.title,
893
+ status: item.status,
894
+ ownerRole: item.ownerRole,
895
+ department: item.department,
896
+ priority: item.priority,
897
+ risk: item.risk,
898
+ traceStatus: item.traceStatus,
899
+ nextAction: item.nextAction,
900
+ targetFiles: [...item.targetFiles],
901
+ };
902
+ }
903
+
904
+ function normalizeProjectionList(value) {
905
+ return Array.isArray(value) ? value : [];
906
+ }
907
+
908
+ function normalizeStatus(status) {
909
+ const normalizedStatus = String(status ?? 'backlog').trim();
910
+ return normalizedStatus === '' ? 'backlog' : normalizedStatus;
911
+ }
912
+
913
+ function parseList(value) {
914
+ if (typeof value !== 'string' || value.trim() === '') {
915
+ return [];
916
+ }
917
+
918
+ return value
919
+ .split(LIST_SEPARATOR_PATTERN)
920
+ .map((item) => item.trim())
921
+ .filter(Boolean)
922
+ .sort(compareText);
923
+ }
924
+
925
+ function stableItems(items) {
926
+ return [...items].sort((left, right) => compareText(left.id, right.id));
927
+ }
928
+
929
+ function hasEvidence(item, options) {
930
+ return item.evidence.length > 0 || normalizeOptionalText(options.evidence) !== '';
931
+ }
932
+
933
+ function hasBlockerReason(options) {
934
+ return normalizeOptionalText(options.reason) !== '' || normalizeOptionalText(options.blocker) !== '';
935
+ }
936
+
937
+ function normalizeEvidence(evidence) {
938
+ if (typeof evidence === 'string') {
939
+ const trimmed = evidence.trim();
940
+ if (trimmed === '') {
941
+ throw new WorkGraphPolicyError('evidence must be non-empty');
942
+ }
943
+
944
+ return trimmed;
945
+ }
946
+
947
+ if (evidence && typeof evidence === 'object') {
948
+ return JSON.stringify(sortObject(evidence));
949
+ }
950
+
951
+ throw new WorkGraphPolicyError('evidence must be a non-empty string or object');
952
+ }
953
+
954
+ function normalizeOptionalText(value) {
955
+ return value === undefined ? '' : String(value).trim();
956
+ }
957
+
958
+ function stripListMarker(line) {
959
+ return line.replace(/^-\s*/u, '').trim();
960
+ }
961
+
962
+ function normalizeSectionText(lines) {
963
+ return lines.map((line) => line.trim()).filter(Boolean).join('\n');
964
+ }
965
+
966
+ function sortObject(value) {
967
+ if (Array.isArray(value)) {
968
+ return value.map(sortObject);
969
+ }
970
+
971
+ if (value && typeof value === 'object') {
972
+ return Object.fromEntries(
973
+ Object.keys(value)
974
+ .sort(compareText)
975
+ .map((key) => [key, sortObject(value[key])]),
976
+ );
977
+ }
978
+
979
+ return value;
980
+ }
981
+
982
+ function assertItems(items) {
983
+ if (!Array.isArray(items)) {
984
+ throw new TypeError('items must be an array');
985
+ }
986
+
987
+ items.forEach(assertItem);
988
+ }
989
+
990
+ function assertWorkGraphSnapshot(snapshot) {
991
+ if (!snapshot || typeof snapshot !== 'object' || !Array.isArray(snapshot.items)) {
992
+ throw new TypeError('snapshot must be a Work Graph snapshot');
993
+ }
994
+ }
995
+
996
+ function assertItem(item) {
997
+ if (!item || typeof item !== 'object' || typeof item.id !== 'string') {
998
+ throw new TypeError('item must be a parsed WorkItem');
999
+ }
1000
+
1001
+ if (!Array.isArray(item.dependsOn) || !Array.isArray(item.evidence) || !Array.isArray(item.checks)) {
1002
+ throw new TypeError('item must be a parsed WorkItem');
1003
+ }
1004
+ }
1005
+
1006
+ function compareText(left, right) {
1007
+ return left.localeCompare(right, 'en', { sensitivity: 'variant' });
1008
+ }