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