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