@elevasis/core 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/dist/index.d.ts +2518 -2169
  2. package/dist/index.js +2495 -1095
  3. package/dist/knowledge/index.d.ts +706 -1044
  4. package/dist/knowledge/index.js +9 -9
  5. package/dist/organization-model/index.d.ts +2518 -2169
  6. package/dist/organization-model/index.js +2495 -1095
  7. package/dist/test-utils/index.d.ts +826 -1014
  8. package/dist/test-utils/index.js +1894 -1032
  9. package/package.json +3 -3
  10. package/src/__tests__/template-core-compatibility.test.ts +11 -79
  11. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +852 -397
  12. package/src/auth/multi-tenancy/permissions.ts +20 -8
  13. package/src/business/README.md +2 -2
  14. package/src/business/acquisition/api-schemas.test.ts +175 -2
  15. package/src/business/acquisition/api-schemas.ts +132 -16
  16. package/src/business/acquisition/build-templates.test.ts +4 -4
  17. package/src/business/acquisition/build-templates.ts +72 -30
  18. package/src/business/acquisition/crm-state-actions.test.ts +13 -11
  19. package/src/business/acquisition/index.ts +12 -0
  20. package/src/business/acquisition/types.ts +7 -3
  21. package/src/business/clients/api-schemas.test.ts +115 -0
  22. package/src/business/clients/api-schemas.ts +158 -0
  23. package/src/business/clients/index.ts +1 -0
  24. package/src/business/deals/api-schemas.ts +8 -0
  25. package/src/business/index.ts +5 -2
  26. package/src/business/projects/types.ts +19 -0
  27. package/src/execution/engine/__tests__/fixtures/test-agents.ts +10 -8
  28. package/src/execution/engine/agent/core/__tests__/agent.test.ts +16 -12
  29. package/src/execution/engine/agent/core/__tests__/error-passthrough.test.ts +4 -3
  30. package/src/execution/engine/agent/core/types.ts +25 -15
  31. package/src/execution/engine/agent/index.ts +6 -4
  32. package/src/execution/engine/agent/reasoning/__tests__/request-builder.test.ts +24 -18
  33. package/src/execution/engine/index.ts +3 -0
  34. package/src/execution/engine/workflow/types.ts +9 -2
  35. package/src/knowledge/README.md +8 -7
  36. package/src/knowledge/__tests__/queries.test.ts +74 -73
  37. package/src/knowledge/format.ts +10 -9
  38. package/src/knowledge/index.ts +1 -1
  39. package/src/knowledge/published.ts +1 -1
  40. package/src/knowledge/queries.ts +26 -25
  41. package/src/organization-model/README.md +73 -26
  42. package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -0
  43. package/src/organization-model/__tests__/defaults.test.ts +76 -96
  44. package/src/organization-model/__tests__/domains/actions.test.ts +56 -0
  45. package/src/organization-model/__tests__/domains/customers.test.ts +299 -295
  46. package/src/organization-model/__tests__/domains/entities.test.ts +56 -0
  47. package/src/organization-model/__tests__/domains/goals.test.ts +493 -479
  48. package/src/organization-model/__tests__/domains/identity.test.ts +280 -279
  49. package/src/organization-model/__tests__/domains/navigation.test.ts +268 -212
  50. package/src/organization-model/__tests__/domains/offerings.test.ts +414 -419
  51. package/src/organization-model/__tests__/domains/policies.test.ts +323 -0
  52. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +271 -271
  53. package/src/organization-model/__tests__/domains/resources.test.ts +310 -0
  54. package/src/organization-model/__tests__/domains/roles.test.ts +463 -347
  55. package/src/organization-model/__tests__/domains/statuses.test.ts +246 -243
  56. package/src/organization-model/__tests__/domains/systems.test.ts +209 -0
  57. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +361 -0
  58. package/src/organization-model/__tests__/foundation.test.ts +74 -102
  59. package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -0
  60. package/src/organization-model/__tests__/graph.test.ts +899 -71
  61. package/src/organization-model/__tests__/knowledge.test.ts +209 -49
  62. package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -0
  63. package/src/organization-model/__tests__/migration-helpers.test.ts +591 -0
  64. package/src/organization-model/__tests__/prospecting-ssot.test.ts +36 -27
  65. package/src/organization-model/__tests__/recursive-system-schema.test.ts +520 -0
  66. package/src/organization-model/__tests__/resolve.test.ts +174 -23
  67. package/src/organization-model/__tests__/schema.test.ts +291 -114
  68. package/src/organization-model/__tests__/surface-projection.test.ts +207 -97
  69. package/src/organization-model/catalogs/lead-gen.ts +144 -0
  70. package/src/organization-model/content-kinds/config.ts +36 -0
  71. package/src/organization-model/content-kinds/index.ts +74 -0
  72. package/src/organization-model/content-kinds/pipeline.ts +68 -0
  73. package/src/organization-model/content-kinds/registry.ts +44 -0
  74. package/src/organization-model/content-kinds/status.ts +71 -0
  75. package/src/organization-model/content-kinds/template.ts +83 -0
  76. package/src/organization-model/content-kinds/types.ts +117 -0
  77. package/src/organization-model/contracts.ts +13 -3
  78. package/src/organization-model/defaults.ts +499 -86
  79. package/src/organization-model/domains/actions.ts +239 -0
  80. package/src/organization-model/domains/customers.ts +78 -75
  81. package/src/organization-model/domains/entities.ts +144 -0
  82. package/src/organization-model/domains/goals.ts +83 -80
  83. package/src/organization-model/domains/knowledge.ts +76 -17
  84. package/src/organization-model/domains/navigation.ts +107 -384
  85. package/src/organization-model/domains/offerings.ts +71 -66
  86. package/src/organization-model/domains/policies.ts +102 -0
  87. package/src/organization-model/domains/projects.ts +14 -48
  88. package/src/organization-model/domains/prospecting.ts +62 -181
  89. package/src/organization-model/domains/resources.ts +145 -0
  90. package/src/organization-model/domains/roles.ts +96 -55
  91. package/src/organization-model/domains/sales.ts +10 -219
  92. package/src/organization-model/domains/shared.ts +57 -57
  93. package/src/organization-model/domains/statuses.ts +339 -130
  94. package/src/organization-model/domains/systems.ts +203 -0
  95. package/src/organization-model/foundation.ts +54 -67
  96. package/src/organization-model/graph/build.ts +682 -54
  97. package/src/organization-model/graph/link.ts +1 -1
  98. package/src/organization-model/graph/schema.ts +24 -9
  99. package/src/organization-model/graph/types.ts +20 -7
  100. package/src/organization-model/helpers.ts +231 -26
  101. package/src/organization-model/icons.ts +1 -0
  102. package/src/organization-model/index.ts +118 -5
  103. package/src/organization-model/migration-helpers.ts +249 -0
  104. package/src/organization-model/organization-graph.mdx +16 -15
  105. package/src/organization-model/organization-model.mdx +111 -44
  106. package/src/organization-model/published.ts +172 -19
  107. package/src/organization-model/resolve.ts +117 -54
  108. package/src/organization-model/schema.ts +654 -112
  109. package/src/organization-model/surface-projection.ts +116 -122
  110. package/src/organization-model/types.ts +146 -20
  111. package/src/platform/api/types.ts +38 -35
  112. package/src/platform/constants/versions.ts +1 -1
  113. package/src/platform/registry/__tests__/command-view.test.ts +6 -8
  114. package/src/platform/registry/__tests__/resource-link.test.ts +13 -8
  115. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +16 -31
  116. package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -0
  117. package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2005
  118. package/src/platform/registry/__tests__/validation.test.ts +1347 -1086
  119. package/src/platform/registry/index.ts +14 -0
  120. package/src/platform/registry/resource-registry.ts +52 -2
  121. package/src/platform/registry/serialization.ts +241 -202
  122. package/src/platform/registry/serialized-types.ts +1 -0
  123. package/src/platform/registry/types.ts +411 -361
  124. package/src/platform/registry/validation.ts +745 -513
  125. package/src/projects/api-schemas.ts +290 -267
  126. package/src/reference/_generated/contracts.md +853 -397
  127. package/src/reference/glossary.md +23 -18
  128. package/src/supabase/database.types.ts +181 -0
  129. package/src/test-utils/test-utils.test.ts +1 -6
  130. package/src/organization-model/__tests__/domains/operations.test.ts +0 -203
  131. package/src/organization-model/domains/features.ts +0 -31
  132. package/src/organization-model/domains/operations.ts +0 -85
@@ -1,35 +1,103 @@
1
1
  import { z } from 'zod'
2
+ import { lookupContentType } from './content-kinds/index'
2
3
  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'
4
+ import { OrganizationModelNavigationSchema, type SidebarNode } from './domains/navigation'
8
5
  import { IdentityDomainSchema, DEFAULT_ORGANIZATION_MODEL_IDENTITY } from './domains/identity'
9
6
  import { CustomersDomainSchema, DEFAULT_ORGANIZATION_MODEL_CUSTOMERS } from './domains/customers'
10
7
  import { OfferingsDomainSchema, DEFAULT_ORGANIZATION_MODEL_OFFERINGS } from './domains/offerings'
11
8
  import { RolesDomainSchema, DEFAULT_ORGANIZATION_MODEL_ROLES } from './domains/roles'
12
9
  import { GoalsDomainSchema, DEFAULT_ORGANIZATION_MODEL_GOALS } from './domains/goals'
13
- import { OperationsDomainSchema } from './domains/operations'
14
- import { StatusesDomainSchema } from './domains/statuses'
15
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
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',
26
+ 'systems',
27
+ 'resources',
28
+ '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' },
49
+ systems: { version: 1, lastModified: '2026-05-10' },
50
+ resources: { version: 1, lastModified: '2026-05-10' },
51
+ 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,
65
+ systems: OrganizationModelDomainMetadataSchema,
66
+ resources: OrganizationModelDomainMetadataSchema,
67
+ 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.
17
84
  const OrganizationModelSchemaBase = z.object({
18
85
  version: z.literal(1).default(1),
19
- features: z.array(FeatureSchema).default([]),
86
+ domainMetadata: OrganizationModelDomainMetadataByDomainSchema,
20
87
  branding: OrganizationModelBrandingSchema,
21
- navigation: OrganizationModelNavigationSchema.default({ surfaces: [], groups: [] }),
22
- sales: OrganizationModelSalesSchema,
23
- prospecting: OrganizationModelProspectingSchema,
24
- projects: OrganizationModelProjectsSchema,
88
+ navigation: OrganizationModelNavigationSchema,
25
89
  identity: IdentityDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_IDENTITY),
26
90
  customers: CustomersDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_CUSTOMERS),
27
91
  offerings: OfferingsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_OFFERINGS),
28
92
  roles: RolesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_ROLES),
29
93
  goals: GoalsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_GOALS),
30
- statuses: StatusesDomainSchema.default({ entries: [] }),
31
- operations: OperationsDomainSchema.default({ entries: [] }),
32
- knowledge: KnowledgeDomainSchema.default({ nodes: [] })
94
+ systems: SystemsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_SYSTEMS),
95
+ resources: ResourcesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_RESOURCES),
96
+ 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({})
33
101
  })
34
102
 
35
103
  function addIssue(ctx: z.RefinementCtx, path: Array<string | number>, message: string): void {
@@ -40,174 +108,648 @@ function addIssue(ctx: z.RefinementCtx, path: Array<string | number>, message: s
40
108
  })
41
109
  }
42
110
 
43
- function collectIds<T extends { id: string }>(
44
- items: T[],
45
- ctx: z.RefinementCtx,
46
- collectionPath: Array<string | number>,
47
- label: string
48
- ): Map<string, T> {
49
- const itemsById = new Map<string, T>()
50
-
51
- items.forEach((item, index) => {
52
- if (itemsById.has(item.id)) {
53
- addIssue(ctx, [...collectionPath, index, 'id'], `${label} id "${item.id}" must be unique`)
54
- return
55
- }
56
-
57
- itemsById.set(item.id, item)
58
- })
59
-
60
- return itemsById
111
+ function isLifecycleEnabled(lifecycle: string | undefined, enabled: boolean | undefined): boolean {
112
+ if (enabled === false) return false
113
+ return lifecycle !== 'deprecated' && lifecycle !== 'archived'
61
114
  }
62
115
 
63
- const LEGACY_FEATURE_ALIASES = new Map<string, string>([
64
- ['crm', 'sales.crm'],
65
- ['lead-gen', 'sales.lead-gen'],
66
- ['submitted-requests', 'monitoring.submitted-requests']
67
- ])
116
+ function defaultSystemPathFor(id: string): string {
117
+ return `/${id.replaceAll('.', '/')}`
118
+ }
68
119
 
69
- function hasFeature(featuresById: Map<string, unknown>, featureId: string): boolean {
70
- return featuresById.has(featureId) || featuresById.has(LEGACY_FEATURE_ALIASES.get(featureId) ?? '')
120
+ function asRoleHolderArray(
121
+ heldBy: NonNullable<z.infer<typeof OrganizationModelSchemaBase>['roles'][string]['heldBy']>
122
+ ) {
123
+ return Array.isArray(heldBy) ? heldBy : [heldBy]
71
124
  }
72
125
 
73
- function defaultFeaturePathFor(id: string): string {
74
- return `/${id.replaceAll('.', '/')}`
126
+ function isKnowledgeKindCompatibleWithTarget(knowledgeKind: string, targetKind: string): boolean {
127
+ if (knowledgeKind === 'reference') return true
128
+ if (knowledgeKind === 'playbook') {
129
+ return ['system', 'resource', 'stage', 'action'].includes(targetKind)
130
+ }
131
+ if (knowledgeKind === 'strategy') {
132
+ return ['system', 'goal', 'offering', 'customer-segment'].includes(targetKind)
133
+ }
134
+ return false
75
135
  }
76
136
 
77
137
  export const OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((model, ctx) => {
78
- const featuresById = collectIds(model.features, ctx, ['features'], 'Feature')
79
- const featureIdsByEffectivePath = new Map<string, string>()
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 }
80
142
 
81
- model.features.forEach((feature, featureIndex) => {
82
- const segments = feature.id.split('.')
83
- if (segments.length > 1) {
84
- const parentId = segments.slice(0, -1).join('.')
85
- if (!featuresById.has(parentId)) {
86
- addIssue(
87
- ctx,
88
- ['features', featureIndex, 'id'],
89
- `Feature "${feature.id}" references unknown parent "${parentId}"`
90
- )
143
+ function collectAllSystems(
144
+ systems: Record<string, SystemEntry>,
145
+ prefix = '',
146
+ schemaPath: Array<string | number> = ['systems']
147
+ ): SystemWithPath[] {
148
+ const result: SystemWithPath[] = []
149
+ for (const [key, system] of Object.entries(systems)) {
150
+ const path = prefix ? `${prefix}.${key}` : key
151
+ const currentSchemaPath = [...schemaPath, key]
152
+ result.push({ path, schemaPath: currentSchemaPath, system })
153
+ if (system.subsystems !== undefined) {
154
+ result.push(...collectAllSystems(system.subsystems, path, [...currentSchemaPath, 'subsystems']))
91
155
  }
92
156
  }
157
+ 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
+ }
93
176
 
94
- const hasChildren = model.features.some(
95
- (candidate) => candidate.id.startsWith(`${feature.id}.`) && candidate.id !== feature.id
96
- )
97
- const contributesRoutePath = feature.path !== undefined || !hasChildren
177
+ const hasChildren =
178
+ Object.keys(system.subsystems ?? {}).length > 0 ||
179
+ 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
98
181
  if (contributesRoutePath) {
99
- const effectivePath = feature.path ?? defaultFeaturePathFor(feature.id)
100
- const existingFeatureId = featureIdsByEffectivePath.get(effectivePath)
101
- if (existingFeatureId !== undefined) {
182
+ const effectivePath = system.ui?.path ?? system.path ?? defaultSystemPathFor(path)
183
+ const existingSystemId = systemIdsByEffectivePath.get(effectivePath)
184
+ if (existingSystemId !== undefined) {
102
185
  addIssue(
103
186
  ctx,
104
- ['features', featureIndex, feature.path === undefined ? 'id' : 'path'],
105
- `Feature "${feature.id}" effective path "${effectivePath}" duplicates feature "${existingFeatureId}"`
187
+ [...schemaPath, system.ui?.path !== undefined ? 'ui' : 'path'],
188
+ `System "${path}" effective path "${effectivePath}" duplicates system "${existingSystemId}"`
106
189
  )
107
190
  } else {
108
- featureIdsByEffectivePath.set(effectivePath, feature.id)
191
+ systemIdsByEffectivePath.set(effectivePath, path)
109
192
  }
110
193
  }
111
194
 
112
- if (hasChildren && feature.enabled) {
113
- const hasEnabledDescendant = model.features.some(
114
- (candidate) => candidate.id.startsWith(`${feature.id}.`) && candidate.enabled
115
- )
195
+ if (hasChildren && isLifecycleEnabled(system.lifecycle, system.enabled)) {
196
+ const hasEnabledDescendant =
197
+ Object.values(system.subsystems ?? {}).some((candidate) =>
198
+ isLifecycleEnabled(candidate.lifecycle, candidate.enabled)
199
+ ) ||
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
+ )
116
206
  if (!hasEnabledDescendant) {
117
207
  addIssue(
118
208
  ctx,
119
- ['features', featureIndex, 'enabled'],
120
- `Feature "${feature.id}" is enabled but has no enabled descendants`
209
+ [...schemaPath, 'lifecycle'],
210
+ `System "${path}" is active but has no active descendants`
121
211
  )
122
212
  }
123
213
  }
124
214
  })
125
215
 
126
- const surfacesById = collectIds(model.navigation.surfaces, ctx, ['navigation', 'surfaces'], 'Navigation surface')
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
+ })
127
230
 
128
- if (model.navigation.defaultSurfaceId !== undefined && !surfacesById.has(model.navigation.defaultSurfaceId)) {
129
- addIssue(
130
- ctx,
131
- ['navigation', 'defaultSurfaceId'],
132
- `Navigation defaultSurfaceId references unknown surface "${model.navigation.defaultSurfaceId}"`
133
- )
231
+ type CollectedSidebarSurface = {
232
+ id: string
233
+ node: Extract<SidebarNode, { type: 'surface' }>
234
+ path: Array<string | number>
134
235
  }
135
236
 
136
- model.navigation.groups.forEach((group, groupIndex) => {
137
- group.surfaceIds.forEach((surfaceId, surfaceIndex) => {
138
- if (!surfacesById.has(surfaceId)) {
139
- addIssue(
140
- ctx,
141
- ['navigation', 'groups', groupIndex, 'surfaceIds', surfaceIndex],
142
- `Navigation group "${group.id}" references unknown surface "${surfaceId}"`
143
- )
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)
144
256
  }
145
- })
146
- })
147
257
 
148
- model.navigation.surfaces.forEach((surface, surfaceIndex) => {
149
- if (surface.featureId !== undefined && !hasFeature(featuresById, surface.featureId)) {
150
- addIssue(
151
- ctx,
152
- ['navigation', 'surfaces', surfaceIndex, 'featureId'],
153
- `Navigation surface "${surface.id}" references unknown feature "${surface.featureId}"`
154
- )
155
- }
258
+ if (node.type === 'group') {
259
+ collectSidebarNodes(node.children, [...nodePath, 'children'])
260
+ return
261
+ }
156
262
 
157
- surface.featureIds.forEach((featureId, featureIndex) => {
158
- if (!hasFeature(featuresById, featureId)) {
263
+ sidebarSurfaces.push({ id: nodeId, node, path: nodePath })
264
+ const normalizedPath = normalizeRoutePath(node.path)
265
+ const existingSurfaceId = sidebarSurfacePaths.get(normalizedPath)
266
+ if (existingSurfaceId !== undefined) {
159
267
  addIssue(
160
268
  ctx,
161
- ['navigation', 'surfaces', surfaceIndex, 'featureIds', featureIndex],
162
- `Navigation surface "${surface.id}" references unknown feature "${featureId}"`
269
+ [...nodePath, 'path'],
270
+ `Sidebar surface path "${node.path}" duplicates surface "${existingSurfaceId}"`
163
271
  )
272
+ } else {
273
+ sidebarSurfacePaths.set(normalizedPath, nodeId)
164
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
+ })
165
285
  })
166
- })
286
+ }
287
+
288
+ collectSidebarNodes(model.navigation.sidebar.primary, ['navigation', 'sidebar', 'primary'])
289
+ collectSidebarNodes(model.navigation.sidebar.bottom, ['navigation', 'sidebar', 'bottom'])
167
290
 
168
291
  // Offerings -> CustomerSegment cross-ref: targetSegmentIds must resolve
169
- const segmentsById = new Map(model.customers.segments.map((seg) => [seg.id, seg]))
170
- model.offerings.products.forEach((product, productIndex) => {
292
+ const segmentsById = new Map(Object.entries(model.customers))
293
+ Object.values(model.offerings).forEach((product) => {
171
294
  product.targetSegmentIds.forEach((segmentId, segmentIndex) => {
172
295
  if (!segmentsById.has(segmentId)) {
173
296
  addIssue(
174
297
  ctx,
175
- ['offerings', 'products', productIndex, 'targetSegmentIds', segmentIndex],
298
+ ['offerings', product.id, 'targetSegmentIds', segmentIndex],
176
299
  `Product "${product.id}" references unknown customer segment "${segmentId}"`
177
300
  )
178
301
  }
179
302
  })
180
303
 
181
- // Offerings -> Feature cross-ref: deliveryFeatureId must resolve (when present)
182
- if (product.deliveryFeatureId !== undefined && !hasFeature(featuresById, product.deliveryFeatureId)) {
304
+ // Offerings -> System cross-ref: deliveryFeatureId must resolve (when present)
305
+ if (product.deliveryFeatureId !== undefined && !systemsById.has(product.deliveryFeatureId)) {
183
306
  addIssue(
184
307
  ctx,
185
- ['offerings', 'products', productIndex, 'deliveryFeatureId'],
186
- `Product "${product.id}" references unknown delivery feature "${product.deliveryFeatureId}"`
308
+ ['offerings', product.id, 'deliveryFeatureId'],
309
+ `Product "${product.id}" references unknown delivery system "${product.deliveryFeatureId}"`
187
310
  )
188
311
  }
189
312
  })
190
313
 
191
314
  // Goals -> period-range validation: periodEnd must be strictly after periodStart
192
- model.goals.objectives.forEach((objective, index) => {
315
+ Object.values(model.goals).forEach((objective) => {
193
316
  if (objective.periodEnd <= objective.periodStart) {
194
317
  addIssue(
195
318
  ctx,
196
- ['goals', 'objectives', index, 'periodEnd'],
319
+ ['goals', objective.id, 'periodEnd'],
197
320
  `Goal "${objective.id}" has periodEnd "${objective.periodEnd}" which must be strictly after periodStart "${objective.periodStart}"`
198
321
  )
199
322
  }
200
323
  })
201
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
+ })
353
+
354
+ Object.values(model.entities).forEach((entity) => {
355
+ if (!systemsById.has(entity.ownedBySystemId)) {
356
+ addIssue(
357
+ ctx,
358
+ ['entities', entity.id, 'ownedBySystemId'],
359
+ `Entity "${entity.id}" references unknown ownedBySystemId "${entity.ownedBySystemId}"`
360
+ )
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
+ })
372
+ })
373
+
202
374
  // Roles -> reportsToId cross-ref: each reportsToId must resolve to another role in the same collection
203
- const rolesById = new Map(model.roles.roles.map((role) => [role.id, role]))
204
- model.roles.roles.forEach((role, roleIndex) => {
375
+ const rolesById = new Map(Object.entries(model.roles))
376
+ Object.values(model.roles).forEach((role) => {
205
377
  if (role.reportsToId !== undefined && !rolesById.has(role.reportsToId)) {
206
378
  addIssue(
207
379
  ctx,
208
- ['roles', 'roles', roleIndex, 'reportsToId'],
380
+ ['roles', role.id, 'reportsToId'],
209
381
  `Role "${role.id}" references unknown reportsToId "${role.reportsToId}"`
210
382
  )
211
383
  }
212
384
  })
385
+
386
+ Object.values(model.roles).forEach((role) => {
387
+ const visited = new Set<string>()
388
+ let currentReportsToId = role.reportsToId
389
+
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
+ })
400
+
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
+ )
459
+ }
460
+ })
461
+ })
462
+
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
+ }
472
+ })
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).
477
+
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))
496
+
497
+ 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)
555
+ if (kind === 'goal') return goalsById.has(id)
556
+ if (kind === 'customer-segment') return segmentsById.has(id)
557
+ if (kind === 'offering') return offeringsById.has(id)
558
+ return false
559
+ }
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
+
585
+ 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
+ )
608
+ }
609
+ })
610
+
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
+ }
626
+
627
+ if (resource.kind !== 'agent') {
628
+ addIssue(
629
+ 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`
632
+ )
633
+ }
634
+ })
635
+ })
636
+
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
+ })
648
+ })
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
+
664
+ type SystemLike = {
665
+ id?: string
666
+ content?: Record<string, { kind: string; type: string; parentContentId?: string; data?: Record<string, unknown> }>
667
+ subsystems?: Record<string, SystemLike>
668
+ }
669
+
670
+ function validateSystemContent(system: SystemLike, systemPath: Array<string | number>): void {
671
+ const content = system.content
672
+ 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])
677
+ })
678
+ }
679
+ 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])
748
+ })
749
+ }
750
+ }
751
+
752
+ Object.entries(model.systems).forEach(([systemKey, system]) => {
753
+ validateSystemContent(system as SystemLike, ['systems', systemKey])
754
+ })
213
755
  })