@lota-sdk/core 0.4.7 → 0.4.9

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 (259) hide show
  1. package/package.json +11 -12
  2. package/src/ai/embedding-cache.ts +94 -22
  3. package/src/ai-gateway/ai-gateway.ts +738 -223
  4. package/src/config/agent-defaults.ts +176 -75
  5. package/src/config/agent-types.ts +54 -4
  6. package/src/config/constants.ts +8 -2
  7. package/src/config/logger.ts +286 -19
  8. package/src/config/model-constants.ts +1 -0
  9. package/src/config/thread-defaults.ts +33 -21
  10. package/src/create-runtime.ts +725 -383
  11. package/src/db/base.service.ts +52 -28
  12. package/src/db/cursor-pagination.ts +71 -30
  13. package/src/db/memory-store.helpers.ts +4 -7
  14. package/src/db/memory-store.ts +856 -598
  15. package/src/db/memory.ts +398 -275
  16. package/src/db/record-id.ts +32 -10
  17. package/src/db/schema-fingerprint.ts +30 -12
  18. package/src/db/service-normalization.ts +255 -0
  19. package/src/db/service.ts +726 -761
  20. package/src/db/startup.ts +140 -66
  21. package/src/db/transaction-conflict.ts +15 -0
  22. package/src/effect/awaitable-effect.ts +87 -0
  23. package/src/effect/errors.ts +121 -0
  24. package/src/effect/helpers.ts +98 -0
  25. package/src/effect/index.ts +22 -0
  26. package/src/effect/layers.ts +228 -0
  27. package/src/effect/runtime-ref.ts +25 -0
  28. package/src/effect/runtime.ts +31 -0
  29. package/src/effect/services.ts +57 -0
  30. package/src/effect/zod.ts +43 -0
  31. package/src/embeddings/provider.ts +122 -71
  32. package/src/index.ts +46 -1
  33. package/src/openrouter/direct-provider.ts +29 -0
  34. package/src/queues/autonomous-job.queue.ts +130 -74
  35. package/src/queues/context-compaction.queue.ts +60 -15
  36. package/src/queues/delayed-node-promotion.queue.ts +52 -15
  37. package/src/queues/document-processor.queue.ts +52 -77
  38. package/src/queues/memory-consolidation.queue.ts +47 -32
  39. package/src/queues/organization-learning.queue.ts +13 -4
  40. package/src/queues/plan-agent-heartbeat.queue.ts +65 -21
  41. package/src/queues/plan-scheduler.queue.ts +107 -31
  42. package/src/queues/post-chat-memory.queue.ts +66 -24
  43. package/src/queues/queue-factory.ts +142 -52
  44. package/src/queues/standalone-worker.ts +39 -0
  45. package/src/queues/title-generation.queue.ts +54 -9
  46. package/src/redis/connection.ts +84 -32
  47. package/src/redis/index.ts +6 -8
  48. package/src/redis/org-memory-lock.ts +60 -27
  49. package/src/redis/redis-lease-lock.ts +200 -121
  50. package/src/redis/runtime-connection.ts +10 -0
  51. package/src/redis/stream-context.ts +84 -46
  52. package/src/runtime/agent-identity-overrides.ts +2 -2
  53. package/src/runtime/agent-runtime-policy.ts +4 -1
  54. package/src/runtime/agent-stream-helpers.ts +20 -9
  55. package/src/runtime/chat-run-orchestration.ts +102 -19
  56. package/src/runtime/chat-run-registry.ts +36 -2
  57. package/src/runtime/context-compaction/context-compaction-runtime.ts +107 -0
  58. package/src/runtime/{context-compaction.ts → context-compaction/context-compaction.ts} +114 -91
  59. package/src/runtime/execution-plan-visibility.ts +2 -2
  60. package/src/runtime/execution-plan.ts +42 -15
  61. package/src/runtime/graph-designer.ts +11 -7
  62. package/src/runtime/helper-model.ts +135 -48
  63. package/src/runtime/index.ts +7 -7
  64. package/src/runtime/indexed-repositories-policy.ts +3 -3
  65. package/src/runtime/{memory-block.ts → memory/memory-block.ts} +40 -36
  66. package/src/runtime/{memory-digest-policy.ts → memory/memory-digest-policy.ts} +1 -1
  67. package/src/runtime/{memory-pipeline.ts → memory/memory-pipeline.ts} +1 -1
  68. package/src/runtime/{memory-prompts-fact.ts → memory/memory-prompts-fact.ts} +2 -2
  69. package/src/runtime/{memory-scope.ts → memory/memory-scope.ts} +12 -6
  70. package/src/runtime/plugin-resolution.ts +144 -24
  71. package/src/runtime/plugin-types.ts +9 -1
  72. package/src/runtime/post-turn-side-effects.ts +197 -130
  73. package/src/runtime/retrieval-adapters.ts +38 -4
  74. package/src/runtime/runtime-config.ts +165 -61
  75. package/src/runtime/runtime-extensions.ts +21 -34
  76. package/src/runtime/social-chat/social-chat-agent-runner.ts +157 -0
  77. package/src/runtime/{social-chat-history.ts → social-chat/social-chat-history.ts} +42 -20
  78. package/src/runtime/social-chat/social-chat.ts +594 -0
  79. package/src/runtime/specialist-runner.ts +36 -10
  80. package/src/runtime/team-consultation/team-consultation-orchestrator.ts +427 -0
  81. package/src/runtime/{team-consultation-prompts.ts → team-consultation/team-consultation-prompts.ts} +6 -2
  82. package/src/runtime/thread-chat-helpers.ts +2 -2
  83. package/src/runtime/thread-plan-turn.ts +2 -1
  84. package/src/runtime/thread-turn-context.ts +172 -94
  85. package/src/runtime/turn-lifecycle.ts +93 -27
  86. package/src/services/agent-activity.service.ts +287 -203
  87. package/src/services/agent-executor.service.ts +329 -217
  88. package/src/services/artifact.service.ts +225 -148
  89. package/src/services/attachment.service.ts +137 -115
  90. package/src/services/autonomous-job.service.ts +888 -491
  91. package/src/services/chat-run-registry.service.ts +11 -1
  92. package/src/services/context-compaction.service.ts +136 -86
  93. package/src/services/document-chunk.service.ts +162 -90
  94. package/src/services/execution-plan/execution-plan-approval.ts +26 -0
  95. package/src/services/execution-plan/execution-plan-context.ts +29 -0
  96. package/src/services/execution-plan/execution-plan-graph.ts +256 -0
  97. package/src/services/execution-plan/execution-plan-schedule.ts +84 -0
  98. package/src/services/execution-plan/execution-plan-spec.ts +75 -0
  99. package/src/services/execution-plan/execution-plan.service.ts +1041 -0
  100. package/src/services/feedback-loop.service.ts +132 -76
  101. package/src/services/global-orchestrator.service.ts +80 -170
  102. package/src/services/graph-full-routing.ts +182 -0
  103. package/src/services/index.ts +18 -20
  104. package/src/services/institutional-memory.service.ts +220 -123
  105. package/src/services/learned-skill.service.ts +364 -259
  106. package/src/services/memory/memory-conversation.ts +95 -0
  107. package/src/services/memory/memory-org-memory.ts +39 -0
  108. package/src/services/memory/memory-preseeded.ts +80 -0
  109. package/src/services/memory/memory-rerank.ts +297 -0
  110. package/src/services/{memory-utils.ts → memory/memory-utils.ts} +5 -5
  111. package/src/services/memory/memory.service.ts +692 -0
  112. package/src/services/memory/rerank.service.ts +209 -0
  113. package/src/services/monitoring-window.service.ts +92 -70
  114. package/src/services/mutating-approval.service.ts +62 -53
  115. package/src/services/node-workspace.service.ts +141 -98
  116. package/src/services/notification.service.ts +17 -16
  117. package/src/services/organization-member.service.ts +120 -66
  118. package/src/services/organization.service.ts +144 -51
  119. package/src/services/ownership-dispatcher.service.ts +415 -264
  120. package/src/services/plan/plan-agent-heartbeat.service.ts +234 -0
  121. package/src/services/plan/plan-agent-query.service.ts +322 -0
  122. package/src/services/plan/plan-approval.service.ts +102 -0
  123. package/src/services/plan/plan-artifact.service.ts +60 -0
  124. package/src/services/plan/plan-builder.service.ts +76 -0
  125. package/src/services/plan/plan-checkpoint.service.ts +103 -0
  126. package/src/services/{plan-compiler.service.ts → plan/plan-compiler.service.ts} +26 -9
  127. package/src/services/plan/plan-completion-side-effects.ts +175 -0
  128. package/src/services/plan/plan-coordination.service.ts +181 -0
  129. package/src/services/plan/plan-cycle.service.ts +398 -0
  130. package/src/services/plan/plan-deadline.service.ts +547 -0
  131. package/src/services/plan/plan-event-delivery.service.ts +261 -0
  132. package/src/services/plan/plan-executor-context.ts +35 -0
  133. package/src/services/plan/plan-executor-graph.ts +475 -0
  134. package/src/services/plan/plan-executor-helpers.ts +322 -0
  135. package/src/services/plan/plan-executor-persistence.ts +209 -0
  136. package/src/services/plan/plan-executor.service.ts +1654 -0
  137. package/src/services/{plan-helpers.ts → plan/plan-helpers.ts} +1 -1
  138. package/src/services/{plan-run-data.ts → plan/plan-run-data.ts} +4 -4
  139. package/src/services/plan/plan-run-serialization.ts +15 -0
  140. package/src/services/plan/plan-run.service.ts +644 -0
  141. package/src/services/plan/plan-scheduler.service.ts +385 -0
  142. package/src/services/plan/plan-template.service.ts +224 -0
  143. package/src/services/plan/plan-transaction-events.ts +33 -0
  144. package/src/services/plan/plan-validator.service.ts +907 -0
  145. package/src/services/plan/plan-workspace.service.ts +125 -0
  146. package/src/services/plugin-executor.service.ts +97 -68
  147. package/src/services/quality-metrics.service.ts +112 -94
  148. package/src/services/queue-job.service.ts +296 -230
  149. package/src/services/recent-activity-title.service.ts +65 -36
  150. package/src/services/recent-activity.service.ts +274 -259
  151. package/src/services/skill-resolver.service.ts +38 -12
  152. package/src/services/social-chat-history.service.ts +176 -125
  153. package/src/services/system-executor.service.ts +91 -61
  154. package/src/services/thread/thread-active-run.ts +203 -0
  155. package/src/services/thread/thread-bootstrap.ts +369 -0
  156. package/src/services/thread/thread-listing.ts +198 -0
  157. package/src/services/thread/thread-memory-block.ts +117 -0
  158. package/src/services/thread/thread-message.service.ts +363 -0
  159. package/src/services/thread/thread-record-store.ts +155 -0
  160. package/src/services/thread/thread-title.service.ts +74 -0
  161. package/src/services/thread/thread-turn-execution.ts +280 -0
  162. package/src/services/thread/thread-turn-message-context.ts +73 -0
  163. package/src/services/thread/thread-turn-preparation.service.ts +1146 -0
  164. package/src/services/thread/thread-turn-streaming.ts +402 -0
  165. package/src/services/thread/thread-turn-tracing.ts +35 -0
  166. package/src/services/thread/thread-turn.ts +343 -0
  167. package/src/services/thread/thread.service.ts +335 -0
  168. package/src/services/user.service.ts +82 -32
  169. package/src/services/write-intent-validator.service.ts +63 -51
  170. package/src/storage/attachment-parser.ts +69 -27
  171. package/src/storage/attachment-storage.service.ts +331 -275
  172. package/src/storage/generated-document-storage.service.ts +66 -34
  173. package/src/system-agents/agent-result.ts +3 -1
  174. package/src/system-agents/context-compaction.agent.ts +2 -2
  175. package/src/system-agents/delegated-agent-factory.ts +159 -90
  176. package/src/system-agents/memory-reranker.agent.ts +2 -2
  177. package/src/system-agents/memory.agent.ts +2 -2
  178. package/src/system-agents/recent-activity-title-refiner.agent.ts +2 -2
  179. package/src/system-agents/regular-chat-memory-digest.agent.ts +2 -2
  180. package/src/system-agents/skill-extractor.agent.ts +2 -2
  181. package/src/system-agents/skill-manager.agent.ts +2 -2
  182. package/src/system-agents/thread-router.agent.ts +157 -113
  183. package/src/system-agents/title-generator.agent.ts +2 -2
  184. package/src/tools/execution-plan.tool.ts +220 -161
  185. package/src/tools/fetch-webpage.tool.ts +21 -17
  186. package/src/tools/firecrawl-client.ts +16 -6
  187. package/src/tools/index.ts +1 -0
  188. package/src/tools/memory-block.tool.ts +14 -6
  189. package/src/tools/plan-approval.tool.ts +49 -47
  190. package/src/tools/read-file-parts.tool.ts +44 -33
  191. package/src/tools/remember-memory.tool.ts +65 -45
  192. package/src/tools/search-web.tool.ts +26 -22
  193. package/src/tools/search.tool.ts +41 -29
  194. package/src/tools/team-think.tool.ts +124 -83
  195. package/src/tools/user-questions.tool.ts +4 -3
  196. package/src/tools/web-tool-shared.ts +6 -0
  197. package/src/utils/async.ts +17 -23
  198. package/src/utils/crypto.ts +21 -0
  199. package/src/utils/date-time.ts +40 -1
  200. package/src/utils/errors.ts +95 -16
  201. package/src/utils/hono-error-handler.ts +24 -39
  202. package/src/utils/index.ts +2 -1
  203. package/src/utils/null-proto-record.ts +41 -0
  204. package/src/utils/sse-keepalive.ts +124 -21
  205. package/src/workers/bootstrap.ts +186 -51
  206. package/src/workers/memory-consolidation.worker.ts +325 -237
  207. package/src/workers/organization-learning.worker.ts +50 -16
  208. package/src/workers/regular-chat-memory-digest.helpers.ts +28 -27
  209. package/src/workers/regular-chat-memory-digest.runner.ts +175 -114
  210. package/src/workers/skill-extraction.runner.ts +176 -93
  211. package/src/workers/utils/file-section-chunker.ts +8 -10
  212. package/src/workers/utils/repo-structure-extractor.ts +349 -260
  213. package/src/workers/utils/repomix-file-sections.ts +2 -2
  214. package/src/workers/utils/thread-message-query.ts +97 -38
  215. package/src/workers/worker-utils.ts +56 -31
  216. package/src/config/debug-logger.ts +0 -47
  217. package/src/redis/connection-accessor.ts +0 -26
  218. package/src/runtime/context-compaction-runtime.ts +0 -87
  219. package/src/runtime/social-chat-agent-runner.ts +0 -118
  220. package/src/runtime/social-chat.ts +0 -516
  221. package/src/runtime/team-consultation-orchestrator.ts +0 -272
  222. package/src/services/adaptive-playbook.service.ts +0 -152
  223. package/src/services/artifact-provenance.service.ts +0 -172
  224. package/src/services/chat-attachments.service.ts +0 -17
  225. package/src/services/context-compaction-runtime.singleton.ts +0 -13
  226. package/src/services/execution-plan.service.ts +0 -1118
  227. package/src/services/memory.service.ts +0 -844
  228. package/src/services/plan-agent-heartbeat.service.ts +0 -136
  229. package/src/services/plan-agent-query.service.ts +0 -267
  230. package/src/services/plan-approval.service.ts +0 -83
  231. package/src/services/plan-artifact.service.ts +0 -50
  232. package/src/services/plan-builder.service.ts +0 -67
  233. package/src/services/plan-checkpoint.service.ts +0 -81
  234. package/src/services/plan-completion-side-effects.ts +0 -80
  235. package/src/services/plan-coordination.service.ts +0 -157
  236. package/src/services/plan-cycle.service.ts +0 -284
  237. package/src/services/plan-deadline.service.ts +0 -430
  238. package/src/services/plan-event-delivery.service.ts +0 -166
  239. package/src/services/plan-executor.service.ts +0 -1950
  240. package/src/services/plan-run.service.ts +0 -515
  241. package/src/services/plan-scheduler.service.ts +0 -240
  242. package/src/services/plan-template.service.ts +0 -177
  243. package/src/services/plan-validator.service.ts +0 -818
  244. package/src/services/plan-workspace.service.ts +0 -83
  245. package/src/services/thread-message.service.ts +0 -275
  246. package/src/services/thread-plan-registry.service.ts +0 -22
  247. package/src/services/thread-title.service.ts +0 -39
  248. package/src/services/thread-turn-preparation.service.ts +0 -1147
  249. package/src/services/thread-turn.ts +0 -172
  250. package/src/services/thread.service.ts +0 -869
  251. package/src/utils/env.ts +0 -8
  252. /package/src/runtime/{context-compaction-constants.ts → context-compaction/context-compaction-constants.ts} +0 -0
  253. /package/src/runtime/{memory-format.ts → memory/memory-format.ts} +0 -0
  254. /package/src/runtime/{memory-prompts-parse.ts → memory/memory-prompts-parse.ts} +0 -0
  255. /package/src/runtime/{memory-prompts-update.ts → memory/memory-prompts-update.ts} +0 -0
  256. /package/src/runtime/{social-chat-prompts.ts → social-chat/social-chat-prompts.ts} +0 -0
  257. /package/src/services/{plan-node-spec.ts → plan/plan-node-spec.ts} +0 -0
  258. /package/src/services/{thread-constants.ts → thread/thread-constants.ts} +0 -0
  259. /package/src/services/{thread.types.ts → thread/thread.types.ts} +0 -0
@@ -0,0 +1,907 @@
1
+ import type {
2
+ PlanCompletionCheck,
3
+ PlanDataSchema,
4
+ PlanDraft,
5
+ PlanFailureClass,
6
+ PlanNodeType,
7
+ PlanNodeResultSubmission,
8
+ PlanValidationIssueSeverity,
9
+ } from '@lota-sdk/shared'
10
+ import { HUMAN_NODE_TYPES, STRUCTURAL_NODE_TYPES } from '@lota-sdk/shared'
11
+ import { Context, Effect, Layer } from 'effect'
12
+
13
+ import { isRecord } from '../../utils/string'
14
+ import type { makePlanCoordinationService } from './plan-coordination.service'
15
+ import { PlanCoordinationServiceTag } from './plan-coordination.service'
16
+ import { readPathValue } from './plan-helpers'
17
+ import type { PlanNodeValidationSpec } from './plan-node-spec'
18
+
19
+ const STRUCTURAL_NODE_TYPE_SET = new Set<PlanNodeType>(STRUCTURAL_NODE_TYPES)
20
+ const HUMAN_NODE_TYPE_SET = new Set<PlanNodeType>(HUMAN_NODE_TYPES)
21
+
22
+ export interface PlanValidationIssueInput {
23
+ severity: PlanValidationIssueSeverity
24
+ code: string
25
+ message: string
26
+ detail?: Record<string, unknown>
27
+ nodeId?: string
28
+ }
29
+
30
+ interface DraftValidationResult {
31
+ blocking: PlanValidationIssueInput[]
32
+ warnings: PlanValidationIssueInput[]
33
+ }
34
+
35
+ interface NodeResultValidationResult {
36
+ blocking: PlanValidationIssueInput[]
37
+ warnings: PlanValidationIssueInput[]
38
+ failureClass: PlanFailureClass | null
39
+ }
40
+
41
+ function createIssue(params: {
42
+ code: string
43
+ message: string
44
+ severity?: PlanValidationIssueSeverity
45
+ detail?: Record<string, unknown>
46
+ nodeId?: string
47
+ }): PlanValidationIssueInput {
48
+ return {
49
+ severity: params.severity ?? 'blocking',
50
+ code: params.code,
51
+ message: params.message,
52
+ ...(params.detail ? { detail: params.detail } : {}),
53
+ ...(params.nodeId ? { nodeId: params.nodeId } : {}),
54
+ }
55
+ }
56
+
57
+ function hasAllFields(value: unknown, fields: string[]): boolean {
58
+ if (!isRecord(value)) return false
59
+ return fields.every((field) => readPathValue(value, field) !== undefined)
60
+ }
61
+
62
+ export function validateSchemaValue(params: { schema: PlanDataSchema; value: unknown; path: string }): string[] {
63
+ const issues: string[] = []
64
+ const { schema, value, path } = params
65
+
66
+ if (value === null || value === undefined) {
67
+ if (schema.required || (!schema.nullable && value === null)) {
68
+ issues.push(`${path} is required`)
69
+ }
70
+ return issues
71
+ }
72
+
73
+ if (schema.enum && !schema.enum.some((candidate) => Object.is(candidate, value))) {
74
+ issues.push(`${path} must be one of the allowed enum values`)
75
+ return issues
76
+ }
77
+
78
+ if (schema.type === 'string') {
79
+ if (typeof value !== 'string') issues.push(`${path} must be a string`)
80
+ return issues
81
+ }
82
+
83
+ if (schema.type === 'number') {
84
+ if (typeof value !== 'number' || Number.isNaN(value)) issues.push(`${path} must be a number`)
85
+ return issues
86
+ }
87
+
88
+ if (schema.type === 'boolean') {
89
+ if (typeof value !== 'boolean') issues.push(`${path} must be a boolean`)
90
+ return issues
91
+ }
92
+
93
+ if (schema.type === 'array') {
94
+ if (!Array.isArray(value)) {
95
+ issues.push(`${path} must be an array`)
96
+ return issues
97
+ }
98
+ if (schema.minItems !== undefined && value.length < schema.minItems) {
99
+ issues.push(`${path} must contain at least ${schema.minItems} item(s)`)
100
+ }
101
+ if (schema.maxItems !== undefined && value.length > schema.maxItems) {
102
+ issues.push(`${path} must contain at most ${schema.maxItems} item(s)`)
103
+ }
104
+ const itemSchema = schema.items
105
+ if (itemSchema) {
106
+ value.forEach((entry, index) => {
107
+ issues.push(...validateSchemaValue({ schema: itemSchema, value: entry, path: `${path}[${index}]` }))
108
+ })
109
+ }
110
+ return issues
111
+ }
112
+
113
+ if (!isRecord(value)) {
114
+ issues.push(`${path} must be an object`)
115
+ return issues
116
+ }
117
+
118
+ for (const [key, childSchema] of Object.entries(schema.properties ?? {})) {
119
+ issues.push(...validateSchemaValue({ schema: childSchema, value: value[key], path: `${path}.${key}` }))
120
+ }
121
+
122
+ return issues
123
+ }
124
+
125
+ function buildReachableNodeIds(entryNodeIds: string[], adjacency: Map<string, string[]>): Set<string> {
126
+ const visited = new Set<string>()
127
+ const queue = [...entryNodeIds]
128
+
129
+ while (queue.length > 0) {
130
+ const nodeId = queue.shift()
131
+ if (!nodeId || visited.has(nodeId)) continue
132
+ visited.add(nodeId)
133
+ for (const next of adjacency.get(nodeId) ?? []) {
134
+ if (!visited.has(next)) {
135
+ queue.push(next)
136
+ }
137
+ }
138
+ }
139
+
140
+ return visited
141
+ }
142
+
143
+ function graphHasCycle(nodeIds: string[], adjacency: Map<string, string[]>): boolean {
144
+ const visiting = new Set<string>()
145
+ const visited = new Set<string>()
146
+
147
+ const visit = (nodeId: string): boolean => {
148
+ if (visited.has(nodeId)) return false
149
+ if (visiting.has(nodeId)) return true
150
+
151
+ visiting.add(nodeId)
152
+ for (const next of adjacency.get(nodeId) ?? []) {
153
+ if (visit(next)) return true
154
+ }
155
+ visiting.delete(nodeId)
156
+ visited.add(nodeId)
157
+ return false
158
+ }
159
+
160
+ return nodeIds.some((nodeId) => visit(nodeId))
161
+ }
162
+
163
+ function resolveSchemaRef(
164
+ draft: Pick<PlanDraft, 'schemas'>,
165
+ schemaRef: string | null | undefined,
166
+ ): PlanDataSchema | undefined {
167
+ if (!schemaRef) return undefined
168
+ return draft.schemas[schemaRef]
169
+ }
170
+
171
+ function evaluateCompletionCheck(params: {
172
+ draft: Pick<PlanDraft, 'schemas'>
173
+ node: PlanNodeValidationSpec
174
+ result: PlanNodeResultSubmission
175
+ check: PlanCompletionCheck
176
+ }): PlanValidationIssueInput | null {
177
+ const { check, node, result, draft } = params
178
+ const artifactName = typeof check.config.artifact === 'string' ? check.config.artifact : undefined
179
+ const artifact = artifactName ? result.artifacts.find((candidate) => candidate.name === artifactName) : undefined
180
+ const target =
181
+ artifactName && artifact
182
+ ? artifact.payload
183
+ : typeof check.config.target === 'string'
184
+ ? readPathValue(result.structuredOutput, check.config.target)
185
+ : result.structuredOutput
186
+
187
+ if (check.type === 'schema') {
188
+ const schemaRef =
189
+ (typeof check.config.schemaRef === 'string' ? check.config.schemaRef : undefined) ??
190
+ (artifactName ? node.deliverables.find((candidate) => candidate.name === artifactName)?.schemaRef : undefined)
191
+ if (!schemaRef) {
192
+ return createIssue({
193
+ code: 'schema_check_missing_schema_ref',
194
+ message: `Completion check "${check.description}" does not resolve a schema reference.`,
195
+ nodeId: node.id,
196
+ severity: check.blocking ? 'blocking' : 'warning',
197
+ })
198
+ }
199
+
200
+ if (artifactName && !artifact) {
201
+ return createIssue({
202
+ code: 'schema_check_artifact_missing',
203
+ message: `Completion check "${check.description}" requires artifact "${artifactName}".`,
204
+ nodeId: node.id,
205
+ severity: check.blocking ? 'blocking' : 'warning',
206
+ })
207
+ }
208
+
209
+ const schema = resolveSchemaRef(draft, schemaRef)
210
+ if (!schema) {
211
+ return createIssue({
212
+ code: 'schema_check_schema_missing',
213
+ message: `Completion check "${check.description}" references unknown schema "${schemaRef}".`,
214
+ nodeId: node.id,
215
+ severity: check.blocking ? 'blocking' : 'warning',
216
+ })
217
+ }
218
+
219
+ const schemaIssues = validateSchemaValue({
220
+ schema,
221
+ value: target,
222
+ path: artifactName ? `artifact:${artifactName}` : 'structuredOutput',
223
+ })
224
+ if (schemaIssues.length > 0) {
225
+ return createIssue({
226
+ code: 'schema_validation_failed',
227
+ message: `Completion check "${check.description}" failed schema validation.`,
228
+ nodeId: node.id,
229
+ severity: check.blocking ? 'blocking' : 'warning',
230
+ detail: { issues: schemaIssues, schemaRef },
231
+ })
232
+ }
233
+
234
+ return null
235
+ }
236
+
237
+ if (check.type === 'assertion') {
238
+ const requiredFields = Array.isArray(check.config.mustContainFields)
239
+ ? check.config.mustContainFields.filter((field): field is string => typeof field === 'string')
240
+ : []
241
+ if (requiredFields.length > 0 && !hasAllFields(target, requiredFields)) {
242
+ return createIssue({
243
+ code: 'assertion_failed',
244
+ message: `Assertion "${check.description}" failed because required fields are missing.`,
245
+ nodeId: node.id,
246
+ severity: check.blocking ? 'blocking' : 'warning',
247
+ detail: { mustContainFields: requiredFields },
248
+ })
249
+ }
250
+
251
+ const equalityPath = typeof check.config.path === 'string' ? check.config.path : undefined
252
+ if (equalityPath && 'equals' in check.config) {
253
+ const actual = readPathValue(target, equalityPath)
254
+ if (!Object.is(actual, check.config.equals)) {
255
+ return createIssue({
256
+ code: 'assertion_failed',
257
+ message: `Assertion "${check.description}" failed at path "${equalityPath}".`,
258
+ nodeId: node.id,
259
+ severity: check.blocking ? 'blocking' : 'warning',
260
+ detail: { path: equalityPath, expected: check.config.equals, actual },
261
+ })
262
+ }
263
+ }
264
+
265
+ const truthyPaths = Array.isArray(check.config.truthyPaths)
266
+ ? check.config.truthyPaths.filter((entry): entry is string => typeof entry === 'string')
267
+ : []
268
+ if (truthyPaths.some((path) => !readPathValue(target, path))) {
269
+ return createIssue({
270
+ code: 'assertion_failed',
271
+ message: `Assertion "${check.description}" expected truthy values that were not present.`,
272
+ nodeId: node.id,
273
+ severity: check.blocking ? 'blocking' : 'warning',
274
+ detail: { truthyPaths },
275
+ })
276
+ }
277
+
278
+ return null
279
+ }
280
+
281
+ if (check.type === 'tool-check') {
282
+ const mode = typeof check.config.mode === 'string' ? check.config.mode : 'artifact-present'
283
+ if (mode === 'artifact-present' && artifactName && !artifact) {
284
+ return createIssue({
285
+ code: 'required_artifact_missing',
286
+ message: `Tool check "${check.description}" requires artifact "${artifactName}".`,
287
+ nodeId: node.id,
288
+ severity: check.blocking ? 'blocking' : 'warning',
289
+ })
290
+ }
291
+ if (
292
+ mode === 'artifact-kind' &&
293
+ artifactName &&
294
+ artifact &&
295
+ typeof check.config.kind === 'string' &&
296
+ artifact.kind !== check.config.kind
297
+ ) {
298
+ return createIssue({
299
+ code: 'artifact_kind_mismatch',
300
+ message: `Tool check "${check.description}" expected artifact "${artifactName}" to be kind "${check.config.kind}".`,
301
+ nodeId: node.id,
302
+ severity: check.blocking ? 'blocking' : 'warning',
303
+ })
304
+ }
305
+ if (
306
+ mode === 'min-artifact-count' &&
307
+ typeof check.config.count === 'number' &&
308
+ result.artifacts.length < check.config.count
309
+ ) {
310
+ return createIssue({
311
+ code: 'required_artifact_missing',
312
+ message: `Tool check "${check.description}" expected at least ${check.config.count} artifact(s).`,
313
+ nodeId: node.id,
314
+ severity: check.blocking ? 'blocking' : 'warning',
315
+ })
316
+ }
317
+ return null
318
+ }
319
+
320
+ if (check.type === 'human-approval') {
321
+ const approvedField = typeof check.config.approvedField === 'string' ? check.config.approvedField : 'approved'
322
+ if (readPathValue(result.structuredOutput, approvedField) !== true) {
323
+ return createIssue({
324
+ code: 'human_rejected',
325
+ message: `Human approval check "${check.description}" did not pass.`,
326
+ nodeId: node.id,
327
+ severity: check.blocking ? 'blocking' : 'warning',
328
+ detail: { approvedField },
329
+ })
330
+ }
331
+ return null
332
+ }
333
+
334
+ const resultField = typeof check.config.resultField === 'string' ? check.config.resultField : 'passed'
335
+ if (readPathValue(result.structuredOutput, resultField) !== true) {
336
+ return createIssue({
337
+ code: 'schema_validation_failed',
338
+ message: `LLM judge check "${check.description}" did not report success.`,
339
+ nodeId: node.id,
340
+ severity: check.blocking ? 'blocking' : 'warning',
341
+ detail: { resultField },
342
+ })
343
+ }
344
+ return null
345
+ }
346
+
347
+ function resolveFailureClass(blocking: PlanValidationIssueInput[]): PlanFailureClass | null {
348
+ for (const issue of blocking) {
349
+ if (issue.code === 'required_artifact_missing') return 'required_artifact_missing'
350
+ if (issue.code === 'human_rejected') return 'human_rejected'
351
+ if (issue.code === 'schema_validation_failed') return 'schema_validation_failed'
352
+ }
353
+ return blocking.length > 0 ? 'non_recoverable_logic_error' : null
354
+ }
355
+
356
+ function collectNodeDefinitionIssues(params: {
357
+ draft: PlanDraft
358
+ nodesById: Map<string, PlanDraft['nodes'][number]>
359
+ blocking: PlanValidationIssueInput[]
360
+ warnings: PlanValidationIssueInput[]
361
+ }): void {
362
+ const { draft, nodesById, blocking, warnings } = params
363
+
364
+ for (const node of draft.nodes) {
365
+ const isStructuralNode = STRUCTURAL_NODE_TYPE_SET.has(node.type)
366
+ if (!isStructuralNode && node.deliverables.length === 0) {
367
+ blocking.push(
368
+ createIssue({
369
+ code: 'node_missing_deliverables',
370
+ message: `Node "${node.label}" must define at least one deliverable.`,
371
+ nodeId: node.id,
372
+ }),
373
+ )
374
+ }
375
+ if (node.successCriteria.length === 0) {
376
+ blocking.push(
377
+ createIssue({
378
+ code: 'node_missing_success_criteria',
379
+ message: `Node "${node.label}" must define at least one success criterion.`,
380
+ nodeId: node.id,
381
+ }),
382
+ )
383
+ }
384
+ if (!isStructuralNode && node.completionChecks.length === 0) {
385
+ blocking.push(
386
+ createIssue({
387
+ code: 'node_missing_completion_checks',
388
+ message: `Node "${node.label}" must define at least one completion check.`,
389
+ nodeId: node.id,
390
+ }),
391
+ )
392
+ }
393
+ if (HUMAN_NODE_TYPE_SET.has(node.type) && node.owner.executorType !== 'user') {
394
+ blocking.push(
395
+ createIssue({
396
+ code: 'human_node_owner_mismatch',
397
+ message: `Human node "${node.label}" must be owned by a user executor.`,
398
+ nodeId: node.id,
399
+ }),
400
+ )
401
+ }
402
+ if (
403
+ !HUMAN_NODE_TYPE_SET.has(node.type) &&
404
+ node.type !== 'join' &&
405
+ node.type !== 'switch' &&
406
+ node.owner.ref.trim().length === 0
407
+ ) {
408
+ blocking.push(
409
+ createIssue({
410
+ code: 'node_owner_missing',
411
+ message: `Node "${node.label}" must declare an executor reference.`,
412
+ nodeId: node.id,
413
+ }),
414
+ )
415
+ }
416
+ if (node.owner.executorType === 'plugin' && node.owner.operation.trim().length === 0) {
417
+ blocking.push(
418
+ createIssue({
419
+ code: 'plugin_owner_operation_missing',
420
+ message: `Plugin-owned node "${node.label}" must declare an operation.`,
421
+ nodeId: node.id,
422
+ }),
423
+ )
424
+ }
425
+ if (node.owner.executorType === 'system' && node.owner.operation.trim().length === 0) {
426
+ blocking.push(
427
+ createIssue({
428
+ code: 'system_owner_operation_missing',
429
+ message: `System-owned node "${node.label}" must declare an operation.`,
430
+ nodeId: node.id,
431
+ }),
432
+ )
433
+ }
434
+ if (node.inputSchemaRef && !resolveSchemaRef(draft, node.inputSchemaRef)) {
435
+ blocking.push(
436
+ createIssue({
437
+ code: 'missing_input_schema_ref',
438
+ message: `Node "${node.label}" references unknown input schema "${node.inputSchemaRef}".`,
439
+ nodeId: node.id,
440
+ }),
441
+ )
442
+ }
443
+ if (node.outputSchemaRef && !resolveSchemaRef(draft, node.outputSchemaRef)) {
444
+ blocking.push(
445
+ createIssue({
446
+ code: 'missing_output_schema_ref',
447
+ message: `Node "${node.label}" references unknown output schema "${node.outputSchemaRef}".`,
448
+ nodeId: node.id,
449
+ }),
450
+ )
451
+ }
452
+ for (const deliverable of node.deliverables) {
453
+ if (deliverable.schemaRef && !resolveSchemaRef(draft, deliverable.schemaRef)) {
454
+ blocking.push(
455
+ createIssue({
456
+ code: 'missing_artifact_schema_ref',
457
+ message: `Node "${node.label}" references unknown artifact schema "${deliverable.schemaRef}".`,
458
+ nodeId: node.id,
459
+ detail: { artifactName: deliverable.name },
460
+ }),
461
+ )
462
+ }
463
+ }
464
+ for (const check of node.completionChecks) {
465
+ const checkSchemaRef = typeof check.config.schemaRef === 'string' ? check.config.schemaRef : undefined
466
+ if (checkSchemaRef && !resolveSchemaRef(draft, checkSchemaRef)) {
467
+ blocking.push(
468
+ createIssue({
469
+ code: 'missing_completion_check_schema_ref',
470
+ message: `Node "${node.label}" references unknown completion-check schema "${checkSchemaRef}".`,
471
+ nodeId: node.id,
472
+ detail: { checkDescription: check.description },
473
+ }),
474
+ )
475
+ }
476
+ }
477
+ if (node.toolPolicy.allow.some((toolName) => node.toolPolicy.deny.includes(toolName))) {
478
+ warnings.push(
479
+ createIssue({
480
+ code: 'tool_policy_overlap',
481
+ severity: 'warning',
482
+ message: `Node "${node.label}" lists the same tool in allow and deny policy.`,
483
+ nodeId: node.id,
484
+ }),
485
+ )
486
+ }
487
+
488
+ if (node.type === 'deliberation-fork') {
489
+ if (!node.deliberationConfig) {
490
+ blocking.push(
491
+ createIssue({
492
+ code: 'deliberation_fork_missing_config',
493
+ message: `Deliberation-fork node "${node.label}" must define deliberationConfig.`,
494
+ nodeId: node.id,
495
+ }),
496
+ )
497
+ } else {
498
+ if (node.deliberationConfig.branches.length < 2) {
499
+ blocking.push(
500
+ createIssue({
501
+ code: 'deliberation_fork_insufficient_branches',
502
+ message: `Deliberation-fork node "${node.label}" must define at least 2 branches.`,
503
+ nodeId: node.id,
504
+ }),
505
+ )
506
+ }
507
+ for (const branch of node.deliberationConfig.branches) {
508
+ if (!nodesById.has(branch.entryNodeId)) {
509
+ blocking.push(
510
+ createIssue({
511
+ code: 'deliberation_fork_missing_branch_entry',
512
+ message: `Deliberation-fork node "${node.label}" references missing branch entry node "${branch.entryNodeId}".`,
513
+ nodeId: node.id,
514
+ detail: { branchId: branch.branchId, entryNodeId: branch.entryNodeId },
515
+ }),
516
+ )
517
+ }
518
+ }
519
+ const gateNode = nodesById.get(node.deliberationConfig.resolutionGateNodeId)
520
+ if (!gateNode) {
521
+ blocking.push(
522
+ createIssue({
523
+ code: 'deliberation_fork_missing_gate',
524
+ message: `Deliberation-fork node "${node.label}" references missing resolution gate node "${node.deliberationConfig.resolutionGateNodeId}".`,
525
+ nodeId: node.id,
526
+ detail: { resolutionGateNodeId: node.deliberationConfig.resolutionGateNodeId },
527
+ }),
528
+ )
529
+ } else if (gateNode.type !== 'human-decision') {
530
+ blocking.push(
531
+ createIssue({
532
+ code: 'deliberation_fork_gate_type_mismatch',
533
+ message: `Resolution gate node "${gateNode.label}" must be of type "human-decision".`,
534
+ nodeId: node.id,
535
+ detail: { gateNodeId: gateNode.id, gateNodeType: gateNode.type },
536
+ }),
537
+ )
538
+ }
539
+ }
540
+ }
541
+ }
542
+ }
543
+
544
+ function collectEdgeStructureIssues(params: {
545
+ draft: PlanDraft
546
+ nodesById: Map<string, PlanDraft['nodes'][number]>
547
+ edgeIds: Set<string>
548
+ adjacency: Map<string, string[]>
549
+ inbound: Map<string, string[]>
550
+ blocking: PlanValidationIssueInput[]
551
+ }): void {
552
+ const { draft, nodesById, edgeIds, adjacency, inbound, blocking } = params
553
+
554
+ for (const edge of draft.edges) {
555
+ if (edgeIds.has(edge.id)) {
556
+ blocking.push(
557
+ createIssue({
558
+ code: 'duplicate_edge_id',
559
+ message: `Plan contains duplicate edge id "${edge.id}".`,
560
+ detail: { edgeId: edge.id },
561
+ }),
562
+ )
563
+ }
564
+ edgeIds.add(edge.id)
565
+
566
+ if (!nodesById.has(edge.source)) {
567
+ blocking.push(
568
+ createIssue({
569
+ code: 'missing_edge_source',
570
+ message: `Edge "${edge.id}" references missing source node "${edge.source}".`,
571
+ detail: { edgeId: edge.id, source: edge.source },
572
+ }),
573
+ )
574
+ continue
575
+ }
576
+ if (!nodesById.has(edge.target)) {
577
+ blocking.push(
578
+ createIssue({
579
+ code: 'missing_edge_target',
580
+ message: `Edge "${edge.id}" references missing target node "${edge.target}".`,
581
+ detail: { edgeId: edge.id, target: edge.target },
582
+ }),
583
+ )
584
+ continue
585
+ }
586
+
587
+ adjacency.get(edge.source)?.push(edge.target)
588
+ inbound.get(edge.target)?.push(edge.source)
589
+ }
590
+ }
591
+
592
+ function collectGraphTopologyIssues(params: {
593
+ draft: PlanDraft
594
+ entryNodeIds: string[]
595
+ adjacency: Map<string, string[]>
596
+ inbound: Map<string, string[]>
597
+ blocking: PlanValidationIssueInput[]
598
+ warnings: PlanValidationIssueInput[]
599
+ planCoordinationService: ReturnType<typeof makePlanCoordinationService>
600
+ }): void {
601
+ const { draft, entryNodeIds, adjacency, inbound, blocking, warnings, planCoordinationService } = params
602
+
603
+ for (const node of draft.nodes) {
604
+ const inboundEdges = inbound.get(node.id) ?? []
605
+ const outboundEdges = adjacency.get(node.id) ?? []
606
+
607
+ if (node.type === 'join' && inboundEdges.length < 2) {
608
+ blocking.push(
609
+ createIssue({
610
+ code: 'join_requires_multiple_inbound_edges',
611
+ message: `Join node "${node.label}" must have at least two inbound edges.`,
612
+ nodeId: node.id,
613
+ }),
614
+ )
615
+ }
616
+
617
+ if (node.type === 'switch') {
618
+ const conditionalEdges = draft.edges.filter((edge) => edge.source === node.id && edge.when)
619
+ if (conditionalEdges.length === 0) {
620
+ blocking.push(
621
+ createIssue({
622
+ code: 'switch_requires_conditional_edges',
623
+ message: `Switch node "${node.label}" must define conditional outbound edges.`,
624
+ nodeId: node.id,
625
+ }),
626
+ )
627
+ }
628
+ if (outboundEdges.length < 2) {
629
+ warnings.push(
630
+ createIssue({
631
+ code: 'switch_has_single_path',
632
+ severity: 'warning',
633
+ message: `Switch node "${node.label}" only routes to one target.`,
634
+ nodeId: node.id,
635
+ }),
636
+ )
637
+ }
638
+ }
639
+
640
+ if (!entryNodeIds.includes(node.id) && inboundEdges.length === 0) {
641
+ blocking.push(
642
+ createIssue({
643
+ code: 'orphan_node',
644
+ message: `Node "${node.label}" is not connected to the graph.`,
645
+ nodeId: node.id,
646
+ }),
647
+ )
648
+ }
649
+
650
+ if (node.inputSchemaRef && inboundEdges.length > 0) {
651
+ const hasMappedInput = draft.edges
652
+ .filter((edge) => edge.target === node.id)
653
+ .some((edge) => Object.keys(edge.map).length > 0)
654
+ if (!hasMappedInput) {
655
+ blocking.push(
656
+ createIssue({
657
+ code: 'unresolved_node_input',
658
+ message: `Node "${node.label}" declares inputSchemaRef but no inbound edge maps data into it.`,
659
+ nodeId: node.id,
660
+ }),
661
+ )
662
+ }
663
+ }
664
+ }
665
+
666
+ const reachableNodeIds = buildReachableNodeIds(entryNodeIds, adjacency)
667
+ for (const node of draft.nodes) {
668
+ if (!reachableNodeIds.has(node.id)) {
669
+ blocking.push(
670
+ createIssue({
671
+ code: 'unreachable_node',
672
+ message: `Node "${node.label}" is unreachable from the configured entry nodes.`,
673
+ nodeId: node.id,
674
+ }),
675
+ )
676
+ }
677
+ }
678
+
679
+ if (
680
+ graphHasCycle(
681
+ draft.nodes.map((node) => node.id),
682
+ adjacency,
683
+ )
684
+ ) {
685
+ blocking.push(
686
+ createIssue({
687
+ code: 'cycle_detected',
688
+ message: 'Plan graph contains a cycle. Loop semantics are not supported in this cutover.',
689
+ }),
690
+ )
691
+ }
692
+
693
+ if (draft.dependencies && draft.dependencies.length > 0) {
694
+ const cycleIssues = planCoordinationService.validateNoCycles([
695
+ { id: draft.title, title: draft.title, dependencies: draft.dependencies },
696
+ ])
697
+ blocking.push(...cycleIssues)
698
+ }
699
+ }
700
+
701
+ function collectNodeResultArtifactIssues(params: {
702
+ draft: Pick<PlanDraft, 'schemas'>
703
+ node: PlanNodeValidationSpec
704
+ result: PlanNodeResultSubmission
705
+ artifactsByName: Map<string, PlanNodeResultSubmission['artifacts'][number]>
706
+ blocking: PlanValidationIssueInput[]
707
+ }): void {
708
+ const { draft, node, result, artifactsByName, blocking } = params
709
+
710
+ if (node.outputSchemaRef) {
711
+ const outputSchema = resolveSchemaRef(draft, node.outputSchemaRef)
712
+ if (!result.structuredOutput) {
713
+ blocking.push(
714
+ createIssue({
715
+ code: 'structured_output_missing',
716
+ message: `Node "${node.label}" requires structured output.`,
717
+ nodeId: node.id,
718
+ }),
719
+ )
720
+ } else if (outputSchema) {
721
+ const schemaIssues = validateSchemaValue({
722
+ schema: outputSchema,
723
+ value: result.structuredOutput,
724
+ path: 'structuredOutput',
725
+ })
726
+ if (schemaIssues.length > 0) {
727
+ blocking.push(
728
+ createIssue({
729
+ code: 'schema_validation_failed',
730
+ message: `Structured output for node "${node.label}" failed schema validation.`,
731
+ nodeId: node.id,
732
+ detail: { issues: schemaIssues },
733
+ }),
734
+ )
735
+ }
736
+ }
737
+ }
738
+
739
+ for (const deliverable of node.deliverables) {
740
+ const artifact = artifactsByName.get(deliverable.name)
741
+ if (!artifact) {
742
+ if (deliverable.required) {
743
+ blocking.push(
744
+ createIssue({
745
+ code: 'required_artifact_missing',
746
+ message: `Node "${node.label}" is missing required artifact "${deliverable.name}".`,
747
+ nodeId: node.id,
748
+ }),
749
+ )
750
+ }
751
+ continue
752
+ }
753
+
754
+ if (artifact.kind !== deliverable.kind) {
755
+ blocking.push(
756
+ createIssue({
757
+ code: 'artifact_kind_mismatch',
758
+ message: `Artifact "${deliverable.name}" must be of kind "${deliverable.kind}".`,
759
+ nodeId: node.id,
760
+ }),
761
+ )
762
+ }
763
+
764
+ if (deliverable.schemaRef) {
765
+ const artifactSchema = resolveSchemaRef(draft, deliverable.schemaRef)
766
+ if (!artifact.payload) {
767
+ blocking.push(
768
+ createIssue({
769
+ code: 'artifact_payload_missing',
770
+ message: `Artifact "${deliverable.name}" must include payload data for schema validation.`,
771
+ nodeId: node.id,
772
+ }),
773
+ )
774
+ } else if (artifactSchema) {
775
+ const schemaIssues = validateSchemaValue({
776
+ schema: artifactSchema,
777
+ value: artifact.payload,
778
+ path: `artifact:${deliverable.name}`,
779
+ })
780
+ if (schemaIssues.length > 0) {
781
+ blocking.push(
782
+ createIssue({
783
+ code: 'schema_validation_failed',
784
+ message: `Artifact "${deliverable.name}" failed schema validation.`,
785
+ nodeId: node.id,
786
+ detail: { issues: schemaIssues, artifact: deliverable.name },
787
+ }),
788
+ )
789
+ }
790
+ }
791
+ }
792
+ }
793
+ }
794
+
795
+ function collectNodeResultCompletionCheckIssues(params: {
796
+ draft: Pick<PlanDraft, 'schemas'>
797
+ node: PlanNodeValidationSpec
798
+ result: PlanNodeResultSubmission
799
+ blocking: PlanValidationIssueInput[]
800
+ warnings: PlanValidationIssueInput[]
801
+ }): void {
802
+ const { draft, node, result, blocking, warnings } = params
803
+
804
+ for (const check of node.completionChecks) {
805
+ const issue = evaluateCompletionCheck({ draft, node, result, check })
806
+ if (!issue) continue
807
+ if (issue.severity === 'warning') {
808
+ warnings.push(issue)
809
+ } else {
810
+ blocking.push(issue)
811
+ }
812
+ }
813
+ }
814
+
815
+ export function makePlanValidatorService(planCoordinationService: ReturnType<typeof makePlanCoordinationService>) {
816
+ return {
817
+ validateDraft(draft: PlanDraft): DraftValidationResult {
818
+ const blocking: PlanValidationIssueInput[] = []
819
+ const warnings: PlanValidationIssueInput[] = []
820
+ const nodeIds = new Set<string>()
821
+ const edgeIds = new Set<string>()
822
+ const nodesById = new Map(draft.nodes.map((node) => [node.id, node]))
823
+ const adjacency = new Map<string, string[]>(draft.nodes.map((node) => [node.id, []]))
824
+ const inbound = new Map<string, string[]>(draft.nodes.map((node) => [node.id, []]))
825
+
826
+ for (const node of draft.nodes) {
827
+ if (nodeIds.has(node.id)) {
828
+ blocking.push(
829
+ createIssue({
830
+ code: 'duplicate_node_id',
831
+ message: `Plan contains duplicate node id "${node.id}".`,
832
+ nodeId: node.id,
833
+ }),
834
+ )
835
+ }
836
+ nodeIds.add(node.id)
837
+ }
838
+
839
+ collectNodeDefinitionIssues({ draft, nodesById, blocking, warnings })
840
+
841
+ const entryNodeIds = draft.entryNodeIds ?? []
842
+ for (const entryNodeId of entryNodeIds) {
843
+ if (!nodesById.has(entryNodeId)) {
844
+ blocking.push(
845
+ createIssue({
846
+ code: 'missing_entry_node',
847
+ message: `Entry node "${entryNodeId}" does not exist in the plan.`,
848
+ detail: { entryNodeId },
849
+ }),
850
+ )
851
+ }
852
+ }
853
+
854
+ collectEdgeStructureIssues({ draft, nodesById, edgeIds, adjacency, inbound, blocking })
855
+ collectGraphTopologyIssues({
856
+ draft,
857
+ entryNodeIds,
858
+ adjacency,
859
+ inbound,
860
+ blocking,
861
+ warnings,
862
+ planCoordinationService,
863
+ })
864
+
865
+ return { blocking, warnings }
866
+ },
867
+
868
+ validateNodeResult(params: {
869
+ draft: Pick<PlanDraft, 'schemas'>
870
+ node: PlanNodeValidationSpec
871
+ result: PlanNodeResultSubmission
872
+ }): NodeResultValidationResult {
873
+ const blocking: PlanValidationIssueInput[] = []
874
+ const warnings: PlanValidationIssueInput[] = []
875
+ const artifactsByName = new Map(params.result.artifacts.map((artifact) => [artifact.name, artifact]))
876
+
877
+ collectNodeResultArtifactIssues({
878
+ draft: params.draft,
879
+ node: params.node,
880
+ result: params.result,
881
+ artifactsByName,
882
+ blocking,
883
+ })
884
+ collectNodeResultCompletionCheckIssues({
885
+ draft: params.draft,
886
+ node: params.node,
887
+ result: params.result,
888
+ blocking,
889
+ warnings,
890
+ })
891
+
892
+ return { blocking, warnings, failureClass: resolveFailureClass(blocking) }
893
+ },
894
+ }
895
+ }
896
+
897
+ export class PlanValidatorServiceTag extends Context.Service<
898
+ PlanValidatorServiceTag,
899
+ ReturnType<typeof makePlanValidatorService>
900
+ >()('PlanValidatorService') {}
901
+
902
+ export const PlanValidatorServiceLive = Layer.effect(
903
+ PlanValidatorServiceTag,
904
+ Effect.map(PlanCoordinationServiceTag.asEffect(), (planCoordinationService) =>
905
+ makePlanValidatorService(planCoordinationService),
906
+ ),
907
+ )