@elevasis/core 0.21.0 → 0.23.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 (132) hide show
  1. package/dist/index.d.ts +2518 -2169
  2. package/dist/index.js +2495 -1095
  3. package/dist/knowledge/index.d.ts +706 -1044
  4. package/dist/knowledge/index.js +9 -9
  5. package/dist/organization-model/index.d.ts +2518 -2169
  6. package/dist/organization-model/index.js +2495 -1095
  7. package/dist/test-utils/index.d.ts +826 -1014
  8. package/dist/test-utils/index.js +1894 -1032
  9. package/package.json +3 -3
  10. package/src/__tests__/template-core-compatibility.test.ts +11 -79
  11. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +852 -397
  12. package/src/auth/multi-tenancy/permissions.ts +20 -8
  13. package/src/business/README.md +2 -2
  14. package/src/business/acquisition/api-schemas.test.ts +175 -2
  15. package/src/business/acquisition/api-schemas.ts +132 -16
  16. package/src/business/acquisition/build-templates.test.ts +4 -4
  17. package/src/business/acquisition/build-templates.ts +72 -30
  18. package/src/business/acquisition/crm-state-actions.test.ts +13 -11
  19. package/src/business/acquisition/index.ts +12 -0
  20. package/src/business/acquisition/types.ts +7 -3
  21. package/src/business/clients/api-schemas.test.ts +115 -0
  22. package/src/business/clients/api-schemas.ts +158 -0
  23. package/src/business/clients/index.ts +1 -0
  24. package/src/business/deals/api-schemas.ts +8 -0
  25. package/src/business/index.ts +5 -2
  26. package/src/business/projects/types.ts +19 -0
  27. package/src/execution/engine/__tests__/fixtures/test-agents.ts +10 -8
  28. package/src/execution/engine/agent/core/__tests__/agent.test.ts +16 -12
  29. package/src/execution/engine/agent/core/__tests__/error-passthrough.test.ts +4 -3
  30. package/src/execution/engine/agent/core/types.ts +25 -15
  31. package/src/execution/engine/agent/index.ts +6 -4
  32. package/src/execution/engine/agent/reasoning/__tests__/request-builder.test.ts +24 -18
  33. package/src/execution/engine/index.ts +3 -0
  34. package/src/execution/engine/workflow/types.ts +9 -2
  35. package/src/knowledge/README.md +8 -7
  36. package/src/knowledge/__tests__/queries.test.ts +74 -73
  37. package/src/knowledge/format.ts +10 -9
  38. package/src/knowledge/index.ts +1 -1
  39. package/src/knowledge/published.ts +1 -1
  40. package/src/knowledge/queries.ts +26 -25
  41. package/src/organization-model/README.md +73 -26
  42. package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -0
  43. package/src/organization-model/__tests__/defaults.test.ts +76 -96
  44. package/src/organization-model/__tests__/domains/actions.test.ts +56 -0
  45. package/src/organization-model/__tests__/domains/customers.test.ts +299 -295
  46. package/src/organization-model/__tests__/domains/entities.test.ts +56 -0
  47. package/src/organization-model/__tests__/domains/goals.test.ts +493 -479
  48. package/src/organization-model/__tests__/domains/identity.test.ts +280 -279
  49. package/src/organization-model/__tests__/domains/navigation.test.ts +268 -212
  50. package/src/organization-model/__tests__/domains/offerings.test.ts +414 -419
  51. package/src/organization-model/__tests__/domains/policies.test.ts +323 -0
  52. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +271 -271
  53. package/src/organization-model/__tests__/domains/resources.test.ts +310 -0
  54. package/src/organization-model/__tests__/domains/roles.test.ts +463 -347
  55. package/src/organization-model/__tests__/domains/statuses.test.ts +246 -243
  56. package/src/organization-model/__tests__/domains/systems.test.ts +209 -0
  57. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +361 -0
  58. package/src/organization-model/__tests__/foundation.test.ts +74 -102
  59. package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -0
  60. package/src/organization-model/__tests__/graph.test.ts +899 -71
  61. package/src/organization-model/__tests__/knowledge.test.ts +209 -49
  62. package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -0
  63. package/src/organization-model/__tests__/migration-helpers.test.ts +591 -0
  64. package/src/organization-model/__tests__/prospecting-ssot.test.ts +36 -27
  65. package/src/organization-model/__tests__/recursive-system-schema.test.ts +520 -0
  66. package/src/organization-model/__tests__/resolve.test.ts +174 -23
  67. package/src/organization-model/__tests__/schema.test.ts +291 -114
  68. package/src/organization-model/__tests__/surface-projection.test.ts +207 -97
  69. package/src/organization-model/catalogs/lead-gen.ts +144 -0
  70. package/src/organization-model/content-kinds/config.ts +36 -0
  71. package/src/organization-model/content-kinds/index.ts +74 -0
  72. package/src/organization-model/content-kinds/pipeline.ts +68 -0
  73. package/src/organization-model/content-kinds/registry.ts +44 -0
  74. package/src/organization-model/content-kinds/status.ts +71 -0
  75. package/src/organization-model/content-kinds/template.ts +83 -0
  76. package/src/organization-model/content-kinds/types.ts +117 -0
  77. package/src/organization-model/contracts.ts +13 -3
  78. package/src/organization-model/defaults.ts +499 -86
  79. package/src/organization-model/domains/actions.ts +239 -0
  80. package/src/organization-model/domains/customers.ts +78 -75
  81. package/src/organization-model/domains/entities.ts +144 -0
  82. package/src/organization-model/domains/goals.ts +83 -80
  83. package/src/organization-model/domains/knowledge.ts +76 -17
  84. package/src/organization-model/domains/navigation.ts +107 -384
  85. package/src/organization-model/domains/offerings.ts +71 -66
  86. package/src/organization-model/domains/policies.ts +102 -0
  87. package/src/organization-model/domains/projects.ts +14 -48
  88. package/src/organization-model/domains/prospecting.ts +62 -181
  89. package/src/organization-model/domains/resources.ts +145 -0
  90. package/src/organization-model/domains/roles.ts +96 -55
  91. package/src/organization-model/domains/sales.ts +10 -219
  92. package/src/organization-model/domains/shared.ts +57 -57
  93. package/src/organization-model/domains/statuses.ts +339 -130
  94. package/src/organization-model/domains/systems.ts +203 -0
  95. package/src/organization-model/foundation.ts +54 -67
  96. package/src/organization-model/graph/build.ts +682 -54
  97. package/src/organization-model/graph/link.ts +1 -1
  98. package/src/organization-model/graph/schema.ts +24 -9
  99. package/src/organization-model/graph/types.ts +20 -7
  100. package/src/organization-model/helpers.ts +231 -26
  101. package/src/organization-model/icons.ts +1 -0
  102. package/src/organization-model/index.ts +118 -5
  103. package/src/organization-model/migration-helpers.ts +249 -0
  104. package/src/organization-model/organization-graph.mdx +16 -15
  105. package/src/organization-model/organization-model.mdx +111 -44
  106. package/src/organization-model/published.ts +172 -19
  107. package/src/organization-model/resolve.ts +117 -54
  108. package/src/organization-model/schema.ts +654 -112
  109. package/src/organization-model/surface-projection.ts +116 -122
  110. package/src/organization-model/types.ts +146 -20
  111. package/src/platform/api/types.ts +38 -35
  112. package/src/platform/constants/versions.ts +1 -1
  113. package/src/platform/registry/__tests__/command-view.test.ts +6 -8
  114. package/src/platform/registry/__tests__/resource-link.test.ts +13 -8
  115. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +16 -31
  116. package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -0
  117. package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2005
  118. package/src/platform/registry/__tests__/validation.test.ts +1347 -1086
  119. package/src/platform/registry/index.ts +14 -0
  120. package/src/platform/registry/resource-registry.ts +52 -2
  121. package/src/platform/registry/serialization.ts +241 -202
  122. package/src/platform/registry/serialized-types.ts +1 -0
  123. package/src/platform/registry/types.ts +411 -361
  124. package/src/platform/registry/validation.ts +745 -513
  125. package/src/projects/api-schemas.ts +290 -267
  126. package/src/reference/_generated/contracts.md +853 -397
  127. package/src/reference/glossary.md +23 -18
  128. package/src/supabase/database.types.ts +181 -0
  129. package/src/test-utils/test-utils.test.ts +1 -6
  130. package/src/organization-model/__tests__/domains/operations.test.ts +0 -203
  131. package/src/organization-model/domains/features.ts +0 -31
  132. package/src/organization-model/domains/operations.ts +0 -85
@@ -1,513 +1,745 @@
1
- /**
2
- * Registry Validation Utilities
3
- *
4
- * Centralized validation logic for ResourceRegistry.
5
- * All validation runs at API startup - fails fast in development.
6
- */
7
-
8
- import type { z } from 'zod'
9
- import type { ModelConfig } from '../../execution/engine/llm/model-info'
10
- import { validateModelConfig, ModelConfigError } from '../../execution/engine/llm/errors'
11
- import type { DeploymentSpec } from './resource-registry'
12
- import type {
13
- TriggerDefinition,
14
- ResourceRelationships,
15
- ExternalResourceDefinition,
16
- HumanCheckpointDefinition
17
- } from './types'
18
-
19
- // ============================================================================
20
- // Validation Error Types
21
- // ============================================================================
22
-
23
- export class RegistryValidationError extends Error {
24
- constructor(
25
- public readonly orgName: string,
26
- public readonly resourceId: string | null,
27
- public readonly field: string | null,
28
- message: string
29
- ) {
30
- super(message)
31
- this.name = 'RegistryValidationError'
32
- }
33
- }
34
-
35
- // ============================================================================
36
- // Resource Validation
37
- // ============================================================================
38
-
39
- /**
40
- * Validates resources for a single organization
41
- * - Duplicate resourceId check
42
- * - Model configuration validation
43
- * - ExecutionInterface-to-inputSchema validation
44
- * @throws RegistryValidationError if validation fails
45
- */
46
- export function validateDeploymentSpec(orgName: string, resources: DeploymentSpec): void {
47
- const seenIds = new Set<string>()
48
-
49
- // Validate workflows
50
- resources.workflows?.forEach((workflow) => {
51
- const id = workflow.config.resourceId
52
-
53
- // Check for duplicate IDs
54
- if (seenIds.has(id)) {
55
- throw new RegistryValidationError(
56
- orgName,
57
- id,
58
- null,
59
- `Duplicate resourceId "${id}" in organization "${orgName}". ` +
60
- `Workflows and agents must have unique IDs within an organization.`
61
- )
62
- }
63
- seenIds.add(id)
64
- // Validate model config if present (workflows may optionally have modelConfig)
65
- if ('modelConfig' in workflow && workflow.modelConfig) {
66
- validateResourceModelConfig(orgName, id, workflow.modelConfig as ModelConfig)
67
- }
68
-
69
- // Validate ExecutionInterface matches inputSchema
70
- if (workflow.interface) {
71
- validateExecutionInterface(orgName, id, workflow.interface, workflow.contract.inputSchema)
72
- }
73
- })
74
-
75
- // Validate agents
76
- resources.agents?.forEach((agent) => {
77
- const id = agent.config.resourceId
78
-
79
- // Check for duplicate IDs
80
- if (seenIds.has(id)) {
81
- throw new RegistryValidationError(
82
- orgName,
83
- id,
84
- null,
85
- `Duplicate resourceId "${id}" in organization "${orgName}". ` +
86
- `Workflows and agents must have unique IDs within an organization.`
87
- )
88
- }
89
- seenIds.add(id)
90
-
91
- // Validate model config
92
- validateResourceModelConfig(orgName, id, agent.modelConfig)
93
-
94
- // Validate ExecutionInterface matches inputSchema
95
- if (agent.interface) {
96
- validateExecutionInterface(orgName, id, agent.interface, agent.contract.inputSchema)
97
- }
98
- })
99
- }
100
-
101
- /**
102
- * Validates model configuration for a resource
103
- */
104
- function validateResourceModelConfig(orgName: string, resourceId: string, modelConfig: ModelConfig): void {
105
- try {
106
- validateModelConfig(modelConfig)
107
- } catch (error) {
108
- if (error instanceof ModelConfigError) {
109
- throw new RegistryValidationError(
110
- orgName,
111
- resourceId,
112
- error.field,
113
- `Invalid model config in ${orgName}/${resourceId}: ${error.message} (field: ${error.field})`
114
- )
115
- }
116
- throw error
117
- }
118
- }
119
-
120
- // ============================================================================
121
- // ExecutionInterface Validation
122
- // ============================================================================
123
-
124
- /**
125
- * Validates that ExecutionInterface form fields match the inputSchema
126
- *
127
- * Checks:
128
- * 1. All required fields in inputSchema have corresponding form fields
129
- * 2. Form field names match schema field names (or have valid fieldMappings)
130
- * 3. Required/optional alignment between form and schema
131
- *
132
- * @throws RegistryValidationError if interface doesn't match schema
133
- */
134
- export function validateExecutionInterface(
135
- orgName: string,
136
- resourceId: string,
137
- executionInterface: {
138
- form: { fields: Array<{ name: string; required?: boolean }>; fieldMappings?: Record<string, string> }
139
- },
140
- inputSchema: z.ZodType
141
- ): void {
142
- const form = executionInterface.form
143
- const fieldMappings = form.fieldMappings ?? {}
144
-
145
- // Extract schema shape (only works for ZodObject)
146
- const schemaShape = extractZodShape(inputSchema)
147
- if (!schemaShape) {
148
- // Can't validate non-object schemas - skip validation
149
- return
150
- }
151
-
152
- const schemaFieldNames = Object.keys(schemaShape)
153
-
154
- // Build effective mapping: form field name -> schema field name
155
- const formToSchemaMap = new Map<string, string>()
156
- for (const field of form.fields) {
157
- const schemaKey = fieldMappings[field.name] ?? field.name
158
- formToSchemaMap.set(field.name, schemaKey)
159
- }
160
-
161
- // Check 1: All required schema fields have form fields
162
- for (const schemaFieldName of schemaFieldNames) {
163
- const schemaField = schemaShape[schemaFieldName]
164
- const isRequired = !isZodOptional(schemaField)
165
-
166
- // Find form field that maps to this schema field
167
- let hasFormField = false
168
- for (const [formFieldName, mappedSchemaName] of Array.from(formToSchemaMap.entries())) {
169
- if (mappedSchemaName === schemaFieldName) {
170
- hasFormField = true
171
-
172
- // Check required alignment
173
- const formField = form.fields.find((f) => f.name === formFieldName)
174
- if (isRequired && !formField?.required) {
175
- throw new RegistryValidationError(
176
- orgName,
177
- resourceId,
178
- `interface.form.fields.${formFieldName}`,
179
- `ExecutionInterface field "${formFieldName}" should be required to match inputSchema field "${schemaFieldName}" in ${orgName}/${resourceId}`
180
- )
181
- }
182
- break
183
- }
184
- }
185
-
186
- if (isRequired && !hasFormField) {
187
- throw new RegistryValidationError(
188
- orgName,
189
- resourceId,
190
- 'interface.form.fields',
191
- `ExecutionInterface missing required field "${schemaFieldName}" from inputSchema in ${orgName}/${resourceId}`
192
- )
193
- }
194
- }
195
-
196
- // Check 2: All form fields map to valid schema fields
197
- for (const [formFieldName, schemaFieldName] of Array.from(formToSchemaMap.entries())) {
198
- // Detect nested field notation (e.g., "criteria.targetTitles") and suggest flattening
199
- if (schemaFieldName.includes('.')) {
200
- const topLevelField = schemaFieldName.split('.')[0]
201
- throw new RegistryValidationError(
202
- orgName,
203
- resourceId,
204
- `interface.form.fields.${formFieldName}`,
205
- `ExecutionInterface field "${formFieldName}" uses nested notation. ` +
206
- `Flatten the inputSchema by moving nested fields to top-level ` +
207
- `(e.g., "${topLevelField}.x" "x") in ${orgName}/${resourceId}`
208
- )
209
- }
210
-
211
- if (!schemaFieldNames.includes(schemaFieldName)) {
212
- throw new RegistryValidationError(
213
- orgName,
214
- resourceId,
215
- `interface.form.fields.${formFieldName}`,
216
- `ExecutionInterface field "${formFieldName}" maps to non-existent schema field "${schemaFieldName}" in ${orgName}/${resourceId}`
217
- )
218
- }
219
- }
220
- }
221
-
222
- /**
223
- * Extract shape from a Zod schema (handles ZodObject and wrapped types)
224
- */
225
- function extractZodShape(schema: z.ZodType): Record<string, z.ZodType> | null {
226
- // Handle ZodObject directly
227
- if ('shape' in schema && typeof schema.shape === 'object') {
228
- return schema.shape as Record<string, z.ZodType>
229
- }
230
-
231
- // Handle wrapped types (ZodEffects, ZodDefault, etc.)
232
- if ('_def' in schema) {
233
- const def = schema._def as { innerType?: z.ZodType; schema?: z.ZodType }
234
- if (def.innerType) {
235
- return extractZodShape(def.innerType)
236
- }
237
- if (def.schema) {
238
- return extractZodShape(def.schema)
239
- }
240
- }
241
-
242
- return null
243
- }
244
-
245
- /**
246
- * Check if a Zod type is optional (or has a default)
247
- * Uses isOptional() method which handles ZodOptional, ZodDefault, ZodNullable
248
- */
249
- function isZodOptional(schema: z.ZodType): boolean {
250
- // Zod provides isOptional() which returns true for optional, default, and nullable types
251
- return schema.isOptional()
252
- }
253
-
254
- // ============================================================================
255
- // Relationship Validation
256
- // ============================================================================
257
-
258
- /**
259
- * Validates relationship declarations reference valid resources
260
- * @throws RegistryValidationError if validation fails
261
- */
262
- export function validateRelationships(orgName: string, resources: DeploymentSpec): void {
263
- // Skip if no manifest data
264
- if (!resources.relationships && !resources.triggers && !resources.externalResources && !resources.humanCheckpoints)
265
- return
266
-
267
- // Build resource ID sets for validation
268
- const validAgentIds = new Set(resources.agents?.map((a) => a.config.resourceId) ?? [])
269
- const validWorkflowIds = new Set(resources.workflows?.map((w) => w.config.resourceId) ?? [])
270
- const validIntegrationIds = new Set(resources.integrations?.map((i) => i.resourceId) ?? [])
271
- const validTriggerIds = new Set(resources.triggers?.map((t) => t.resourceId) ?? [])
272
-
273
- // Collect all internal resource IDs for uniqueness check
274
- const allInternalIds = new Set([
275
- ...Array.from(validAgentIds),
276
- ...Array.from(validWorkflowIds),
277
- ...Array.from(validTriggerIds),
278
- ...Array.from(validIntegrationIds)
279
- ])
280
-
281
- // Validate triggers
282
- validateTriggers(orgName, resources.triggers ?? [], validAgentIds, validWorkflowIds)
283
-
284
- // Validate resource relationships
285
- validateResourceRelationships(
286
- orgName,
287
- resources.relationships ?? {},
288
- validAgentIds,
289
- validWorkflowIds,
290
- validIntegrationIds,
291
- validTriggerIds
292
- )
293
-
294
- // Validate external resources
295
- validateExternalResources(
296
- orgName,
297
- resources.externalResources ?? [],
298
- allInternalIds,
299
- validAgentIds,
300
- validWorkflowIds,
301
- validIntegrationIds
302
- )
303
-
304
- // Validate human checkpoints
305
- validateHumanCheckpoints(orgName, resources.humanCheckpoints ?? [], allInternalIds, validAgentIds, validWorkflowIds)
306
- }
307
-
308
- /**
309
- * Validates trigger invocations
310
- * NOTE: Trigger relationships are declared in ResourceRelationships, not on TriggerDefinition
311
- * This validation is now handled by validateResourceRelationships()
312
- */
313
- function validateTriggers(
314
- _orgName: string,
315
- _triggers: TriggerDefinition[],
316
- _validAgentIds: Set<string>,
317
- _validWorkflowIds: Set<string>
318
- ): void {
319
- // No validation needed here - triggers declare their relationships in ResourceRelationships
320
- // This prevents duplication and keeps all relationship declarations in one place
321
- }
322
-
323
- /**
324
- * Validates resource relationship declarations
325
- */
326
- function validateResourceRelationships(
327
- orgName: string,
328
- relationships: ResourceRelationships,
329
- validAgentIds: Set<string>,
330
- validWorkflowIds: Set<string>,
331
- validIntegrationIds: Set<string>,
332
- validTriggerIds: Set<string>
333
- ): void {
334
- for (const [resourceId, declaration] of Object.entries(relationships)) {
335
- // Validate declaring resource exists (agents, workflows, or triggers can declare relationships)
336
- const resourceExists =
337
- validAgentIds.has(resourceId) || validWorkflowIds.has(resourceId) || validTriggerIds.has(resourceId)
338
- if (!resourceExists) {
339
- throw new RegistryValidationError(
340
- orgName,
341
- resourceId,
342
- null,
343
- `[${orgName}] Relationship declared for non-existent resource: ${resourceId}`
344
- )
345
- }
346
-
347
- // Validate triggers.agents
348
- declaration.triggers?.agents?.forEach((agentId) => {
349
- if (!validAgentIds.has(agentId)) {
350
- throw new RegistryValidationError(
351
- orgName,
352
- resourceId,
353
- 'triggers.agents',
354
- `[${orgName}] Resource '${resourceId}' triggers non-existent agent: ${agentId}`
355
- )
356
- }
357
- })
358
-
359
- // Validate triggers.workflows
360
- declaration.triggers?.workflows?.forEach((workflowId) => {
361
- if (!validWorkflowIds.has(workflowId)) {
362
- throw new RegistryValidationError(
363
- orgName,
364
- resourceId,
365
- 'triggers.workflows',
366
- `[${orgName}] Resource '${resourceId}' triggers non-existent workflow: ${workflowId}`
367
- )
368
- }
369
- })
370
-
371
- // Validate uses.integrations
372
- declaration.uses?.integrations?.forEach((integrationId) => {
373
- if (!validIntegrationIds.has(integrationId)) {
374
- throw new RegistryValidationError(
375
- orgName,
376
- resourceId,
377
- 'uses.integrations',
378
- `[${orgName}] Resource '${resourceId}' uses non-existent integration: ${integrationId}`
379
- )
380
- }
381
- })
382
- }
383
- }
384
-
385
- /**
386
- * Validates external resource definitions
387
- */
388
- function validateExternalResources(
389
- orgName: string,
390
- externalResources: ExternalResourceDefinition[],
391
- allInternalIds: Set<string>,
392
- validAgentIds: Set<string>,
393
- validWorkflowIds: Set<string>,
394
- validIntegrationIds: Set<string>
395
- ): void {
396
- externalResources.forEach((external) => {
397
- // Validate unique ID (no conflict with internal resources)
398
- if (allInternalIds.has(external.resourceId)) {
399
- throw new RegistryValidationError(
400
- orgName,
401
- external.resourceId,
402
- null,
403
- `[${orgName}] External resource ID '${external.resourceId}' conflicts with internal resource ID`
404
- )
405
- }
406
-
407
- // Validate triggers (external -> internal)
408
- external.triggers?.agents?.forEach((agentId) => {
409
- if (!validAgentIds.has(agentId)) {
410
- throw new RegistryValidationError(
411
- orgName,
412
- external.resourceId,
413
- 'triggers.agents',
414
- `[${orgName}] External resource '${external.resourceId}' triggers non-existent agent: ${agentId}`
415
- )
416
- }
417
- })
418
-
419
- external.triggers?.workflows?.forEach((workflowId) => {
420
- if (!validWorkflowIds.has(workflowId)) {
421
- throw new RegistryValidationError(
422
- orgName,
423
- external.resourceId,
424
- 'triggers.workflows',
425
- `[${orgName}] External resource '${external.resourceId}' triggers non-existent workflow: ${workflowId}`
426
- )
427
- }
428
- })
429
-
430
- // Validate uses integrations
431
- external.uses?.integrations?.forEach((integrationId) => {
432
- if (!validIntegrationIds.has(integrationId)) {
433
- throw new RegistryValidationError(
434
- orgName,
435
- external.resourceId,
436
- 'uses.integrations',
437
- `[${orgName}] External resource '${external.resourceId}' uses non-existent integration: ${integrationId}`
438
- )
439
- }
440
- })
441
- })
442
- }
443
-
444
- /**
445
- * Validates human checkpoint definitions
446
- */
447
- function validateHumanCheckpoints(
448
- orgName: string,
449
- humanCheckpoints: HumanCheckpointDefinition[],
450
- allInternalIds: Set<string>,
451
- validAgentIds: Set<string>,
452
- validWorkflowIds: Set<string>
453
- ): void {
454
- humanCheckpoints.forEach((humanCheckpoint) => {
455
- // Check for ID conflicts with internal resources
456
- if (allInternalIds.has(humanCheckpoint.resourceId)) {
457
- throw new RegistryValidationError(
458
- orgName,
459
- humanCheckpoint.resourceId,
460
- null,
461
- `[${orgName}] Human checkpoint ID '${humanCheckpoint.resourceId}' conflicts with internal resource ID`
462
- )
463
- }
464
-
465
- // Validate requestedBy.agents exist
466
- humanCheckpoint.requestedBy?.agents?.forEach((agentId) => {
467
- if (!validAgentIds.has(agentId)) {
468
- throw new RegistryValidationError(
469
- orgName,
470
- humanCheckpoint.resourceId,
471
- 'requestedBy.agents',
472
- `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' requestedBy non-existent agent: ${agentId}`
473
- )
474
- }
475
- })
476
-
477
- // Validate requestedBy.workflows exist
478
- humanCheckpoint.requestedBy?.workflows?.forEach((workflowId) => {
479
- if (!validWorkflowIds.has(workflowId)) {
480
- throw new RegistryValidationError(
481
- orgName,
482
- humanCheckpoint.resourceId,
483
- 'requestedBy.workflows',
484
- `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' requestedBy non-existent workflow: ${workflowId}`
485
- )
486
- }
487
- })
488
-
489
- // Validate routesTo.agents exist
490
- humanCheckpoint.routesTo?.agents?.forEach((agentId) => {
491
- if (!validAgentIds.has(agentId)) {
492
- throw new RegistryValidationError(
493
- orgName,
494
- humanCheckpoint.resourceId,
495
- 'routesTo.agents',
496
- `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' routesTo non-existent agent: ${agentId}`
497
- )
498
- }
499
- })
500
-
501
- // Validate routesTo.workflows exist
502
- humanCheckpoint.routesTo?.workflows?.forEach((workflowId) => {
503
- if (!validWorkflowIds.has(workflowId)) {
504
- throw new RegistryValidationError(
505
- orgName,
506
- humanCheckpoint.resourceId,
507
- 'routesTo.workflows',
508
- `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' routesTo non-existent workflow: ${workflowId}`
509
- )
510
- }
511
- })
512
- })
513
- }
1
+ /**
2
+ * Registry Validation Utilities
3
+ *
4
+ * Centralized validation logic for ResourceRegistry.
5
+ * All validation runs at API startup - fails fast in development.
6
+ */
7
+
8
+ import type { z } from 'zod'
9
+ import type { ModelConfig } from '../../execution/engine/llm/model-info'
10
+ import { validateModelConfig, ModelConfigError } from '../../execution/engine/llm/errors'
11
+ import type { DeploymentSpec } from './resource-registry'
12
+ import type { ResourceEntry } from '../../organization-model/domains/resources'
13
+ import type { SystemEntry } from '../../organization-model/domains/systems'
14
+ import type { OrganizationModel } from '../../organization-model/types'
15
+ import { listAllSystems } from '../../organization-model/helpers'
16
+ import type {
17
+ TriggerDefinition,
18
+ ResourceRelationships,
19
+ ExternalResourceDefinition,
20
+ HumanCheckpointDefinition,
21
+ ResourceType
22
+ } from './types'
23
+
24
+ // ============================================================================
25
+ // Validation Error Types
26
+ // ============================================================================
27
+
28
+ export class RegistryValidationError extends Error {
29
+ constructor(
30
+ public readonly orgName: string,
31
+ public readonly resourceId: string | null,
32
+ public readonly field: string | null,
33
+ message: string
34
+ ) {
35
+ super(message)
36
+ this.name = 'RegistryValidationError'
37
+ }
38
+ }
39
+
40
+ export type ResourceValidatorMode = 'strict' | 'warn-only'
41
+
42
+ export type ResourceGovernanceValidationIssueType =
43
+ | 'missing-code-resource'
44
+ | 'missing-om-resource'
45
+ | 'type-mismatch'
46
+ | 'system-mismatch'
47
+ | 'missing-om-system'
48
+ | 'raw-resource-id'
49
+
50
+ export interface ResourceGovernanceModel {
51
+ systems?: Record<string, SystemEntry>
52
+ resources?: Record<string, ResourceEntry>
53
+ }
54
+
55
+ export interface ResourceGovernanceValidationIssue {
56
+ type: ResourceGovernanceValidationIssueType
57
+ orgName: string
58
+ resourceId: string
59
+ message: string
60
+ }
61
+
62
+ export interface ResourceGovernanceValidationResult {
63
+ valid: boolean
64
+ mode: ResourceValidatorMode
65
+ issues: ResourceGovernanceValidationIssue[]
66
+ }
67
+
68
+ export interface ResourceGovernanceValidationOptions {
69
+ mode?: ResourceValidatorMode
70
+ onWarning?: (issue: ResourceGovernanceValidationIssue) => void
71
+ }
72
+
73
+ function getResourceValidatorMode(explicitMode?: ResourceValidatorMode): ResourceValidatorMode {
74
+ if (explicitMode) return explicitMode
75
+ const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env
76
+ return env?.ELEVASIS_RESOURCE_VALIDATOR === 'warn-only' ? 'warn-only' : 'strict'
77
+ }
78
+
79
+ function addGovernanceIssue(
80
+ issues: ResourceGovernanceValidationIssue[],
81
+ type: ResourceGovernanceValidationIssueType,
82
+ orgName: string,
83
+ resourceId: string,
84
+ message: string
85
+ ): void {
86
+ issues.push({
87
+ type,
88
+ orgName,
89
+ resourceId,
90
+ message
91
+ })
92
+ }
93
+
94
+ function emitGovernanceIssues(
95
+ issues: ResourceGovernanceValidationIssue[],
96
+ mode: ResourceValidatorMode,
97
+ onWarning?: (issue: ResourceGovernanceValidationIssue) => void
98
+ ): void {
99
+ if (issues.length === 0) return
100
+
101
+ if (mode === 'strict') {
102
+ const first = issues[0]
103
+ throw new RegistryValidationError(first.orgName, first.resourceId, 'organizationModel.resources', first.message)
104
+ }
105
+
106
+ const warn = onWarning ?? ((issue: ResourceGovernanceValidationIssue) => console.warn(issue.message))
107
+ for (const issue of issues) {
108
+ warn(issue)
109
+ }
110
+ }
111
+
112
+ function getRuntimeResources(resources: DeploymentSpec): Array<{
113
+ resourceId: string
114
+ type: ResourceType
115
+ descriptor?: ResourceEntry
116
+ }> {
117
+ return [
118
+ ...(resources.workflows ?? []).map((workflow) => ({
119
+ resourceId: workflow.config.resourceId,
120
+ type: workflow.config.type,
121
+ descriptor: workflow.config.resource
122
+ })),
123
+ ...(resources.agents ?? []).map((agent) => ({
124
+ resourceId: agent.config.resourceId,
125
+ type: agent.config.type,
126
+ descriptor: agent.config.resource
127
+ })),
128
+ ...(resources.integrations ?? []).map((integration) => ({
129
+ resourceId: integration.resourceId,
130
+ type: integration.type,
131
+ descriptor: (integration as { resource?: ResourceEntry }).resource
132
+ }))
133
+ ]
134
+ }
135
+
136
+ /**
137
+ * Validates runtime resource definitions against OM Resources and Systems.
138
+ *
139
+ * This is the shared core entry point for SDK, CI, deploy, and ResourceRegistry.
140
+ * Default mode is strict. ELEVASIS_RESOURCE_VALIDATOR=warn-only remains a
141
+ * permanent emergency escape hatch unless an explicit mode is passed.
142
+ */
143
+ export function validateResourceGovernance(
144
+ orgName: string,
145
+ deployment: DeploymentSpec,
146
+ organizationModel: ResourceGovernanceModel | undefined = deployment.organizationModel,
147
+ options: ResourceGovernanceValidationOptions = {}
148
+ ): ResourceGovernanceValidationResult {
149
+ const mode = getResourceValidatorMode(options.mode)
150
+ const omResourcesMap = organizationModel?.resources
151
+ const omSystemsMap = organizationModel?.systems
152
+ const issues: ResourceGovernanceValidationIssue[] = []
153
+
154
+ if (!omResourcesMap || !omSystemsMap) {
155
+ return { valid: true, mode, issues }
156
+ }
157
+
158
+ // Use listAllSystems (DFS) so nested systems under .subsystems are included.
159
+ // The lookup key is the dot-joined path, which equals resource.systemPath.
160
+ const systemsById = new Map(
161
+ listAllSystems({ systems: omSystemsMap } as OrganizationModel).map(({ path, system }) => [path, system])
162
+ )
163
+ const activeOmResources = Object.values(omResourcesMap).filter((resource) => resource.status === 'active')
164
+ const omResourcesById = new Map(activeOmResources.map((resource) => [resource.id, resource]))
165
+ const runtimeResources = getRuntimeResources(deployment)
166
+ const runtimeResourcesById = new Map(runtimeResources.map((resource) => [resource.resourceId, resource]))
167
+
168
+ for (const resource of activeOmResources) {
169
+ if (!systemsById.has(resource.systemPath)) {
170
+ addGovernanceIssue(
171
+ issues,
172
+ 'missing-om-system',
173
+ orgName,
174
+ resource.id,
175
+ `[${orgName}] OM resource '${resource.id}' references missing system path '${resource.systemPath}'.`
176
+ )
177
+ }
178
+
179
+ const runtimeResource = runtimeResourcesById.get(resource.id)
180
+ if (!runtimeResource) {
181
+ addGovernanceIssue(
182
+ issues,
183
+ 'missing-code-resource',
184
+ orgName,
185
+ resource.id,
186
+ `[${orgName}] OM resource '${resource.id}' has no matching code-side resource.`
187
+ )
188
+ continue
189
+ }
190
+
191
+ if (runtimeResource.type !== resource.kind) {
192
+ addGovernanceIssue(
193
+ issues,
194
+ 'type-mismatch',
195
+ orgName,
196
+ resource.id,
197
+ `[${orgName}] Resource '${resource.id}' type mismatch: code has '${runtimeResource.type}', OM has '${resource.kind}'.`
198
+ )
199
+ }
200
+
201
+ if (runtimeResource.descriptor && runtimeResource.descriptor.systemPath !== resource.systemPath) {
202
+ addGovernanceIssue(
203
+ issues,
204
+ 'system-mismatch',
205
+ orgName,
206
+ resource.id,
207
+ `[${orgName}] Resource '${resource.id}' system mismatch: code descriptor has '${runtimeResource.descriptor.systemPath}', OM has '${resource.systemPath}'.`
208
+ )
209
+ }
210
+ }
211
+
212
+ for (const runtimeResource of runtimeResources) {
213
+ const omResource = omResourcesById.get(runtimeResource.resourceId)
214
+ if (!omResource) {
215
+ addGovernanceIssue(
216
+ issues,
217
+ 'missing-om-resource',
218
+ orgName,
219
+ runtimeResource.resourceId,
220
+ `[${orgName}] Code-side resource '${runtimeResource.resourceId}' has no active OM Resource descriptor.`
221
+ )
222
+ }
223
+
224
+ if (!runtimeResource.descriptor) {
225
+ addGovernanceIssue(
226
+ issues,
227
+ 'raw-resource-id',
228
+ orgName,
229
+ runtimeResource.resourceId,
230
+ `[${orgName}] Code-side resource '${runtimeResource.resourceId}' authors raw resourceId/type values. Use an OM Resource descriptor and bindResourceDescriptor().`
231
+ )
232
+ continue
233
+ }
234
+
235
+ if (runtimeResource.descriptor.id !== runtimeResource.resourceId) {
236
+ addGovernanceIssue(
237
+ issues,
238
+ 'raw-resource-id',
239
+ orgName,
240
+ runtimeResource.resourceId,
241
+ `[${orgName}] Code-side resource '${runtimeResource.resourceId}' does not derive identity from its OM descriptor '${runtimeResource.descriptor.id}'.`
242
+ )
243
+ }
244
+
245
+ if (runtimeResource.descriptor.kind !== runtimeResource.type) {
246
+ addGovernanceIssue(
247
+ issues,
248
+ 'type-mismatch',
249
+ orgName,
250
+ runtimeResource.resourceId,
251
+ `[${orgName}] Code-side resource '${runtimeResource.resourceId}' descriptor kind '${runtimeResource.descriptor.kind}' does not match runtime type '${runtimeResource.type}'.`
252
+ )
253
+ }
254
+ }
255
+
256
+ emitGovernanceIssues(issues, mode, options.onWarning)
257
+
258
+ return {
259
+ valid: issues.length === 0,
260
+ mode,
261
+ issues
262
+ }
263
+ }
264
+
265
+ // ============================================================================
266
+ // Resource Validation
267
+ // ============================================================================
268
+
269
+ /**
270
+ * Validates resources for a single organization
271
+ * - Duplicate resourceId check
272
+ * - Model configuration validation
273
+ * - ExecutionInterface-to-inputSchema validation
274
+ * @throws RegistryValidationError if validation fails
275
+ */
276
+ export function validateDeploymentSpec(orgName: string, resources: DeploymentSpec): void {
277
+ const seenIds = new Set<string>()
278
+
279
+ // Validate workflows
280
+ resources.workflows?.forEach((workflow) => {
281
+ const id = workflow.config.resourceId
282
+
283
+ // Check for duplicate IDs
284
+ if (seenIds.has(id)) {
285
+ throw new RegistryValidationError(
286
+ orgName,
287
+ id,
288
+ null,
289
+ `Duplicate resourceId "${id}" in organization "${orgName}". ` +
290
+ `Workflows and agents must have unique IDs within an organization.`
291
+ )
292
+ }
293
+ seenIds.add(id)
294
+ // Validate model config if present (workflows may optionally have modelConfig)
295
+ if ('modelConfig' in workflow && workflow.modelConfig) {
296
+ validateResourceModelConfig(orgName, id, workflow.modelConfig as ModelConfig)
297
+ }
298
+
299
+ // Validate ExecutionInterface matches inputSchema
300
+ if (workflow.interface) {
301
+ validateExecutionInterface(orgName, id, workflow.interface, workflow.contract.inputSchema)
302
+ }
303
+ })
304
+
305
+ // Validate agents
306
+ resources.agents?.forEach((agent) => {
307
+ const id = agent.config.resourceId
308
+
309
+ // Check for duplicate IDs
310
+ if (seenIds.has(id)) {
311
+ throw new RegistryValidationError(
312
+ orgName,
313
+ id,
314
+ null,
315
+ `Duplicate resourceId "${id}" in organization "${orgName}". ` +
316
+ `Workflows and agents must have unique IDs within an organization.`
317
+ )
318
+ }
319
+ seenIds.add(id)
320
+
321
+ // Validate model config
322
+ validateResourceModelConfig(orgName, id, agent.modelConfig)
323
+
324
+ // Validate ExecutionInterface matches inputSchema
325
+ if (agent.interface) {
326
+ validateExecutionInterface(orgName, id, agent.interface, agent.contract.inputSchema)
327
+ }
328
+ })
329
+
330
+ validateResourceGovernance(orgName, resources)
331
+ }
332
+
333
+ /**
334
+ * Validates model configuration for a resource
335
+ */
336
+ function validateResourceModelConfig(orgName: string, resourceId: string, modelConfig: ModelConfig): void {
337
+ try {
338
+ validateModelConfig(modelConfig)
339
+ } catch (error) {
340
+ if (error instanceof ModelConfigError) {
341
+ throw new RegistryValidationError(
342
+ orgName,
343
+ resourceId,
344
+ error.field,
345
+ `Invalid model config in ${orgName}/${resourceId}: ${error.message} (field: ${error.field})`
346
+ )
347
+ }
348
+ throw error
349
+ }
350
+ }
351
+
352
+ // ============================================================================
353
+ // ExecutionInterface Validation
354
+ // ============================================================================
355
+
356
+ /**
357
+ * Validates that ExecutionInterface form fields match the inputSchema
358
+ *
359
+ * Checks:
360
+ * 1. All required fields in inputSchema have corresponding form fields
361
+ * 2. Form field names match schema field names (or have valid fieldMappings)
362
+ * 3. Required/optional alignment between form and schema
363
+ *
364
+ * @throws RegistryValidationError if interface doesn't match schema
365
+ */
366
+ export function validateExecutionInterface(
367
+ orgName: string,
368
+ resourceId: string,
369
+ executionInterface: {
370
+ form: { fields: Array<{ name: string; required?: boolean }>; fieldMappings?: Record<string, string> }
371
+ },
372
+ inputSchema: z.ZodType
373
+ ): void {
374
+ const form = executionInterface.form
375
+ const fieldMappings = form.fieldMappings ?? {}
376
+
377
+ // Extract schema shape (only works for ZodObject)
378
+ const schemaShape = extractZodShape(inputSchema)
379
+ if (!schemaShape) {
380
+ // Can't validate non-object schemas - skip validation
381
+ return
382
+ }
383
+
384
+ const schemaFieldNames = Object.keys(schemaShape)
385
+
386
+ // Build effective mapping: form field name -> schema field name
387
+ const formToSchemaMap = new Map<string, string>()
388
+ for (const field of form.fields) {
389
+ const schemaKey = fieldMappings[field.name] ?? field.name
390
+ formToSchemaMap.set(field.name, schemaKey)
391
+ }
392
+
393
+ // Check 1: All required schema fields have form fields
394
+ for (const schemaFieldName of schemaFieldNames) {
395
+ const schemaField = schemaShape[schemaFieldName]
396
+ const isRequired = !isZodOptional(schemaField)
397
+
398
+ // Find form field that maps to this schema field
399
+ let hasFormField = false
400
+ for (const [formFieldName, mappedSchemaName] of Array.from(formToSchemaMap.entries())) {
401
+ if (mappedSchemaName === schemaFieldName) {
402
+ hasFormField = true
403
+
404
+ // Check required alignment
405
+ const formField = form.fields.find((f) => f.name === formFieldName)
406
+ if (isRequired && !formField?.required) {
407
+ throw new RegistryValidationError(
408
+ orgName,
409
+ resourceId,
410
+ `interface.form.fields.${formFieldName}`,
411
+ `ExecutionInterface field "${formFieldName}" should be required to match inputSchema field "${schemaFieldName}" in ${orgName}/${resourceId}`
412
+ )
413
+ }
414
+ break
415
+ }
416
+ }
417
+
418
+ if (isRequired && !hasFormField) {
419
+ throw new RegistryValidationError(
420
+ orgName,
421
+ resourceId,
422
+ 'interface.form.fields',
423
+ `ExecutionInterface missing required field "${schemaFieldName}" from inputSchema in ${orgName}/${resourceId}`
424
+ )
425
+ }
426
+ }
427
+
428
+ // Check 2: All form fields map to valid schema fields
429
+ for (const [formFieldName, schemaFieldName] of Array.from(formToSchemaMap.entries())) {
430
+ // Detect nested field notation (e.g., "criteria.targetTitles") and suggest flattening
431
+ if (schemaFieldName.includes('.')) {
432
+ const topLevelField = schemaFieldName.split('.')[0]
433
+ throw new RegistryValidationError(
434
+ orgName,
435
+ resourceId,
436
+ `interface.form.fields.${formFieldName}`,
437
+ `ExecutionInterface field "${formFieldName}" uses nested notation. ` +
438
+ `Flatten the inputSchema by moving nested fields to top-level ` +
439
+ `(e.g., "${topLevelField}.x" → "x") in ${orgName}/${resourceId}`
440
+ )
441
+ }
442
+
443
+ if (!schemaFieldNames.includes(schemaFieldName)) {
444
+ throw new RegistryValidationError(
445
+ orgName,
446
+ resourceId,
447
+ `interface.form.fields.${formFieldName}`,
448
+ `ExecutionInterface field "${formFieldName}" maps to non-existent schema field "${schemaFieldName}" in ${orgName}/${resourceId}`
449
+ )
450
+ }
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Extract shape from a Zod schema (handles ZodObject and wrapped types)
456
+ */
457
+ function extractZodShape(schema: z.ZodType): Record<string, z.ZodType> | null {
458
+ // Handle ZodObject directly
459
+ if ('shape' in schema && typeof schema.shape === 'object') {
460
+ return schema.shape as Record<string, z.ZodType>
461
+ }
462
+
463
+ // Handle wrapped types (ZodEffects, ZodDefault, etc.)
464
+ if ('_def' in schema) {
465
+ const def = schema._def as { innerType?: z.ZodType; schema?: z.ZodType }
466
+ if (def.innerType) {
467
+ return extractZodShape(def.innerType)
468
+ }
469
+ if (def.schema) {
470
+ return extractZodShape(def.schema)
471
+ }
472
+ }
473
+
474
+ return null
475
+ }
476
+
477
+ /**
478
+ * Check if a Zod type is optional (or has a default)
479
+ * Uses isOptional() method which handles ZodOptional, ZodDefault, ZodNullable
480
+ */
481
+ function isZodOptional(schema: z.ZodType): boolean {
482
+ // Zod provides isOptional() which returns true for optional, default, and nullable types
483
+ return schema.isOptional()
484
+ }
485
+
486
+ // ============================================================================
487
+ // Relationship Validation
488
+ // ============================================================================
489
+
490
+ /**
491
+ * Validates relationship declarations reference valid resources
492
+ * @throws RegistryValidationError if validation fails
493
+ */
494
+ export function validateRelationships(orgName: string, resources: DeploymentSpec): void {
495
+ // Skip if no manifest data
496
+ if (!resources.relationships && !resources.triggers && !resources.externalResources && !resources.humanCheckpoints)
497
+ return
498
+
499
+ // Build resource ID sets for validation
500
+ const validAgentIds = new Set(resources.agents?.map((a) => a.config.resourceId) ?? [])
501
+ const validWorkflowIds = new Set(resources.workflows?.map((w) => w.config.resourceId) ?? [])
502
+ const validIntegrationIds = new Set(resources.integrations?.map((i) => i.resourceId) ?? [])
503
+ const validTriggerIds = new Set(resources.triggers?.map((t) => t.resourceId) ?? [])
504
+
505
+ // Collect all internal resource IDs for uniqueness check
506
+ const allInternalIds = new Set([
507
+ ...Array.from(validAgentIds),
508
+ ...Array.from(validWorkflowIds),
509
+ ...Array.from(validTriggerIds),
510
+ ...Array.from(validIntegrationIds)
511
+ ])
512
+
513
+ // Validate triggers
514
+ validateTriggers(orgName, resources.triggers ?? [], validAgentIds, validWorkflowIds)
515
+
516
+ // Validate resource relationships
517
+ validateResourceRelationships(
518
+ orgName,
519
+ resources.relationships ?? {},
520
+ validAgentIds,
521
+ validWorkflowIds,
522
+ validIntegrationIds,
523
+ validTriggerIds
524
+ )
525
+
526
+ // Validate external resources
527
+ validateExternalResources(
528
+ orgName,
529
+ resources.externalResources ?? [],
530
+ allInternalIds,
531
+ validAgentIds,
532
+ validWorkflowIds,
533
+ validIntegrationIds
534
+ )
535
+
536
+ // Validate human checkpoints
537
+ validateHumanCheckpoints(orgName, resources.humanCheckpoints ?? [], allInternalIds, validAgentIds, validWorkflowIds)
538
+ }
539
+
540
+ /**
541
+ * Validates trigger invocations
542
+ * NOTE: Trigger relationships are declared in ResourceRelationships, not on TriggerDefinition
543
+ * This validation is now handled by validateResourceRelationships()
544
+ */
545
+ function validateTriggers(
546
+ _orgName: string,
547
+ _triggers: TriggerDefinition[],
548
+ _validAgentIds: Set<string>,
549
+ _validWorkflowIds: Set<string>
550
+ ): void {
551
+ // No validation needed here - triggers declare their relationships in ResourceRelationships
552
+ // This prevents duplication and keeps all relationship declarations in one place
553
+ }
554
+
555
+ /**
556
+ * Validates resource relationship declarations
557
+ */
558
+ function validateResourceRelationships(
559
+ orgName: string,
560
+ relationships: ResourceRelationships,
561
+ validAgentIds: Set<string>,
562
+ validWorkflowIds: Set<string>,
563
+ validIntegrationIds: Set<string>,
564
+ validTriggerIds: Set<string>
565
+ ): void {
566
+ for (const [resourceId, declaration] of Object.entries(relationships)) {
567
+ // Validate declaring resource exists (agents, workflows, or triggers can declare relationships)
568
+ const resourceExists =
569
+ validAgentIds.has(resourceId) || validWorkflowIds.has(resourceId) || validTriggerIds.has(resourceId)
570
+ if (!resourceExists) {
571
+ throw new RegistryValidationError(
572
+ orgName,
573
+ resourceId,
574
+ null,
575
+ `[${orgName}] Relationship declared for non-existent resource: ${resourceId}`
576
+ )
577
+ }
578
+
579
+ // Validate triggers.agents
580
+ declaration.triggers?.agents?.forEach((agentId) => {
581
+ if (!validAgentIds.has(agentId)) {
582
+ throw new RegistryValidationError(
583
+ orgName,
584
+ resourceId,
585
+ 'triggers.agents',
586
+ `[${orgName}] Resource '${resourceId}' triggers non-existent agent: ${agentId}`
587
+ )
588
+ }
589
+ })
590
+
591
+ // Validate triggers.workflows
592
+ declaration.triggers?.workflows?.forEach((workflowId) => {
593
+ if (!validWorkflowIds.has(workflowId)) {
594
+ throw new RegistryValidationError(
595
+ orgName,
596
+ resourceId,
597
+ 'triggers.workflows',
598
+ `[${orgName}] Resource '${resourceId}' triggers non-existent workflow: ${workflowId}`
599
+ )
600
+ }
601
+ })
602
+
603
+ // Validate uses.integrations
604
+ declaration.uses?.integrations?.forEach((integrationId) => {
605
+ if (!validIntegrationIds.has(integrationId)) {
606
+ throw new RegistryValidationError(
607
+ orgName,
608
+ resourceId,
609
+ 'uses.integrations',
610
+ `[${orgName}] Resource '${resourceId}' uses non-existent integration: ${integrationId}`
611
+ )
612
+ }
613
+ })
614
+ }
615
+ }
616
+
617
+ /**
618
+ * Validates external resource definitions
619
+ */
620
+ function validateExternalResources(
621
+ orgName: string,
622
+ externalResources: ExternalResourceDefinition[],
623
+ allInternalIds: Set<string>,
624
+ validAgentIds: Set<string>,
625
+ validWorkflowIds: Set<string>,
626
+ validIntegrationIds: Set<string>
627
+ ): void {
628
+ externalResources.forEach((external) => {
629
+ // Validate unique ID (no conflict with internal resources)
630
+ if (allInternalIds.has(external.resourceId)) {
631
+ throw new RegistryValidationError(
632
+ orgName,
633
+ external.resourceId,
634
+ null,
635
+ `[${orgName}] External resource ID '${external.resourceId}' conflicts with internal resource ID`
636
+ )
637
+ }
638
+
639
+ // Validate triggers (external -> internal)
640
+ external.triggers?.agents?.forEach((agentId) => {
641
+ if (!validAgentIds.has(agentId)) {
642
+ throw new RegistryValidationError(
643
+ orgName,
644
+ external.resourceId,
645
+ 'triggers.agents',
646
+ `[${orgName}] External resource '${external.resourceId}' triggers non-existent agent: ${agentId}`
647
+ )
648
+ }
649
+ })
650
+
651
+ external.triggers?.workflows?.forEach((workflowId) => {
652
+ if (!validWorkflowIds.has(workflowId)) {
653
+ throw new RegistryValidationError(
654
+ orgName,
655
+ external.resourceId,
656
+ 'triggers.workflows',
657
+ `[${orgName}] External resource '${external.resourceId}' triggers non-existent workflow: ${workflowId}`
658
+ )
659
+ }
660
+ })
661
+
662
+ // Validate uses integrations
663
+ external.uses?.integrations?.forEach((integrationId) => {
664
+ if (!validIntegrationIds.has(integrationId)) {
665
+ throw new RegistryValidationError(
666
+ orgName,
667
+ external.resourceId,
668
+ 'uses.integrations',
669
+ `[${orgName}] External resource '${external.resourceId}' uses non-existent integration: ${integrationId}`
670
+ )
671
+ }
672
+ })
673
+ })
674
+ }
675
+
676
+ /**
677
+ * Validates human checkpoint definitions
678
+ */
679
+ function validateHumanCheckpoints(
680
+ orgName: string,
681
+ humanCheckpoints: HumanCheckpointDefinition[],
682
+ allInternalIds: Set<string>,
683
+ validAgentIds: Set<string>,
684
+ validWorkflowIds: Set<string>
685
+ ): void {
686
+ humanCheckpoints.forEach((humanCheckpoint) => {
687
+ // Check for ID conflicts with internal resources
688
+ if (allInternalIds.has(humanCheckpoint.resourceId)) {
689
+ throw new RegistryValidationError(
690
+ orgName,
691
+ humanCheckpoint.resourceId,
692
+ null,
693
+ `[${orgName}] Human checkpoint ID '${humanCheckpoint.resourceId}' conflicts with internal resource ID`
694
+ )
695
+ }
696
+
697
+ // Validate requestedBy.agents exist
698
+ humanCheckpoint.requestedBy?.agents?.forEach((agentId) => {
699
+ if (!validAgentIds.has(agentId)) {
700
+ throw new RegistryValidationError(
701
+ orgName,
702
+ humanCheckpoint.resourceId,
703
+ 'requestedBy.agents',
704
+ `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' requestedBy non-existent agent: ${agentId}`
705
+ )
706
+ }
707
+ })
708
+
709
+ // Validate requestedBy.workflows exist
710
+ humanCheckpoint.requestedBy?.workflows?.forEach((workflowId) => {
711
+ if (!validWorkflowIds.has(workflowId)) {
712
+ throw new RegistryValidationError(
713
+ orgName,
714
+ humanCheckpoint.resourceId,
715
+ 'requestedBy.workflows',
716
+ `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' requestedBy non-existent workflow: ${workflowId}`
717
+ )
718
+ }
719
+ })
720
+
721
+ // Validate routesTo.agents exist
722
+ humanCheckpoint.routesTo?.agents?.forEach((agentId) => {
723
+ if (!validAgentIds.has(agentId)) {
724
+ throw new RegistryValidationError(
725
+ orgName,
726
+ humanCheckpoint.resourceId,
727
+ 'routesTo.agents',
728
+ `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' routesTo non-existent agent: ${agentId}`
729
+ )
730
+ }
731
+ })
732
+
733
+ // Validate routesTo.workflows exist
734
+ humanCheckpoint.routesTo?.workflows?.forEach((workflowId) => {
735
+ if (!validWorkflowIds.has(workflowId)) {
736
+ throw new RegistryValidationError(
737
+ orgName,
738
+ humanCheckpoint.resourceId,
739
+ 'routesTo.workflows',
740
+ `[${orgName}] Human checkpoint '${humanCheckpoint.resourceId}' routesTo non-existent workflow: ${workflowId}`
741
+ )
742
+ }
743
+ })
744
+ })
745
+ }