@elevasis/core 0.23.0 → 0.24.1

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 (243) hide show
  1. package/dist/index.d.ts +4343 -2690
  2. package/dist/index.js +1101 -156
  3. package/dist/knowledge/index.d.ts +574 -210
  4. package/dist/knowledge/index.js +104 -1
  5. package/dist/organization-model/index.d.ts +4343 -2690
  6. package/dist/organization-model/index.js +1101 -156
  7. package/dist/test-utils/index.d.ts +483 -109
  8. package/dist/test-utils/index.js +904 -144
  9. package/package.json +3 -3
  10. package/src/README.md +14 -14
  11. package/src/__tests__/publish.test.ts +24 -24
  12. package/src/__tests__/template-core-compatibility.test.ts +9 -12
  13. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +2137 -2093
  14. package/src/_gen/__tests__/scaffold-contracts.test.ts +30 -30
  15. package/src/auth/multi-tenancy/credentials/__tests__/encryption.test.ts +217 -217
  16. package/src/auth/multi-tenancy/credentials/server/encryption.ts +69 -69
  17. package/src/auth/multi-tenancy/credentials/server/kek-loader.ts +37 -37
  18. package/src/auth/multi-tenancy/index.ts +26 -26
  19. package/src/auth/multi-tenancy/invitations/api-schemas.ts +104 -104
  20. package/src/auth/multi-tenancy/memberships/api-schemas.ts +143 -143
  21. package/src/auth/multi-tenancy/memberships/index.ts +26 -26
  22. package/src/auth/multi-tenancy/memberships/membership.ts +130 -130
  23. package/src/auth/multi-tenancy/organizations/__tests__/api-schemas.test.ts +194 -194
  24. package/src/auth/multi-tenancy/organizations/api-schemas.ts +136 -136
  25. package/src/auth/multi-tenancy/permissions.test.ts +42 -42
  26. package/src/auth/multi-tenancy/permissions.ts +123 -123
  27. package/src/auth/multi-tenancy/role-management/api-schemas.ts +78 -78
  28. package/src/auth/multi-tenancy/role-management/index.ts +16 -16
  29. package/src/auth/multi-tenancy/theme-presets.ts +45 -45
  30. package/src/auth/multi-tenancy/types.ts +57 -57
  31. package/src/auth/multi-tenancy/users/api-schemas.ts +165 -165
  32. package/src/business/README.md +2 -2
  33. package/src/business/acquisition/activity-events.test.ts +250 -250
  34. package/src/business/acquisition/activity-events.ts +93 -93
  35. package/src/business/acquisition/api-schemas.test.ts +1883 -1843
  36. package/src/business/acquisition/api-schemas.ts +1492 -1497
  37. package/src/business/acquisition/build-templates.test.ts +240 -240
  38. package/src/business/acquisition/build-templates.ts +98 -98
  39. package/src/business/acquisition/crm-next-action.test.ts +262 -262
  40. package/src/business/acquisition/crm-next-action.ts +220 -220
  41. package/src/business/acquisition/crm-priority.test.ts +216 -216
  42. package/src/business/acquisition/crm-priority.ts +349 -349
  43. package/src/business/acquisition/crm-state-actions.test.ts +153 -153
  44. package/src/business/acquisition/deal-ownership.test.ts +351 -351
  45. package/src/business/acquisition/deal-ownership.ts +120 -120
  46. package/src/business/acquisition/derive-actions.test.ts +129 -104
  47. package/src/business/acquisition/derive-actions.ts +74 -84
  48. package/src/business/acquisition/index.ts +171 -170
  49. package/src/business/acquisition/ontology-validation.ts +309 -0
  50. package/src/business/acquisition/stateful.ts +30 -30
  51. package/src/business/acquisition/types.ts +396 -396
  52. package/src/business/clients/api-schemas.test.ts +115 -115
  53. package/src/business/clients/api-schemas.ts +158 -158
  54. package/src/business/clients/index.ts +1 -1
  55. package/src/business/crm/api-schemas.ts +40 -40
  56. package/src/business/crm/index.ts +1 -1
  57. package/src/business/deals/api-schemas.ts +87 -87
  58. package/src/business/deals/index.ts +1 -1
  59. package/src/business/index.ts +5 -5
  60. package/src/business/projects/types.ts +144 -144
  61. package/src/commands/queue/types/task.ts +15 -15
  62. package/src/execution/core/runner-types.ts +61 -61
  63. package/src/execution/core/sse-executions.ts +7 -7
  64. package/src/execution/engine/__tests__/fixtures/test-agents.ts +10 -10
  65. package/src/execution/engine/agent/core/__tests__/agent.test.ts +16 -16
  66. package/src/execution/engine/agent/core/__tests__/error-passthrough.test.ts +4 -4
  67. package/src/execution/engine/agent/core/types.ts +25 -25
  68. package/src/execution/engine/agent/index.ts +6 -6
  69. package/src/execution/engine/agent/reasoning/__tests__/request-builder.test.ts +24 -24
  70. package/src/execution/engine/index.ts +443 -443
  71. package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +298 -298
  72. package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.test.ts +55 -55
  73. package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.ts +107 -107
  74. package/src/execution/engine/tools/integration/server/adapters/apollo/apollo-adapter.test.ts +48 -48
  75. package/src/execution/engine/tools/integration/server/adapters/apollo/apollo-adapter.ts +99 -99
  76. package/src/execution/engine/tools/integration/server/adapters/apollo/index.ts +1 -1
  77. package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +363 -363
  78. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.test.ts +162 -162
  79. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.test.ts +316 -316
  80. package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.test.ts +18 -18
  81. package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.ts +194 -194
  82. package/src/execution/engine/tools/integration/server/adapters/clickup/index.ts +7 -7
  83. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +204 -204
  84. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +105 -105
  85. package/src/execution/engine/tools/integration/server/adapters/google-calendar/google-calendar-adapter.ts +428 -428
  86. package/src/execution/engine/tools/integration/server/adapters/google-calendar/index.ts +2 -2
  87. package/src/execution/engine/tools/integration/server/adapters/google-sheets/__tests__/google-sheets.integration.test.ts +261 -261
  88. package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1474 -1474
  89. package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +103 -103
  90. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.test.ts +88 -88
  91. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +141 -141
  92. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +76 -76
  93. package/src/execution/engine/tools/integration/server/adapters/signature-api/signature-api-tools.ts +182 -182
  94. package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +310 -310
  95. package/src/execution/engine/tools/integration/service.test.ts +239 -239
  96. package/src/execution/engine/tools/integration/service.ts +172 -172
  97. package/src/execution/engine/tools/integration/tool.ts +255 -255
  98. package/src/execution/engine/tools/lead-service-types.ts +1005 -1005
  99. package/src/execution/engine/tools/messages.ts +43 -43
  100. package/src/execution/engine/tools/platform/acquisition/company-tools.ts +7 -7
  101. package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +6 -6
  102. package/src/execution/engine/tools/platform/acquisition/list-tools.ts +6 -6
  103. package/src/execution/engine/tools/platform/acquisition/types.ts +280 -280
  104. package/src/execution/engine/tools/platform/email/types.ts +97 -97
  105. package/src/execution/engine/tools/registry.ts +704 -704
  106. package/src/execution/engine/tools/tool-maps.ts +831 -831
  107. package/src/execution/engine/tools/types.ts +234 -234
  108. package/src/execution/engine/workflow/types.ts +195 -197
  109. package/src/execution/external/__tests__/api-schemas.test.ts +127 -127
  110. package/src/execution/external/api-schemas.ts +40 -40
  111. package/src/execution/external/index.ts +1 -1
  112. package/src/index.ts +18 -18
  113. package/src/integrations/credentials/__tests__/api-schemas.test.ts +420 -420
  114. package/src/integrations/credentials/api-schemas.ts +146 -146
  115. package/src/integrations/credentials/schemas.ts +200 -200
  116. package/src/integrations/oauth/__tests__/provider-registry.test.ts +7 -7
  117. package/src/integrations/oauth/provider-registry.ts +74 -74
  118. package/src/integrations/oauth/server/credentials.ts +43 -43
  119. package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +327 -327
  120. package/src/integrations/webhook-endpoints/api-schemas.ts +103 -103
  121. package/src/integrations/webhook-endpoints/types.ts +58 -58
  122. package/src/knowledge/README.md +32 -32
  123. package/src/knowledge/__tests__/queries.test.ts +626 -535
  124. package/src/knowledge/format.ts +99 -99
  125. package/src/knowledge/index.ts +5 -5
  126. package/src/knowledge/published.ts +5 -5
  127. package/src/knowledge/queries.ts +269 -218
  128. package/src/operations/activities/api-schemas.ts +80 -80
  129. package/src/operations/activities/types.ts +64 -64
  130. package/src/organization-model/README.md +149 -149
  131. package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -210
  132. package/src/organization-model/__tests__/defaults.test.ts +168 -168
  133. package/src/organization-model/__tests__/domains/actions.test.ts +78 -56
  134. package/src/organization-model/__tests__/domains/customers.test.ts +299 -299
  135. package/src/organization-model/__tests__/domains/entities.test.ts +56 -56
  136. package/src/organization-model/__tests__/domains/goals.test.ts +493 -493
  137. package/src/organization-model/__tests__/domains/identity.test.ts +280 -280
  138. package/src/organization-model/__tests__/domains/navigation.test.ts +268 -268
  139. package/src/organization-model/__tests__/domains/offerings.test.ts +414 -414
  140. package/src/organization-model/__tests__/domains/policies.test.ts +323 -323
  141. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +293 -293
  142. package/src/organization-model/__tests__/domains/resources.test.ts +387 -277
  143. package/src/organization-model/__tests__/domains/roles.test.ts +463 -463
  144. package/src/organization-model/__tests__/domains/statuses.test.ts +246 -246
  145. package/src/organization-model/__tests__/domains/systems.test.ts +209 -209
  146. package/src/organization-model/__tests__/domains/topology.test.ts +188 -0
  147. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +362 -361
  148. package/src/organization-model/__tests__/foundation.test.ts +77 -77
  149. package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -144
  150. package/src/organization-model/__tests__/graph.test.ts +1312 -862
  151. package/src/organization-model/__tests__/icons.test.ts +10 -1
  152. package/src/organization-model/__tests__/knowledge.test.ts +251 -15
  153. package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -438
  154. package/src/organization-model/__tests__/migration-helpers.test.ts +591 -591
  155. package/src/organization-model/__tests__/prospecting-ssot.test.ts +103 -103
  156. package/src/organization-model/__tests__/recursive-system-schema.test.ts +535 -506
  157. package/src/organization-model/__tests__/resolve.test.ts +274 -164
  158. package/src/organization-model/__tests__/schema.test.ts +844 -301
  159. package/src/organization-model/__tests__/surface-projection.test.ts +284 -284
  160. package/src/organization-model/catalogs/lead-gen.ts +144 -144
  161. package/src/organization-model/content-kinds/config.ts +36 -36
  162. package/src/organization-model/content-kinds/index.ts +76 -72
  163. package/src/organization-model/content-kinds/pipeline.ts +68 -68
  164. package/src/organization-model/content-kinds/registry.ts +44 -44
  165. package/src/organization-model/content-kinds/status.ts +71 -71
  166. package/src/organization-model/content-kinds/template.ts +83 -83
  167. package/src/organization-model/content-kinds/types.ts +117 -117
  168. package/src/organization-model/contracts.ts +27 -27
  169. package/src/organization-model/defaults.ts +42 -50
  170. package/src/organization-model/domains/actions.ts +333 -239
  171. package/src/organization-model/domains/customers.ts +78 -78
  172. package/src/organization-model/domains/entities.ts +144 -144
  173. package/src/organization-model/domains/goals.ts +83 -83
  174. package/src/organization-model/domains/knowledge.ts +117 -101
  175. package/src/organization-model/domains/navigation.ts +139 -139
  176. package/src/organization-model/domains/offerings.ts +71 -71
  177. package/src/organization-model/domains/policies.ts +102 -102
  178. package/src/organization-model/domains/projects.ts +14 -14
  179. package/src/organization-model/domains/prospecting.ts +395 -395
  180. package/src/organization-model/domains/resources.ts +202 -124
  181. package/src/organization-model/domains/roles.ts +96 -96
  182. package/src/organization-model/domains/sales.test.ts +218 -218
  183. package/src/organization-model/domains/sales.ts +380 -380
  184. package/src/organization-model/domains/shared.ts +63 -63
  185. package/src/organization-model/domains/statuses.ts +339 -339
  186. package/src/organization-model/domains/systems.ts +217 -172
  187. package/src/organization-model/domains/topology.ts +261 -0
  188. package/src/organization-model/foundation.ts +75 -75
  189. package/src/organization-model/graph/build.ts +1043 -867
  190. package/src/organization-model/graph/index.ts +4 -4
  191. package/src/organization-model/graph/link.ts +10 -10
  192. package/src/organization-model/graph/schema.ts +75 -68
  193. package/src/organization-model/graph/types.ts +71 -64
  194. package/src/organization-model/helpers.ts +289 -241
  195. package/src/organization-model/icons.ts +78 -66
  196. package/src/organization-model/index.ts +128 -125
  197. package/src/organization-model/migration-helpers.ts +247 -244
  198. package/src/organization-model/ontology.ts +658 -0
  199. package/src/organization-model/organization-graph.mdx +110 -90
  200. package/src/organization-model/organization-model.mdx +225 -213
  201. package/src/organization-model/published.ts +299 -222
  202. package/src/organization-model/resolve.ts +146 -91
  203. package/src/organization-model/schema.ts +818 -659
  204. package/src/organization-model/surface-projection.ts +212 -212
  205. package/src/organization-model/types.ts +179 -155
  206. package/src/platform/api/types.ts +38 -38
  207. package/src/platform/constants/versions.ts +3 -3
  208. package/src/platform/index.ts +23 -23
  209. package/src/platform/registry/__tests__/command-view.test.ts +10 -10
  210. package/src/platform/registry/__tests__/resource-link.test.ts +35 -35
  211. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +20 -20
  212. package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -245
  213. package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2053
  214. package/src/platform/registry/__tests__/validation.test.ts +1444 -1259
  215. package/src/platform/registry/command-view.ts +10 -10
  216. package/src/platform/registry/index.ts +103 -103
  217. package/src/platform/registry/resource-link.ts +32 -32
  218. package/src/platform/registry/resource-registry.ts +886 -886
  219. package/src/platform/registry/serialization.ts +295 -295
  220. package/src/platform/registry/serialized-types.ts +166 -166
  221. package/src/platform/registry/stats-types.ts +68 -68
  222. package/src/platform/registry/types.ts +425 -425
  223. package/src/platform/registry/validation.ts +876 -684
  224. package/src/platform/utils/__tests__/validation.test.ts +1084 -1084
  225. package/src/platform/utils/validation.ts +425 -425
  226. package/src/projects/api-schemas.test.ts +39 -39
  227. package/src/projects/api-schemas.ts +291 -291
  228. package/src/reference/_generated/contracts.md +2136 -2093
  229. package/src/reference/glossary.md +76 -76
  230. package/src/scaffold-registry/__tests__/index.test.ts +206 -206
  231. package/src/scaffold-registry/__tests__/schema.test.ts +166 -166
  232. package/src/scaffold-registry/index.ts +392 -392
  233. package/src/scaffold-registry/schema.ts +243 -243
  234. package/src/server.ts +289 -289
  235. package/src/supabase/database.types.ts +3 -0
  236. package/src/test-utils/README.md +37 -37
  237. package/src/test-utils/entities.ts +108 -108
  238. package/src/test-utils/fixtures/memberships.ts +82 -82
  239. package/src/test-utils/index.ts +12 -12
  240. package/src/test-utils/organization-model.ts +65 -65
  241. package/src/test-utils/published.ts +6 -6
  242. package/src/test-utils/rls/RLSTestContext.ts +588 -588
  243. package/src/test-utils/test-utils.test.ts +44 -44
@@ -1,750 +1,905 @@
1
- import { z } from 'zod'
2
- import { lookupContentType } from './content-kinds/index'
3
- import { OrganizationModelBrandingSchema } from './domains/branding'
4
- import { OrganizationModelNavigationSchema, type SidebarNode } from './domains/navigation'
5
- import { IdentityDomainSchema, DEFAULT_ORGANIZATION_MODEL_IDENTITY } from './domains/identity'
6
- import { CustomersDomainSchema, DEFAULT_ORGANIZATION_MODEL_CUSTOMERS } from './domains/customers'
7
- import { OfferingsDomainSchema, DEFAULT_ORGANIZATION_MODEL_OFFERINGS } from './domains/offerings'
8
- import { RolesDomainSchema, DEFAULT_ORGANIZATION_MODEL_ROLES } from './domains/roles'
9
- import { GoalsDomainSchema, DEFAULT_ORGANIZATION_MODEL_GOALS } from './domains/goals'
10
- import { KnowledgeDomainSchema } from './domains/knowledge'
1
+ import { z } from 'zod'
2
+ import { lookupContentType } from './content-kinds/index'
3
+ import { OrganizationModelBrandingSchema } from './domains/branding'
4
+ import { OrganizationModelNavigationSchema, type SidebarNode } from './domains/navigation'
5
+ import { IdentityDomainSchema, DEFAULT_ORGANIZATION_MODEL_IDENTITY } from './domains/identity'
6
+ import { CustomersDomainSchema, DEFAULT_ORGANIZATION_MODEL_CUSTOMERS } from './domains/customers'
7
+ import { OfferingsDomainSchema, DEFAULT_ORGANIZATION_MODEL_OFFERINGS } from './domains/offerings'
8
+ import { RolesDomainSchema, DEFAULT_ORGANIZATION_MODEL_ROLES } from './domains/roles'
9
+ import { GoalsDomainSchema, DEFAULT_ORGANIZATION_MODEL_GOALS } from './domains/goals'
10
+ import { KnowledgeDomainSchema } from './domains/knowledge'
11
11
  import { SystemsDomainSchema, DEFAULT_ORGANIZATION_MODEL_SYSTEMS, type SystemEntry } from './domains/systems'
12
12
  import { ResourcesDomainSchema, DEFAULT_ORGANIZATION_MODEL_RESOURCES } from './domains/resources'
13
+ import { OmTopologyDomainSchema, DEFAULT_ORGANIZATION_MODEL_TOPOLOGY, type OmTopologyNodeRef } from './domains/topology'
13
14
  import { ActionsDomainSchema, DEFAULT_ORGANIZATION_MODEL_ACTIONS } from './domains/actions'
14
15
  import { EntitiesDomainSchema, DEFAULT_ORGANIZATION_MODEL_ENTITIES } from './domains/entities'
15
16
  import { PoliciesDomainSchema, DEFAULT_ORGANIZATION_MODEL_POLICIES } from './domains/policies'
16
-
17
- // Phase 4 cut: 'sales', 'prospecting', 'projects', 'statuses' removed.
18
- // domainMetadata.knowledge covers versioning for the knowledge flat-map (D7).
19
- export const OrganizationModelDomainKeySchema = z.enum([
20
- 'branding',
21
- 'identity',
22
- 'customers',
23
- 'offerings',
24
- 'roles',
25
- 'goals',
17
+ import {
18
+ compileOrganizationOntology,
19
+ DEFAULT_ONTOLOGY_SCOPE,
20
+ listResolvedOntologyRecords,
21
+ OntologyScopeSchema,
22
+ type OntologyKind
23
+ } from './ontology'
24
+
25
+ // Phase 4 cut: 'sales', 'prospecting', 'projects', 'statuses' removed.
26
+ // domainMetadata.knowledge covers versioning for the knowledge flat-map (D7).
27
+ export const OrganizationModelDomainKeySchema = z.enum([
28
+ 'branding',
29
+ 'identity',
30
+ 'customers',
31
+ 'offerings',
32
+ 'roles',
33
+ 'goals',
26
34
  'systems',
35
+ 'ontology',
27
36
  'resources',
37
+ 'topology',
28
38
  'actions',
29
- 'entities',
30
- 'policies',
31
- 'knowledge'
32
- ])
33
-
34
- export const OrganizationModelDomainMetadataSchema = z.object({
35
- version: z.literal(1).default(1),
36
- lastModified: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'lastModified must be an ISO date string (YYYY-MM-DD)')
37
- })
38
-
39
- export const DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA: Record<
40
- z.infer<typeof OrganizationModelDomainKeySchema>,
41
- z.infer<typeof OrganizationModelDomainMetadataSchema>
42
- > = {
43
- branding: { version: 1, lastModified: '2026-05-10' },
44
- identity: { version: 1, lastModified: '2026-05-10' },
45
- customers: { version: 1, lastModified: '2026-05-10' },
46
- offerings: { version: 1, lastModified: '2026-05-10' },
47
- roles: { version: 1, lastModified: '2026-05-10' },
48
- goals: { version: 1, lastModified: '2026-05-10' },
39
+ 'entities',
40
+ 'policies',
41
+ 'knowledge'
42
+ ])
43
+
44
+ export const OrganizationModelDomainMetadataSchema = z.object({
45
+ version: z.literal(1).default(1),
46
+ lastModified: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'lastModified must be an ISO date string (YYYY-MM-DD)')
47
+ })
48
+
49
+ export const DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA: Record<
50
+ z.infer<typeof OrganizationModelDomainKeySchema>,
51
+ z.infer<typeof OrganizationModelDomainMetadataSchema>
52
+ > = {
53
+ branding: { version: 1, lastModified: '2026-05-10' },
54
+ identity: { version: 1, lastModified: '2026-05-10' },
55
+ customers: { version: 1, lastModified: '2026-05-10' },
56
+ offerings: { version: 1, lastModified: '2026-05-10' },
57
+ roles: { version: 1, lastModified: '2026-05-10' },
58
+ goals: { version: 1, lastModified: '2026-05-10' },
49
59
  systems: { version: 1, lastModified: '2026-05-10' },
60
+ ontology: { version: 1, lastModified: '2026-05-14' },
50
61
  resources: { version: 1, lastModified: '2026-05-10' },
62
+ topology: { version: 1, lastModified: '2026-05-14' },
51
63
  actions: { version: 1, lastModified: '2026-05-10' },
52
- entities: { version: 1, lastModified: '2026-05-10' },
53
- policies: { version: 1, lastModified: '2026-05-10' },
54
- knowledge: { version: 1, lastModified: '2026-05-10' }
55
- }
56
-
57
- export const OrganizationModelDomainMetadataByDomainSchema = z
58
- .object({
59
- branding: OrganizationModelDomainMetadataSchema,
60
- identity: OrganizationModelDomainMetadataSchema,
61
- customers: OrganizationModelDomainMetadataSchema,
62
- offerings: OrganizationModelDomainMetadataSchema,
63
- roles: OrganizationModelDomainMetadataSchema,
64
- goals: OrganizationModelDomainMetadataSchema,
64
+ entities: { version: 1, lastModified: '2026-05-10' },
65
+ policies: { version: 1, lastModified: '2026-05-10' },
66
+ knowledge: { version: 1, lastModified: '2026-05-10' }
67
+ }
68
+
69
+ export const OrganizationModelDomainMetadataByDomainSchema = z
70
+ .object({
71
+ branding: OrganizationModelDomainMetadataSchema,
72
+ identity: OrganizationModelDomainMetadataSchema,
73
+ customers: OrganizationModelDomainMetadataSchema,
74
+ offerings: OrganizationModelDomainMetadataSchema,
75
+ roles: OrganizationModelDomainMetadataSchema,
76
+ goals: OrganizationModelDomainMetadataSchema,
65
77
  systems: OrganizationModelDomainMetadataSchema,
78
+ ontology: OrganizationModelDomainMetadataSchema,
66
79
  resources: OrganizationModelDomainMetadataSchema,
80
+ topology: OrganizationModelDomainMetadataSchema,
67
81
  actions: OrganizationModelDomainMetadataSchema,
68
- entities: OrganizationModelDomainMetadataSchema,
69
- policies: OrganizationModelDomainMetadataSchema,
70
- knowledge: OrganizationModelDomainMetadataSchema
71
- })
72
- .partial()
73
- .default(DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA)
74
- .transform((metadata) => ({ ...DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA, ...metadata }))
75
-
76
- // Phase 4 schema cut (D8):
77
- // REMOVED top-level fields: sales, prospecting, projects, statuses
78
- // ADDED navigation.sidebar as the authored shell tree
79
- // knowledge: now Record<id, OrgKnowledgeNode> flat map (D3); version/lastModified in domainMetadata.knowledge (D7)
80
- //
81
- // Clean migration rule:
82
- // NO top-level surfaces / navigationGroups compatibility fields.
83
- // Surfaces are derived from navigation.sidebar routeable leaves.
84
- const OrganizationModelSchemaBase = z.object({
85
- version: z.literal(1).default(1),
86
- domainMetadata: OrganizationModelDomainMetadataByDomainSchema,
87
- branding: OrganizationModelBrandingSchema,
88
- navigation: OrganizationModelNavigationSchema,
89
- identity: IdentityDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_IDENTITY),
90
- customers: CustomersDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_CUSTOMERS),
91
- offerings: OfferingsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_OFFERINGS),
92
- roles: RolesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_ROLES),
93
- goals: GoalsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_GOALS),
82
+ entities: OrganizationModelDomainMetadataSchema,
83
+ policies: OrganizationModelDomainMetadataSchema,
84
+ knowledge: OrganizationModelDomainMetadataSchema
85
+ })
86
+ .partial()
87
+ .default(DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA)
88
+ .transform((metadata) => ({ ...DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA, ...metadata }))
89
+
90
+ // Phase 4 schema cut (D8):
91
+ // REMOVED top-level fields: sales, prospecting, projects, statuses
92
+ // ADDED navigation.sidebar as the authored shell tree
93
+ // knowledge: now Record<id, OrgKnowledgeNode> flat map (D3); version/lastModified in domainMetadata.knowledge (D7)
94
+ //
95
+ // Clean migration rule:
96
+ // NO top-level surfaces / navigationGroups compatibility fields.
97
+ // Surfaces are derived from navigation.sidebar routeable leaves.
98
+ const OrganizationModelSchemaBase = z.object({
99
+ version: z.literal(1).default(1),
100
+ domainMetadata: OrganizationModelDomainMetadataByDomainSchema,
101
+ branding: OrganizationModelBrandingSchema,
102
+ navigation: OrganizationModelNavigationSchema,
103
+ identity: IdentityDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_IDENTITY),
104
+ customers: CustomersDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_CUSTOMERS),
105
+ offerings: OfferingsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_OFFERINGS),
106
+ roles: RolesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_ROLES),
107
+ goals: GoalsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_GOALS),
94
108
  systems: SystemsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_SYSTEMS),
109
+ ontology: OntologyScopeSchema.default(DEFAULT_ONTOLOGY_SCOPE),
95
110
  resources: ResourcesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_RESOURCES),
111
+ topology: OmTopologyDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_TOPOLOGY),
96
112
  actions: ActionsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_ACTIONS),
97
- entities: EntitiesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_ENTITIES),
98
- policies: PoliciesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_POLICIES),
99
- // D3: flat Record<id, OrgKnowledgeNode> — no wrapper object
100
- knowledge: KnowledgeDomainSchema.default({})
101
- })
102
-
103
- function addIssue(ctx: z.RefinementCtx, path: Array<string | number>, message: string): void {
104
- ctx.addIssue({
105
- code: z.ZodIssueCode.custom,
106
- path,
107
- message
108
- })
109
- }
110
-
111
- function isLifecycleEnabled(lifecycle: string | undefined, enabled: boolean | undefined): boolean {
112
- if (enabled === false) return false
113
- return lifecycle !== 'deprecated' && lifecycle !== 'archived'
114
- }
115
-
116
- function defaultSystemPathFor(id: string): string {
117
- return `/${id.replaceAll('.', '/')}`
118
- }
119
-
120
- function asRoleHolderArray(
121
- heldBy: NonNullable<z.infer<typeof OrganizationModelSchemaBase>['roles'][string]['heldBy']>
122
- ) {
123
- return Array.isArray(heldBy) ? heldBy : [heldBy]
124
- }
125
-
113
+ entities: EntitiesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_ENTITIES),
114
+ policies: PoliciesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_POLICIES),
115
+ // D3: flat Record<id, OrgKnowledgeNode> — no wrapper object
116
+ knowledge: KnowledgeDomainSchema.default({})
117
+ })
118
+
119
+ function addIssue(ctx: z.RefinementCtx, path: Array<string | number>, message: string): void {
120
+ ctx.addIssue({
121
+ code: z.ZodIssueCode.custom,
122
+ path,
123
+ message
124
+ })
125
+ }
126
+
127
+ function isLifecycleEnabled(lifecycle: string | undefined, enabled: boolean | undefined): boolean {
128
+ if (enabled === false) return false
129
+ return lifecycle !== 'deprecated' && lifecycle !== 'archived'
130
+ }
131
+
132
+ function defaultSystemPathFor(id: string): string {
133
+ return `/${id.replaceAll('.', '/')}`
134
+ }
135
+
136
+ function asRoleHolderArray(
137
+ heldBy: NonNullable<z.infer<typeof OrganizationModelSchemaBase>['roles'][string]['heldBy']>
138
+ ) {
139
+ return Array.isArray(heldBy) ? heldBy : [heldBy]
140
+ }
141
+
126
142
  function isKnowledgeKindCompatibleWithTarget(knowledgeKind: string, targetKind: string): boolean {
127
- if (knowledgeKind === 'reference') return true
143
+ if (knowledgeKind === 'reference') return true
128
144
  if (knowledgeKind === 'playbook') {
129
- return ['system', 'resource', 'stage', 'action'].includes(targetKind)
145
+ return ['system', 'resource', 'stage', 'action', 'ontology'].includes(targetKind)
130
146
  }
131
147
  if (knowledgeKind === 'strategy') {
132
- return ['system', 'goal', 'offering', 'customer-segment'].includes(targetKind)
148
+ return ['system', 'goal', 'offering', 'customer-segment', 'ontology'].includes(targetKind)
133
149
  }
134
150
  return false
135
151
  }
136
152
 
137
- export const OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((model, ctx) => {
138
- // Collect ALL system entries recursively top-level systems plus any nested subsystems.
139
- // Wave 2 canonical OM authors nested subsystems (e.g. sys → subsystems → 'lead-gen' with id
140
- // 'sys.lead-gen'). Resource systemPath cross-refs must resolve against the full flattened set.
141
- type SystemWithPath = { path: string; schemaPath: Array<string | number>; system: SystemEntry }
142
-
153
+ function isRecord(value: unknown): value is Record<string, unknown> {
154
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
155
+ }
156
+
157
+ export const OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((model, ctx) => {
158
+ // Collect ALL system entries recursively — top-level systems plus any nested subsystems.
159
+ // Wave 2 canonical OM authors nested subsystems (e.g. sys → subsystems → 'lead-gen' with id
160
+ // 'sys.lead-gen'). Resource systemPath cross-refs must resolve against the full flattened set.
161
+ type SystemWithPath = { path: string; schemaPath: Array<string | number>; system: SystemEntry }
162
+
143
163
  function collectAllSystems(
144
164
  systems: Record<string, SystemEntry>,
145
165
  prefix = '',
146
166
  schemaPath: Array<string | number> = ['systems']
147
167
  ): SystemWithPath[] {
148
- const result: SystemWithPath[] = []
149
- for (const [key, system] of Object.entries(systems)) {
168
+ const result: SystemWithPath[] = []
169
+ for (const [key, system] of Object.entries(systems)) {
150
170
  const path = prefix ? `${prefix}.${key}` : key
151
171
  const currentSchemaPath = [...schemaPath, key]
152
172
  result.push({ path, schemaPath: currentSchemaPath, system })
153
- if (system.subsystems !== undefined) {
154
- result.push(...collectAllSystems(system.subsystems, path, [...currentSchemaPath, 'subsystems']))
173
+ const childSystems = system.systems ?? system.subsystems
174
+ if (childSystems !== undefined) {
175
+ result.push(
176
+ ...collectAllSystems(childSystems, path, [...currentSchemaPath, system.systems !== undefined ? 'systems' : 'subsystems'])
177
+ )
155
178
  }
156
179
  }
157
180
  return result
158
- }
159
-
160
- const allSystems = collectAllSystems(model.systems)
161
- const systemsById = new Map<string, SystemEntry>()
162
- for (const { path, system } of allSystems) {
163
- systemsById.set(path, system)
164
- systemsById.set(system.id, system)
165
- }
166
-
167
- const systemIdsByEffectivePath = new Map<string, string>()
168
- allSystems.forEach(({ path, schemaPath, system }) => {
169
- if (system.parentSystemId !== undefined && !systemsById.has(system.parentSystemId)) {
170
- addIssue(
171
- ctx,
172
- [...schemaPath, 'parentSystemId'],
173
- `System "${system.id}" references unknown parent "${system.parentSystemId}"`
174
- )
175
- }
176
-
181
+ }
182
+
183
+ const allSystems = collectAllSystems(model.systems)
184
+ const systemsById = new Map<string, SystemEntry>()
185
+ for (const { path, system } of allSystems) {
186
+ systemsById.set(path, system)
187
+ systemsById.set(system.id, system)
188
+ }
189
+
190
+ const systemIdsByEffectivePath = new Map<string, string>()
191
+ allSystems.forEach(({ path, schemaPath, system }) => {
192
+ if (system.parentSystemId !== undefined && !systemsById.has(system.parentSystemId)) {
193
+ addIssue(
194
+ ctx,
195
+ [...schemaPath, 'parentSystemId'],
196
+ `System "${system.id}" references unknown parent "${system.parentSystemId}"`
197
+ )
198
+ }
199
+
177
200
  const hasChildren =
178
- Object.keys(system.subsystems ?? {}).length > 0 ||
201
+ Object.keys(system.systems ?? system.subsystems ?? {}).length > 0 ||
179
202
  allSystems.some((candidate) => candidate.path.startsWith(`${path}.`) && !candidate.path.slice(path.length + 1).includes('.'))
180
- const contributesRoutePath = system.ui?.path !== undefined || system.path !== undefined || !hasChildren
181
- if (contributesRoutePath) {
182
- const effectivePath = system.ui?.path ?? system.path ?? defaultSystemPathFor(path)
183
- const existingSystemId = systemIdsByEffectivePath.get(effectivePath)
184
- if (existingSystemId !== undefined) {
185
- addIssue(
186
- ctx,
187
- [...schemaPath, system.ui?.path !== undefined ? 'ui' : 'path'],
188
- `System "${path}" effective path "${effectivePath}" duplicates system "${existingSystemId}"`
189
- )
190
- } else {
191
- systemIdsByEffectivePath.set(effectivePath, path)
192
- }
193
- }
194
-
195
- if (hasChildren && isLifecycleEnabled(system.lifecycle, system.enabled)) {
203
+ const contributesRoutePath = system.ui?.path !== undefined || system.path !== undefined || !hasChildren
204
+ if (contributesRoutePath) {
205
+ const effectivePath = system.ui?.path ?? system.path ?? defaultSystemPathFor(path)
206
+ const existingSystemId = systemIdsByEffectivePath.get(effectivePath)
207
+ if (existingSystemId !== undefined) {
208
+ addIssue(
209
+ ctx,
210
+ [...schemaPath, system.ui?.path !== undefined ? 'ui' : 'path'],
211
+ `System "${path}" effective path "${effectivePath}" duplicates system "${existingSystemId}"`
212
+ )
213
+ } else {
214
+ systemIdsByEffectivePath.set(effectivePath, path)
215
+ }
216
+ }
217
+
218
+ if (hasChildren && isLifecycleEnabled(system.lifecycle, system.enabled)) {
196
219
  const hasEnabledDescendant =
197
- Object.values(system.subsystems ?? {}).some((candidate) =>
220
+ Object.values(system.systems ?? system.subsystems ?? {}).some((candidate) =>
198
221
  isLifecycleEnabled(candidate.lifecycle, candidate.enabled)
199
222
  ) ||
200
- allSystems.some(
201
- (candidate) =>
202
- candidate.path.startsWith(`${path}.`) &&
203
- !candidate.path.slice(path.length + 1).includes('.') &&
204
- isLifecycleEnabled(candidate.system.lifecycle, candidate.system.enabled)
205
- )
206
- if (!hasEnabledDescendant) {
207
- addIssue(
208
- ctx,
209
- [...schemaPath, 'lifecycle'],
210
- `System "${path}" is active but has no active descendants`
211
- )
212
- }
213
- }
214
- })
215
-
216
- allSystems.forEach(({ schemaPath, system }) => {
217
- const visited = new Set<string>()
218
- let currentParentId = system.parentSystemId
219
-
220
- while (currentParentId !== undefined) {
221
- if (currentParentId === system.id || visited.has(currentParentId)) {
222
- addIssue(ctx, [...schemaPath, 'parentSystemId'], `System "${system.id}" has a parent cycle`)
223
- return
224
- }
225
-
226
- visited.add(currentParentId)
227
- currentParentId = systemsById.get(currentParentId)?.parentSystemId
228
- }
229
- })
230
-
231
- type CollectedSidebarSurface = {
232
- id: string
233
- node: Extract<SidebarNode, { type: 'surface' }>
234
- path: Array<string | number>
235
- }
236
-
237
- function normalizeRoutePath(path: string): string {
238
- return path.length > 1 ? path.replace(/\/+$/, '') : path
239
- }
240
-
241
- const sidebarNodeIds = new Map<string, Array<string | number>>()
242
- const sidebarSurfacePaths = new Map<string, string>()
243
- const sidebarSurfaces: CollectedSidebarSurface[] = []
244
-
245
- function collectSidebarNodes(
246
- nodes: Record<string, SidebarNode>,
247
- schemaPath: Array<string | number>
248
- ): void {
249
- Object.entries(nodes).forEach(([nodeId, node]) => {
250
- const nodePath = [...schemaPath, nodeId]
251
- const existingNodePath = sidebarNodeIds.get(nodeId)
252
- if (existingNodePath !== undefined) {
253
- addIssue(ctx, nodePath, `Sidebar node id "${nodeId}" duplicates another sidebar node`)
254
- } else {
255
- sidebarNodeIds.set(nodeId, nodePath)
256
- }
257
-
258
- if (node.type === 'group') {
259
- collectSidebarNodes(node.children, [...nodePath, 'children'])
260
- return
261
- }
262
-
263
- sidebarSurfaces.push({ id: nodeId, node, path: nodePath })
264
- const normalizedPath = normalizeRoutePath(node.path)
265
- const existingSurfaceId = sidebarSurfacePaths.get(normalizedPath)
266
- if (existingSurfaceId !== undefined) {
267
- addIssue(
268
- ctx,
269
- [...nodePath, 'path'],
270
- `Sidebar surface path "${node.path}" duplicates surface "${existingSurfaceId}"`
271
- )
272
- } else {
273
- sidebarSurfacePaths.set(normalizedPath, nodeId)
274
- }
275
-
276
- node.targets?.systems?.forEach((systemId, systemIndex) => {
277
- if (!systemsById.has(systemId)) {
278
- addIssue(
279
- ctx,
280
- [...nodePath, 'targets', 'systems', systemIndex],
281
- `Sidebar surface "${nodeId}" references unknown system "${systemId}"`
282
- )
283
- }
284
- })
285
- })
223
+ allSystems.some(
224
+ (candidate) =>
225
+ candidate.path.startsWith(`${path}.`) &&
226
+ !candidate.path.slice(path.length + 1).includes('.') &&
227
+ isLifecycleEnabled(candidate.system.lifecycle, candidate.system.enabled)
228
+ )
229
+ if (!hasEnabledDescendant) {
230
+ addIssue(
231
+ ctx,
232
+ [...schemaPath, 'lifecycle'],
233
+ `System "${path}" is active but has no active descendants`
234
+ )
235
+ }
236
+ }
237
+ })
238
+
239
+ allSystems.forEach(({ schemaPath, system }) => {
240
+ const visited = new Set<string>()
241
+ let currentParentId = system.parentSystemId
242
+
243
+ while (currentParentId !== undefined) {
244
+ if (currentParentId === system.id || visited.has(currentParentId)) {
245
+ addIssue(ctx, [...schemaPath, 'parentSystemId'], `System "${system.id}" has a parent cycle`)
246
+ return
247
+ }
248
+
249
+ visited.add(currentParentId)
250
+ currentParentId = systemsById.get(currentParentId)?.parentSystemId
251
+ }
252
+ })
253
+
254
+ type CollectedSidebarSurface = {
255
+ id: string
256
+ node: Extract<SidebarNode, { type: 'surface' }>
257
+ path: Array<string | number>
258
+ }
259
+
260
+ function normalizeRoutePath(path: string): string {
261
+ return path.length > 1 ? path.replace(/\/+$/, '') : path
262
+ }
263
+
264
+ const sidebarNodeIds = new Map<string, Array<string | number>>()
265
+ const sidebarSurfacePaths = new Map<string, string>()
266
+ const sidebarSurfaces: CollectedSidebarSurface[] = []
267
+
268
+ function collectSidebarNodes(
269
+ nodes: Record<string, SidebarNode>,
270
+ schemaPath: Array<string | number>
271
+ ): void {
272
+ Object.entries(nodes).forEach(([nodeId, node]) => {
273
+ const nodePath = [...schemaPath, nodeId]
274
+ const existingNodePath = sidebarNodeIds.get(nodeId)
275
+ if (existingNodePath !== undefined) {
276
+ addIssue(ctx, nodePath, `Sidebar node id "${nodeId}" duplicates another sidebar node`)
277
+ } else {
278
+ sidebarNodeIds.set(nodeId, nodePath)
279
+ }
280
+
281
+ if (node.type === 'group') {
282
+ collectSidebarNodes(node.children, [...nodePath, 'children'])
283
+ return
284
+ }
285
+
286
+ sidebarSurfaces.push({ id: nodeId, node, path: nodePath })
287
+ const normalizedPath = normalizeRoutePath(node.path)
288
+ const existingSurfaceId = sidebarSurfacePaths.get(normalizedPath)
289
+ if (existingSurfaceId !== undefined) {
290
+ addIssue(
291
+ ctx,
292
+ [...nodePath, 'path'],
293
+ `Sidebar surface path "${node.path}" duplicates surface "${existingSurfaceId}"`
294
+ )
295
+ } else {
296
+ sidebarSurfacePaths.set(normalizedPath, nodeId)
297
+ }
298
+
299
+ node.targets?.systems?.forEach((systemId, systemIndex) => {
300
+ if (!systemsById.has(systemId)) {
301
+ addIssue(
302
+ ctx,
303
+ [...nodePath, 'targets', 'systems', systemIndex],
304
+ `Sidebar surface "${nodeId}" references unknown system "${systemId}"`
305
+ )
306
+ }
307
+ })
308
+ })
309
+ }
310
+
311
+ collectSidebarNodes(model.navigation.sidebar.primary, ['navigation', 'sidebar', 'primary'])
312
+ collectSidebarNodes(model.navigation.sidebar.bottom, ['navigation', 'sidebar', 'bottom'])
313
+
314
+ // Offerings -> CustomerSegment cross-ref: targetSegmentIds must resolve
315
+ const segmentsById = new Map(Object.entries(model.customers))
316
+ Object.values(model.offerings).forEach((product) => {
317
+ product.targetSegmentIds.forEach((segmentId, segmentIndex) => {
318
+ if (!segmentsById.has(segmentId)) {
319
+ addIssue(
320
+ ctx,
321
+ ['offerings', product.id, 'targetSegmentIds', segmentIndex],
322
+ `Product "${product.id}" references unknown customer segment "${segmentId}"`
323
+ )
324
+ }
325
+ })
326
+
327
+ // Offerings -> System cross-ref: deliveryFeatureId must resolve (when present)
328
+ if (product.deliveryFeatureId !== undefined && !systemsById.has(product.deliveryFeatureId)) {
329
+ addIssue(
330
+ ctx,
331
+ ['offerings', product.id, 'deliveryFeatureId'],
332
+ `Product "${product.id}" references unknown delivery system "${product.deliveryFeatureId}"`
333
+ )
334
+ }
335
+ })
336
+
337
+ // Goals -> period-range validation: periodEnd must be strictly after periodStart
338
+ Object.values(model.goals).forEach((objective) => {
339
+ if (objective.periodEnd <= objective.periodStart) {
340
+ addIssue(
341
+ ctx,
342
+ ['goals', objective.id, 'periodEnd'],
343
+ `Goal "${objective.id}" has periodEnd "${objective.periodEnd}" which must be strictly after periodStart "${objective.periodStart}"`
344
+ )
345
+ }
346
+ })
347
+
348
+ const goalsById = new Map(Object.entries(model.goals))
349
+ // Phase 4: knowledge is now a flat Record<id, OrgKnowledgeNode> — no .nodes array
350
+ const knowledgeById = new Map(Object.entries(model.knowledge))
351
+ const actionsById = new Map(Object.entries(model.actions))
352
+ const entitiesById = new Map(Object.entries(model.entities))
353
+ const policiesById = new Map(Object.entries(model.policies))
354
+
355
+ sidebarSurfaces.forEach(({ id, node, path }) => {
356
+ node.targets?.entities?.forEach((entityId, entityIndex) => {
357
+ if (!entitiesById.has(entityId)) {
358
+ addIssue(
359
+ ctx,
360
+ [...path, 'targets', 'entities', entityIndex],
361
+ `Sidebar surface "${id}" references unknown entity "${entityId}"`
362
+ )
363
+ }
364
+ })
365
+
366
+ node.targets?.actions?.forEach((actionId, actionIndex) => {
367
+ if (!actionsById.has(actionId)) {
368
+ addIssue(
369
+ ctx,
370
+ [...path, 'targets', 'actions', actionIndex],
371
+ `Sidebar surface "${id}" references unknown action "${actionId}"`
372
+ )
373
+ }
374
+ })
375
+ })
376
+
377
+ Object.values(model.entities).forEach((entity) => {
378
+ if (!systemsById.has(entity.ownedBySystemId)) {
379
+ addIssue(
380
+ ctx,
381
+ ['entities', entity.id, 'ownedBySystemId'],
382
+ `Entity "${entity.id}" references unknown ownedBySystemId "${entity.ownedBySystemId}"`
383
+ )
384
+ }
385
+
386
+ entity.links?.forEach((link, linkIndex) => {
387
+ if (!entitiesById.has(link.toEntity)) {
388
+ addIssue(
389
+ ctx,
390
+ ['entities', entity.id, 'links', linkIndex, 'toEntity'],
391
+ `Entity "${entity.id}" links to unknown entity "${link.toEntity}"`
392
+ )
393
+ }
394
+ })
395
+ })
396
+
397
+ // Roles -> reportsToId cross-ref: each reportsToId must resolve to another role in the same collection
398
+ const rolesById = new Map(Object.entries(model.roles))
399
+ Object.values(model.roles).forEach((role) => {
400
+ if (role.reportsToId !== undefined && !rolesById.has(role.reportsToId)) {
401
+ addIssue(
402
+ ctx,
403
+ ['roles', role.id, 'reportsToId'],
404
+ `Role "${role.id}" references unknown reportsToId "${role.reportsToId}"`
405
+ )
406
+ }
407
+ })
408
+
409
+ Object.values(model.roles).forEach((role) => {
410
+ const visited = new Set<string>()
411
+ let currentReportsToId = role.reportsToId
412
+
413
+ while (currentReportsToId !== undefined) {
414
+ if (currentReportsToId === role.id || visited.has(currentReportsToId)) {
415
+ addIssue(ctx, ['roles', role.id, 'reportsToId'], `Role "${role.id}" has a reportsToId cycle`)
416
+ return
417
+ }
418
+
419
+ visited.add(currentReportsToId)
420
+ currentReportsToId = rolesById.get(currentReportsToId)?.reportsToId
421
+ }
422
+ })
423
+
424
+ Object.values(model.roles).forEach((role) => {
425
+ role.responsibleFor?.forEach((systemId, systemIndex) => {
426
+ if (!systemsById.has(systemId)) {
427
+ addIssue(
428
+ ctx,
429
+ ['roles', role.id, 'responsibleFor', systemIndex],
430
+ `Role "${role.id}" references unknown responsibleFor system "${systemId}"`
431
+ )
432
+ }
433
+ })
434
+ })
435
+
436
+ allSystems.forEach(({ schemaPath, system }) => {
437
+ if (system.responsibleRoleId !== undefined && !rolesById.has(system.responsibleRoleId)) {
438
+ addIssue(
439
+ ctx,
440
+ [...schemaPath, 'responsibleRoleId'],
441
+ `System "${system.id}" references unknown responsibleRoleId "${system.responsibleRoleId}"`
442
+ )
443
+ }
444
+
445
+ system.governedByKnowledge?.forEach((nodeId, nodeIndex) => {
446
+ if (!knowledgeById.has(nodeId)) {
447
+ addIssue(
448
+ ctx,
449
+ [...schemaPath, 'governedByKnowledge', nodeIndex],
450
+ `System "${system.id}" references unknown knowledge node "${nodeId}"`
451
+ )
452
+ }
453
+ })
454
+
455
+ system.drivesGoals?.forEach((goalId, goalIndex) => {
456
+ if (!goalsById.has(goalId)) {
457
+ addIssue(
458
+ ctx,
459
+ [...schemaPath, 'drivesGoals', goalIndex],
460
+ `System "${system.id}" references unknown goal "${goalId}"`
461
+ )
462
+ }
463
+ })
464
+
465
+ system.actions?.forEach((actionRef, actionIndex) => {
466
+ if (!actionsById.has(actionRef.actionId)) {
467
+ addIssue(
468
+ ctx,
469
+ [...schemaPath, 'actions', actionIndex, 'actionId'],
470
+ `System "${system.id}" references unknown action "${actionRef.actionId}"`
471
+ )
472
+ }
473
+ })
474
+
475
+ system.policies?.forEach((policyId, policyIndex) => {
476
+ if (!policiesById.has(policyId)) {
477
+ addIssue(
478
+ ctx,
479
+ [...schemaPath, 'policies', policyIndex],
480
+ `System "${system.id}" references unknown policy "${policyId}"`
481
+ )
482
+ }
483
+ })
484
+ })
485
+
486
+ Object.values(model.actions).forEach((action) => {
487
+ action.affects?.forEach((entityId, entityIndex) => {
488
+ if (!entitiesById.has(entityId)) {
489
+ addIssue(
490
+ ctx,
491
+ ['actions', action.id, 'affects', entityIndex],
492
+ `Action "${action.id}" affects unknown entity "${entityId}"`
493
+ )
494
+ }
495
+ })
496
+ })
497
+
498
+ // Phase 4: sales / prospecting / projects compound-domain entity cross-ref checks removed.
499
+ // Those entity bindings now live in system.content (Wave 2 canonicalOrganizationModel).
500
+
501
+ const resourcesById = new Map(Object.entries(model.resources))
502
+ sidebarSurfaces.forEach(({ id, node, path }) => {
503
+ node.targets?.resources?.forEach((resourceId, resourceIndex) => {
504
+ if (!resourcesById.has(resourceId)) {
505
+ addIssue(
506
+ ctx,
507
+ [...path, 'targets', 'resources', resourceIndex],
508
+ `Sidebar surface "${id}" references unknown resource "${resourceId}"`
509
+ )
510
+ }
511
+ })
512
+ })
513
+ // Phase 4: stageIds previously sourced from model.prospecting.*Stages; stages now live in
514
+ // system.content as schema:stage nodes. knowledge 'stage' target validation is kept permissive
515
+ // (always false) — Wave 2 will wire a content-based stage lookup when canonical OM lands.
516
+ const stageIds = new Set<string>()
517
+ const actionIds = new Set(Object.keys(model.actions))
518
+ const offeringsById = new Map(Object.entries(model.offerings))
519
+ const ontologyCompilation = compileOrganizationOntology(model)
520
+ const ontologyIndexByKind = {
521
+ object: ontologyCompilation.ontology.objectTypes,
522
+ link: ontologyCompilation.ontology.linkTypes,
523
+ action: ontologyCompilation.ontology.actionTypes,
524
+ catalog: ontologyCompilation.ontology.catalogTypes,
525
+ event: ontologyCompilation.ontology.eventTypes,
526
+ interface: ontologyCompilation.ontology.interfaceTypes,
527
+ 'value-type': ontologyCompilation.ontology.valueTypes,
528
+ property: ontologyCompilation.ontology.sharedProperties,
529
+ group: ontologyCompilation.ontology.groups,
530
+ surface: ontologyCompilation.ontology.surfaces
531
+ } satisfies Record<OntologyKind, Record<string, unknown>>
532
+ const ontologyIds = new Set(Object.values(ontologyIndexByKind).flatMap((index) => Object.keys(index)))
533
+
534
+ function topologyTargetExists(ref: OmTopologyNodeRef): boolean {
535
+ if (ref.kind === 'system') return systemsById.has(ref.id)
536
+ if (ref.kind === 'resource') return resourcesById.has(ref.id)
537
+ if (ref.kind === 'ontology') return ontologyIds.has(ref.id)
538
+ if (ref.kind === 'policy') return policiesById.has(ref.id)
539
+ if (ref.kind === 'role') return rolesById.has(ref.id)
540
+
541
+ // Trigger, human checkpoint, and external resource refs are projected
542
+ // topology nodes during the bridge period; their owning runtime indexes are
543
+ // validated by deployment projection in later waves.
544
+ return true
286
545
  }
287
546
 
288
- collectSidebarNodes(model.navigation.sidebar.primary, ['navigation', 'sidebar', 'primary'])
289
- collectSidebarNodes(model.navigation.sidebar.bottom, ['navigation', 'sidebar', 'bottom'])
290
-
291
- // Offerings -> CustomerSegment cross-ref: targetSegmentIds must resolve
292
- const segmentsById = new Map(Object.entries(model.customers))
293
- Object.values(model.offerings).forEach((product) => {
294
- product.targetSegmentIds.forEach((segmentId, segmentIndex) => {
295
- if (!segmentsById.has(segmentId)) {
296
- addIssue(
297
- ctx,
298
- ['offerings', product.id, 'targetSegmentIds', segmentIndex],
299
- `Product "${product.id}" references unknown customer segment "${segmentId}"`
300
- )
301
- }
302
- })
303
-
304
- // Offerings -> System cross-ref: deliveryFeatureId must resolve (when present)
305
- if (product.deliveryFeatureId !== undefined && !systemsById.has(product.deliveryFeatureId)) {
306
- addIssue(
307
- ctx,
308
- ['offerings', product.id, 'deliveryFeatureId'],
309
- `Product "${product.id}" references unknown delivery system "${product.deliveryFeatureId}"`
310
- )
311
- }
312
- })
313
-
314
- // Goals -> period-range validation: periodEnd must be strictly after periodStart
315
- Object.values(model.goals).forEach((objective) => {
316
- if (objective.periodEnd <= objective.periodStart) {
317
- addIssue(
318
- ctx,
319
- ['goals', objective.id, 'periodEnd'],
320
- `Goal "${objective.id}" has periodEnd "${objective.periodEnd}" which must be strictly after periodStart "${objective.periodStart}"`
321
- )
322
- }
323
- })
324
-
325
- const goalsById = new Map(Object.entries(model.goals))
326
- // Phase 4: knowledge is now a flat Record<id, OrgKnowledgeNode> — no .nodes array
327
- const knowledgeById = new Map(Object.entries(model.knowledge))
328
- const actionsById = new Map(Object.entries(model.actions))
329
- const entitiesById = new Map(Object.entries(model.entities))
330
- const policiesById = new Map(Object.entries(model.policies))
331
-
332
- sidebarSurfaces.forEach(({ id, node, path }) => {
333
- node.targets?.entities?.forEach((entityId, entityIndex) => {
334
- if (!entitiesById.has(entityId)) {
335
- addIssue(
336
- ctx,
337
- [...path, 'targets', 'entities', entityIndex],
338
- `Sidebar surface "${id}" references unknown entity "${entityId}"`
339
- )
340
- }
341
- })
342
-
343
- node.targets?.actions?.forEach((actionId, actionIndex) => {
344
- if (!actionsById.has(actionId)) {
345
- addIssue(
346
- ctx,
347
- [...path, 'targets', 'actions', actionIndex],
348
- `Sidebar surface "${id}" references unknown action "${actionId}"`
349
- )
350
- }
351
- })
352
- })
547
+ Object.entries(model.topology.relationships).forEach(([relationshipId, relationship]) => {
548
+ ;(['from', 'to'] as const).forEach((side) => {
549
+ const ref = relationship[side]
550
+ if (topologyTargetExists(ref)) return
353
551
 
354
- Object.values(model.entities).forEach((entity) => {
355
- if (!systemsById.has(entity.ownedBySystemId)) {
356
552
  addIssue(
357
553
  ctx,
358
- ['entities', entity.id, 'ownedBySystemId'],
359
- `Entity "${entity.id}" references unknown ownedBySystemId "${entity.ownedBySystemId}"`
554
+ ['topology', 'relationships', relationshipId, side],
555
+ `Topology relationship "${relationshipId}" ${side} references unknown ${ref.kind} "${ref.id}"`
360
556
  )
361
- }
362
-
363
- entity.links?.forEach((link, linkIndex) => {
364
- if (!entitiesById.has(link.toEntity)) {
365
- addIssue(
366
- ctx,
367
- ['entities', entity.id, 'links', linkIndex, 'toEntity'],
368
- `Entity "${entity.id}" links to unknown entity "${link.toEntity}"`
369
- )
370
- }
371
557
  })
372
558
  })
373
559
 
374
- // Roles -> reportsToId cross-ref: each reportsToId must resolve to another role in the same collection
375
- const rolesById = new Map(Object.entries(model.roles))
376
- Object.values(model.roles).forEach((role) => {
377
- if (role.reportsToId !== undefined && !rolesById.has(role.reportsToId)) {
378
- addIssue(
379
- ctx,
380
- ['roles', role.id, 'reportsToId'],
381
- `Role "${role.id}" references unknown reportsToId "${role.reportsToId}"`
382
- )
560
+ const ontologyReferenceKeyKinds = {
561
+ valueType: 'value-type',
562
+ catalogType: 'catalog',
563
+ objectType: 'object',
564
+ eventType: 'event',
565
+ actionType: 'action',
566
+ linkType: 'link',
567
+ interfaceType: 'interface',
568
+ propertyType: 'property',
569
+ groupType: 'group',
570
+ surfaceType: 'surface',
571
+ stepCatalog: 'catalog'
572
+ } satisfies Record<string, OntologyKind>
573
+
574
+ function validateKnownOntologyReferences(
575
+ ownerId: string,
576
+ value: unknown,
577
+ path: Array<string | number>,
578
+ seen = new WeakSet<object>()
579
+ ): void {
580
+ if (Array.isArray(value)) {
581
+ value.forEach((entry, index) => validateKnownOntologyReferences(ownerId, entry, [...path, index], seen))
582
+ return
383
583
  }
384
- })
385
-
386
- Object.values(model.roles).forEach((role) => {
387
- const visited = new Set<string>()
388
- let currentReportsToId = role.reportsToId
389
584
 
390
- while (currentReportsToId !== undefined) {
391
- if (currentReportsToId === role.id || visited.has(currentReportsToId)) {
392
- addIssue(ctx, ['roles', role.id, 'reportsToId'], `Role "${role.id}" has a reportsToId cycle`)
393
- return
394
- }
395
-
396
- visited.add(currentReportsToId)
397
- currentReportsToId = rolesById.get(currentReportsToId)?.reportsToId
398
- }
399
- })
585
+ if (!isRecord(value)) return
586
+ if (seen.has(value)) return
587
+ seen.add(value)
400
588
 
401
- Object.values(model.roles).forEach((role) => {
402
- role.responsibleFor?.forEach((systemId, systemIndex) => {
403
- if (!systemsById.has(systemId)) {
404
- addIssue(
405
- ctx,
406
- ['roles', role.id, 'responsibleFor', systemIndex],
407
- `Role "${role.id}" references unknown responsibleFor system "${systemId}"`
408
- )
409
- }
410
- })
411
- })
412
-
413
- allSystems.forEach(({ schemaPath, system }) => {
414
- if (system.responsibleRoleId !== undefined && !rolesById.has(system.responsibleRoleId)) {
415
- addIssue(
416
- ctx,
417
- [...schemaPath, 'responsibleRoleId'],
418
- `System "${system.id}" references unknown responsibleRoleId "${system.responsibleRoleId}"`
419
- )
420
- }
421
-
422
- system.governedByKnowledge?.forEach((nodeId, nodeIndex) => {
423
- if (!knowledgeById.has(nodeId)) {
424
- addIssue(
425
- ctx,
426
- [...schemaPath, 'governedByKnowledge', nodeIndex],
427
- `System "${system.id}" references unknown knowledge node "${nodeId}"`
428
- )
429
- }
430
- })
431
-
432
- system.drivesGoals?.forEach((goalId, goalIndex) => {
433
- if (!goalsById.has(goalId)) {
434
- addIssue(
435
- ctx,
436
- [...schemaPath, 'drivesGoals', goalIndex],
437
- `System "${system.id}" references unknown goal "${goalId}"`
438
- )
439
- }
440
- })
441
-
442
- system.actions?.forEach((actionRef, actionIndex) => {
443
- if (!actionsById.has(actionRef.actionId)) {
444
- addIssue(
445
- ctx,
446
- [...schemaPath, 'actions', actionIndex, 'actionId'],
447
- `System "${system.id}" references unknown action "${actionRef.actionId}"`
448
- )
449
- }
450
- })
451
-
452
- system.policies?.forEach((policyId, policyIndex) => {
453
- if (!policiesById.has(policyId)) {
454
- addIssue(
455
- ctx,
456
- [...schemaPath, 'policies', policyIndex],
457
- `System "${system.id}" references unknown policy "${policyId}"`
458
- )
589
+ Object.entries(value).forEach(([key, entry]) => {
590
+ const expectedKind = ontologyReferenceKeyKinds[key as keyof typeof ontologyReferenceKeyKinds]
591
+ if (expectedKind !== undefined) {
592
+ if (typeof entry !== 'string') {
593
+ addIssue(ctx, [...path, key], `Ontology record "${ownerId}" ${key} must be an ontology ID string`)
594
+ } else if (ontologyIndexByKind[expectedKind][entry] === undefined) {
595
+ addIssue(
596
+ ctx,
597
+ [...path, key],
598
+ `Ontology record "${ownerId}" ${key} references unknown ${expectedKind} ontology ID "${entry}"`
599
+ )
600
+ }
459
601
  }
460
- })
461
- })
462
602
 
463
- Object.values(model.actions).forEach((action) => {
464
- action.affects?.forEach((entityId, entityIndex) => {
465
- if (!entitiesById.has(entityId)) {
466
- addIssue(
467
- ctx,
468
- ['actions', action.id, 'affects', entityIndex],
469
- `Action "${action.id}" affects unknown entity "${entityId}"`
470
- )
471
- }
603
+ validateKnownOntologyReferences(ownerId, entry, [...path, key], seen)
472
604
  })
473
- })
474
-
475
- // Phase 4: sales / prospecting / projects compound-domain entity cross-ref checks removed.
476
- // Those entity bindings now live in system.content (Wave 2 canonicalOrganizationModel).
605
+ }
477
606
 
478
- const resourcesById = new Map(Object.entries(model.resources))
479
- sidebarSurfaces.forEach(({ id, node, path }) => {
480
- node.targets?.resources?.forEach((resourceId, resourceIndex) => {
481
- if (!resourcesById.has(resourceId)) {
482
- addIssue(
483
- ctx,
484
- [...path, 'targets', 'resources', resourceIndex],
485
- `Sidebar surface "${id}" references unknown resource "${resourceId}"`
486
- )
487
- }
488
- })
489
- })
490
- // Phase 4: stageIds previously sourced from model.prospecting.*Stages; stages now live in
491
- // system.content as schema:stage nodes. knowledge 'stage' target validation is kept permissive
492
- // (always false) — Wave 2 will wire a content-based stage lookup when canonical OM lands.
493
- const stageIds = new Set<string>()
494
- const actionIds = new Set(Object.keys(model.actions))
495
- const offeringsById = new Map(Object.entries(model.offerings))
607
+ for (const { id, record } of listResolvedOntologyRecords(ontologyCompilation.ontology)) {
608
+ validateKnownOntologyReferences(id, record, record.origin.path)
609
+ }
496
610
 
497
611
  Object.values(model.policies).forEach((policy) => {
498
- policy.appliesTo.systemIds.forEach((systemId, systemIndex) => {
499
- if (!systemsById.has(systemId)) {
500
- addIssue(
501
- ctx,
502
- ['policies', policy.id, 'appliesTo', 'systemIds', systemIndex],
503
- `Policy "${policy.id}" applies to unknown system "${systemId}"`
504
- )
505
- }
506
- })
507
-
508
- policy.appliesTo.actionIds.forEach((actionId, actionIndex) => {
509
- if (!actionsById.has(actionId)) {
510
- addIssue(
511
- ctx,
512
- ['policies', policy.id, 'appliesTo', 'actionIds', actionIndex],
513
- `Policy "${policy.id}" applies to unknown action "${actionId}"`
514
- )
515
- }
516
- })
517
-
518
- policy.actions.forEach((action, actionIndex) => {
519
- if (action.kind === 'invoke-action' && !actionsById.has(action.actionId)) {
520
- addIssue(
521
- ctx,
522
- ['policies', policy.id, 'actions', actionIndex, 'actionId'],
523
- `Policy "${policy.id}" invokes unknown action "${action.actionId}"`
524
- )
525
- }
526
- if (
527
- (action.kind === 'notify-role' || action.kind === 'require-approval') &&
528
- action.roleId !== undefined &&
529
- !rolesById.has(action.roleId)
530
- ) {
531
- addIssue(
532
- ctx,
533
- ['policies', policy.id, 'actions', actionIndex, 'roleId'],
534
- `Policy "${policy.id}" references unknown role "${action.roleId}"`
535
- )
536
- }
537
- })
538
-
539
- if (policy.trigger.kind === 'action-invocation' && !actionsById.has(policy.trigger.actionId)) {
540
- addIssue(
541
- ctx,
542
- ['policies', policy.id, 'trigger', 'actionId'],
543
- `Policy "${policy.id}" references unknown trigger action "${policy.trigger.actionId}"`
544
- )
545
- }
546
- })
547
-
548
- function knowledgeTargetExists(kind: string, id: string): boolean {
549
- if (kind === 'system') return systemsById.has(id)
550
- if (kind === 'resource') return resourcesById.has(id)
551
- if (kind === 'knowledge') return knowledgeById.has(id)
552
- if (kind === 'stage') return stageIds.has(id)
553
- if (kind === 'action') return actionIds.has(id)
554
- if (kind === 'role') return rolesById.has(id)
612
+ policy.appliesTo.systemIds.forEach((systemId, systemIndex) => {
613
+ if (!systemsById.has(systemId)) {
614
+ addIssue(
615
+ ctx,
616
+ ['policies', policy.id, 'appliesTo', 'systemIds', systemIndex],
617
+ `Policy "${policy.id}" applies to unknown system "${systemId}"`
618
+ )
619
+ }
620
+ })
621
+
622
+ policy.appliesTo.actionIds.forEach((actionId, actionIndex) => {
623
+ if (!actionsById.has(actionId)) {
624
+ addIssue(
625
+ ctx,
626
+ ['policies', policy.id, 'appliesTo', 'actionIds', actionIndex],
627
+ `Policy "${policy.id}" applies to unknown action "${actionId}"`
628
+ )
629
+ }
630
+ })
631
+
632
+ policy.actions.forEach((action, actionIndex) => {
633
+ if (action.kind === 'invoke-action' && !actionsById.has(action.actionId)) {
634
+ addIssue(
635
+ ctx,
636
+ ['policies', policy.id, 'actions', actionIndex, 'actionId'],
637
+ `Policy "${policy.id}" invokes unknown action "${action.actionId}"`
638
+ )
639
+ }
640
+ if (
641
+ (action.kind === 'notify-role' || action.kind === 'require-approval') &&
642
+ action.roleId !== undefined &&
643
+ !rolesById.has(action.roleId)
644
+ ) {
645
+ addIssue(
646
+ ctx,
647
+ ['policies', policy.id, 'actions', actionIndex, 'roleId'],
648
+ `Policy "${policy.id}" references unknown role "${action.roleId}"`
649
+ )
650
+ }
651
+ })
652
+
653
+ if (policy.trigger.kind === 'action-invocation' && !actionsById.has(policy.trigger.actionId)) {
654
+ addIssue(
655
+ ctx,
656
+ ['policies', policy.id, 'trigger', 'actionId'],
657
+ `Policy "${policy.id}" references unknown trigger action "${policy.trigger.actionId}"`
658
+ )
659
+ }
660
+ })
661
+
662
+ function knowledgeTargetExists(kind: string, id: string): boolean {
663
+ if (kind === 'system') return systemsById.has(id)
664
+ if (kind === 'resource') return resourcesById.has(id)
665
+ if (kind === 'knowledge') return knowledgeById.has(id)
666
+ if (kind === 'stage') return stageIds.has(id)
667
+ if (kind === 'action') return actionIds.has(id)
668
+ if (kind === 'role') return rolesById.has(id)
555
669
  if (kind === 'goal') return goalsById.has(id)
556
670
  if (kind === 'customer-segment') return segmentsById.has(id)
557
671
  if (kind === 'offering') return offeringsById.has(id)
672
+ if (kind === 'ontology') return ontologyIds.has(id)
558
673
  return false
559
674
  }
560
-
561
- // Phase 4: model.knowledge is now Record<id, OrgKnowledgeNode> — iterate Object.values
562
- Object.entries(model.knowledge).forEach(([nodeId, node]) => {
563
- node.links.forEach((link, linkIndex) => {
564
- if (!knowledgeTargetExists(link.target.kind, link.target.id)) {
565
- addIssue(
566
- ctx,
567
- ['knowledge', nodeId, 'links', linkIndex, 'target'],
568
- `Knowledge node "${node.id}" references unknown ${link.target.kind} target "${link.target.id}"`
569
- )
570
- }
571
-
572
- if (!isKnowledgeKindCompatibleWithTarget(node.kind, link.target.kind)) {
573
- addIssue(
574
- ctx,
575
- ['knowledge', nodeId, 'links', linkIndex, 'target', 'kind'],
576
- `Knowledge node "${node.id}" kind "${node.kind}" cannot govern ${link.target.kind} targets`
577
- )
578
- }
579
-
580
- // `governedByKnowledge` is validated one-way on target nodes above. Knowledge
581
- // links may be authored first and remain valid as forward references.
582
- })
583
- })
584
-
675
+
676
+ // Phase 4: model.knowledge is now Record<id, OrgKnowledgeNode> — iterate Object.values
677
+ Object.entries(model.knowledge).forEach(([nodeId, node]) => {
678
+ node.links.forEach((link, linkIndex) => {
679
+ if (!knowledgeTargetExists(link.target.kind, link.target.id)) {
680
+ addIssue(
681
+ ctx,
682
+ ['knowledge', nodeId, 'links', linkIndex, 'target'],
683
+ `Knowledge node "${node.id}" references unknown ${link.target.kind} target "${link.target.id}"`
684
+ )
685
+ }
686
+
687
+ if (!isKnowledgeKindCompatibleWithTarget(node.kind, link.target.kind)) {
688
+ addIssue(
689
+ ctx,
690
+ ['knowledge', nodeId, 'links', linkIndex, 'target', 'kind'],
691
+ `Knowledge node "${node.id}" kind "${node.kind}" cannot govern ${link.target.kind} targets`
692
+ )
693
+ }
694
+
695
+ // `governedByKnowledge` is validated one-way on target nodes above. Knowledge
696
+ // links may be authored first and remain valid as forward references.
697
+ })
698
+ })
699
+
585
700
  Object.values(model.resources).forEach((resource) => {
586
- if (!systemsById.has(resource.systemPath)) {
587
- addIssue(
588
- ctx,
589
- ['resources', resource.id, 'systemPath'],
590
- `Resource "${resource.id}" references unknown system path "${resource.systemPath}"`
591
- )
592
- }
593
-
594
- if (resource.ownerRoleId !== undefined && !rolesById.has(resource.ownerRoleId)) {
595
- addIssue(
596
- ctx,
597
- ['resources', resource.id, 'ownerRoleId'],
598
- `Resource "${resource.id}" references unknown ownerRoleId "${resource.ownerRoleId}"`
599
- )
600
- }
601
-
602
- if (resource.kind === 'agent' && resource.actsAsRoleId !== undefined && !rolesById.has(resource.actsAsRoleId)) {
603
- addIssue(
604
- ctx,
605
- ['resources', resource.id, 'actsAsRoleId'],
606
- `Agent resource "${resource.id}" references unknown actsAsRoleId "${resource.actsAsRoleId}"`
607
- )
701
+ if (!systemsById.has(resource.systemPath)) {
702
+ addIssue(
703
+ ctx,
704
+ ['resources', resource.id, 'systemPath'],
705
+ `Resource "${resource.id}" references unknown system path "${resource.systemPath}"`
706
+ )
707
+ }
708
+
709
+ if (resource.ownerRoleId !== undefined && !rolesById.has(resource.ownerRoleId)) {
710
+ addIssue(
711
+ ctx,
712
+ ['resources', resource.id, 'ownerRoleId'],
713
+ `Resource "${resource.id}" references unknown ownerRoleId "${resource.ownerRoleId}"`
714
+ )
715
+ }
716
+
717
+ if (resource.kind === 'agent' && resource.actsAsRoleId !== undefined && !rolesById.has(resource.actsAsRoleId)) {
718
+ addIssue(
719
+ ctx,
720
+ ['resources', resource.id, 'actsAsRoleId'],
721
+ `Agent resource "${resource.id}" references unknown actsAsRoleId "${resource.actsAsRoleId}"`
722
+ )
608
723
  }
609
724
  })
610
725
 
611
- Object.values(model.roles).forEach((role) => {
612
- if (role.heldBy === undefined) return
613
-
614
- asRoleHolderArray(role.heldBy).forEach((holder, holderIndex) => {
615
- if (holder.kind !== 'agent') return
616
-
617
- const resource = resourcesById.get(holder.agentId)
618
- if (resource === undefined) {
619
- addIssue(
620
- ctx,
621
- ['roles', role.id, 'heldBy', Array.isArray(role.heldBy) ? holderIndex : 'agentId'],
622
- `Role "${role.id}" references unknown agent holder resource "${holder.agentId}"`
623
- )
624
- return
625
- }
726
+ function validateResourceOntologyBinding(
727
+ resourceId: string,
728
+ bindingKey: 'actions' | 'primaryAction' | 'reads' | 'writes' | 'usesCatalogs' | 'emits',
729
+ expectedKind: OntologyKind,
730
+ ids: string[] | string | undefined
731
+ ): void {
732
+ const ontologyIds = ids === undefined ? [] : Array.isArray(ids) ? ids : [ids]
626
733
 
627
- if (resource.kind !== 'agent') {
734
+ ontologyIds.forEach((ontologyId, ontologyIndex) => {
735
+ if (ontologyIndexByKind[expectedKind][ontologyId] === undefined) {
628
736
  addIssue(
629
737
  ctx,
630
- ['roles', role.id, 'heldBy', Array.isArray(role.heldBy) ? holderIndex : 'agentId'],
631
- `Role "${role.id}" agent holder "${holder.agentId}" must reference an agent resource`
738
+ [
739
+ 'resources',
740
+ resourceId,
741
+ 'ontology',
742
+ bindingKey,
743
+ ...(Array.isArray(ids) ? [ontologyIndex] : [])
744
+ ],
745
+ `Resource "${resourceId}" ontology binding "${bindingKey}" references unknown ${expectedKind} ontology ID "${ontologyId}"`
632
746
  )
633
747
  }
634
748
  })
635
- })
749
+ }
636
750
 
637
- // Phase 4: model.knowledge is now Record<id, OrgKnowledgeNode>
638
- Object.entries(model.knowledge).forEach(([nodeId, node]) => {
639
- node.ownerIds.forEach((roleId, ownerIndex) => {
640
- if (!rolesById.has(roleId)) {
641
- addIssue(
642
- ctx,
643
- ['knowledge', nodeId, 'ownerIds', ownerIndex],
644
- `Knowledge node "${node.id}" references unknown owner role "${roleId}"`
645
- )
646
- }
647
- })
751
+ Object.values(model.resources).forEach((resource) => {
752
+ const binding = resource.ontology
753
+ if (binding === undefined) return
754
+
755
+ validateResourceOntologyBinding(resource.id, 'actions', 'action', binding.actions)
756
+ validateResourceOntologyBinding(resource.id, 'primaryAction', 'action', binding.primaryAction)
757
+ validateResourceOntologyBinding(resource.id, 'reads', 'object', binding.reads)
758
+ validateResourceOntologyBinding(resource.id, 'writes', 'object', binding.writes)
759
+ validateResourceOntologyBinding(resource.id, 'usesCatalogs', 'catalog', binding.usesCatalogs)
760
+ validateResourceOntologyBinding(resource.id, 'emits', 'event', binding.emits)
648
761
  })
649
-
650
- // ---------------------------------------------------------------------------
651
- // B3, B4, B5, L19 — ContentNode refines (Phase 3, Wave 1A)
652
- // ---------------------------------------------------------------------------
653
- //
654
- // These refines apply recursively to every system in the model tree
655
- // (top-level systems + nested subsystems at any depth).
656
- //
657
- // B3 Cycle detection: parentContentId chain must not form a cycle.
658
- // B4 — Same-system-only: parentContentId must resolve within the same content map.
659
- // B5 — Payload validation: registered (kind, type) pairs validate data against
660
- // the registered payloadSchema; unregistered pairs pass through (per D2).
661
- // L19 Same-meta-kind parent: when both child and parent are registered, they
662
- // must share the same `kind` (meta-category).
663
-
762
+
763
+ Object.values(model.roles).forEach((role) => {
764
+ if (role.heldBy === undefined) return
765
+
766
+ asRoleHolderArray(role.heldBy).forEach((holder, holderIndex) => {
767
+ if (holder.kind !== 'agent') return
768
+
769
+ const resource = resourcesById.get(holder.agentId)
770
+ if (resource === undefined) {
771
+ addIssue(
772
+ ctx,
773
+ ['roles', role.id, 'heldBy', Array.isArray(role.heldBy) ? holderIndex : 'agentId'],
774
+ `Role "${role.id}" references unknown agent holder resource "${holder.agentId}"`
775
+ )
776
+ return
777
+ }
778
+
779
+ if (resource.kind !== 'agent') {
780
+ addIssue(
781
+ ctx,
782
+ ['roles', role.id, 'heldBy', Array.isArray(role.heldBy) ? holderIndex : 'agentId'],
783
+ `Role "${role.id}" agent holder "${holder.agentId}" must reference an agent resource`
784
+ )
785
+ }
786
+ })
787
+ })
788
+
789
+ // Phase 4: model.knowledge is now Record<id, OrgKnowledgeNode>
790
+ Object.entries(model.knowledge).forEach(([nodeId, node]) => {
791
+ node.ownerIds.forEach((roleId, ownerIndex) => {
792
+ if (!rolesById.has(roleId)) {
793
+ addIssue(
794
+ ctx,
795
+ ['knowledge', nodeId, 'ownerIds', ownerIndex],
796
+ `Knowledge node "${node.id}" references unknown owner role "${roleId}"`
797
+ )
798
+ }
799
+ })
800
+ })
801
+
802
+ // ---------------------------------------------------------------------------
803
+ // B3, B4, B5, L19 — ContentNode refines (Phase 3, Wave 1A)
804
+ // ---------------------------------------------------------------------------
805
+ //
806
+ // These refines apply recursively to every system in the model tree
807
+ // (top-level systems + nested subsystems at any depth).
808
+ //
809
+ // B3 — Cycle detection: parentContentId chain must not form a cycle.
810
+ // B4 — Same-system-only: parentContentId must resolve within the same content map.
811
+ // B5 — Payload validation: registered (kind, type) pairs validate data against
812
+ // the registered payloadSchema; unregistered pairs pass through (per D2).
813
+ // L19 — Same-meta-kind parent: when both child and parent are registered, they
814
+ // must share the same `kind` (meta-category).
815
+
664
816
  type SystemLike = {
665
817
  id?: string
666
818
  content?: Record<string, { kind: string; type: string; parentContentId?: string; data?: Record<string, unknown> }>
819
+ systems?: Record<string, SystemLike>
667
820
  subsystems?: Record<string, SystemLike>
668
821
  }
669
822
 
670
823
  function validateSystemContent(system: SystemLike, systemPath: Array<string | number>): void {
824
+ const childSystems = system.systems ?? system.subsystems
825
+ const childKey = system.systems !== undefined ? 'systems' : 'subsystems'
671
826
  const content = system.content
672
827
  if (content === undefined || Object.keys(content).length === 0) {
673
- // Recurse into subsystems even when own content is absent.
674
- if (system.subsystems !== undefined) {
675
- Object.entries(system.subsystems).forEach(([childKey, child]) => {
676
- validateSystemContent(child, [...systemPath, 'subsystems', childKey])
828
+ // Recurse into child systems even when own content is absent.
829
+ if (childSystems !== undefined) {
830
+ Object.entries(childSystems).forEach(([childLocalId, child]) => {
831
+ validateSystemContent(child, [...systemPath, childKey, childLocalId])
677
832
  })
678
833
  }
679
834
  return
680
- }
681
-
682
- // B4 — verify every parentContentId resolves within this content map.
683
- Object.entries(content).forEach(([localId, node]) => {
684
- if (node.parentContentId !== undefined && !(node.parentContentId in content)) {
685
- addIssue(
686
- ctx,
687
- [...systemPath, 'content', localId, 'parentContentId'],
688
- `Content node "${localId}" parentContentId "${node.parentContentId}" does not resolve within the same system`
689
- )
690
- }
691
- })
692
-
693
- // B3 — cycle detection on parentContentId chains within this content map.
694
- Object.entries(content).forEach(([localId, node]) => {
695
- const visited = new Set<string>()
696
- let currentId: string | undefined = node.parentContentId
697
-
698
- while (currentId !== undefined) {
699
- if (currentId === localId || visited.has(currentId)) {
700
- addIssue(
701
- ctx,
702
- [...systemPath, 'content', localId, 'parentContentId'],
703
- `Content node "${localId}" has a parentContentId cycle`
704
- )
705
- break
706
- }
707
- visited.add(currentId)
708
- currentId = content[currentId]?.parentContentId
709
- }
710
- })
711
-
712
- // B5 + L19 — per-node payload validation and same-meta-kind parent constraint.
713
- Object.entries(content).forEach(([localId, node]) => {
714
- const childDef = lookupContentType(node.kind, node.type)
715
-
716
- // B5 — validate data against registered payloadSchema when pair is known.
717
- if (childDef !== undefined && node.data !== undefined) {
718
- const result = childDef.payloadSchema.safeParse(node.data)
719
- if (!result.success) {
720
- addIssue(
721
- ctx,
722
- [...systemPath, 'content', localId, 'data'],
723
- `Content node "${localId}" (${node.kind}:${node.type}) data failed payload validation: ${result.error.message}`
724
- )
725
- }
726
- }
727
-
728
- // L19 — when both child and parent are registered, they must share the same kind.
729
- if (node.parentContentId !== undefined && childDef !== undefined) {
730
- const parentNode = content[node.parentContentId]
731
- if (parentNode !== undefined) {
732
- const parentDef = lookupContentType(parentNode.kind, parentNode.type)
733
- if (parentDef !== undefined && childDef.kind !== parentDef.kind) {
734
- addIssue(
735
- ctx,
736
- [...systemPath, 'content', localId, 'parentContentId'],
737
- `Content node "${localId}" kind "${childDef.kind}" cannot parent under "${node.parentContentId}" kind "${parentDef.kind}": parentContentId must be same-meta-kind (per L19)`
738
- )
739
- }
740
- }
741
- }
742
- })
743
-
744
- // Recurse into subsystems.
745
- if (system.subsystems !== undefined) {
746
- Object.entries(system.subsystems).forEach(([childKey, child]) => {
747
- validateSystemContent(child, [...systemPath, 'subsystems', childKey])
835
+ }
836
+
837
+ // B4 — verify every parentContentId resolves within this content map.
838
+ Object.entries(content).forEach(([localId, node]) => {
839
+ if (node.parentContentId !== undefined && !(node.parentContentId in content)) {
840
+ addIssue(
841
+ ctx,
842
+ [...systemPath, 'content', localId, 'parentContentId'],
843
+ `Content node "${localId}" parentContentId "${node.parentContentId}" does not resolve within the same system`
844
+ )
845
+ }
846
+ })
847
+
848
+ // B3 — cycle detection on parentContentId chains within this content map.
849
+ Object.entries(content).forEach(([localId, node]) => {
850
+ const visited = new Set<string>()
851
+ let currentId: string | undefined = node.parentContentId
852
+
853
+ while (currentId !== undefined) {
854
+ if (currentId === localId || visited.has(currentId)) {
855
+ addIssue(
856
+ ctx,
857
+ [...systemPath, 'content', localId, 'parentContentId'],
858
+ `Content node "${localId}" has a parentContentId cycle`
859
+ )
860
+ break
861
+ }
862
+ visited.add(currentId)
863
+ currentId = content[currentId]?.parentContentId
864
+ }
865
+ })
866
+
867
+ // B5 + L19 — per-node payload validation and same-meta-kind parent constraint.
868
+ Object.entries(content).forEach(([localId, node]) => {
869
+ const childDef = lookupContentType(node.kind, node.type)
870
+
871
+ // B5 — validate data against registered payloadSchema when pair is known.
872
+ if (childDef !== undefined && node.data !== undefined) {
873
+ const result = childDef.payloadSchema.safeParse(node.data)
874
+ if (!result.success) {
875
+ addIssue(
876
+ ctx,
877
+ [...systemPath, 'content', localId, 'data'],
878
+ `Content node "${localId}" (${node.kind}:${node.type}) data failed payload validation: ${result.error.message}`
879
+ )
880
+ }
881
+ }
882
+
883
+ // L19 — when both child and parent are registered, they must share the same kind.
884
+ if (node.parentContentId !== undefined && childDef !== undefined) {
885
+ const parentNode = content[node.parentContentId]
886
+ if (parentNode !== undefined) {
887
+ const parentDef = lookupContentType(parentNode.kind, parentNode.type)
888
+ if (parentDef !== undefined && childDef.kind !== parentDef.kind) {
889
+ addIssue(
890
+ ctx,
891
+ [...systemPath, 'content', localId, 'parentContentId'],
892
+ `Content node "${localId}" kind "${childDef.kind}" cannot parent under "${node.parentContentId}" kind "${parentDef.kind}": parentContentId must be same-meta-kind (per L19)`
893
+ )
894
+ }
895
+ }
896
+ }
897
+ })
898
+
899
+ // Recurse into child systems.
900
+ if (childSystems !== undefined) {
901
+ Object.entries(childSystems).forEach(([childLocalId, child]) => {
902
+ validateSystemContent(child, [...systemPath, childKey, childLocalId])
748
903
  })
749
904
  }
750
905
  }
@@ -752,4 +907,8 @@ export const OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((
752
907
  Object.entries(model.systems).forEach(([systemKey, system]) => {
753
908
  validateSystemContent(system as SystemLike, ['systems', systemKey])
754
909
  })
910
+
911
+ for (const diagnostic of ontologyCompilation.diagnostics) {
912
+ addIssue(ctx, diagnostic.path, diagnostic.message)
913
+ }
755
914
  })