@elevasis/core 0.26.0 → 0.28.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 (49) hide show
  1. package/dist/index.d.ts +162 -105
  2. package/dist/index.js +280 -174
  3. package/dist/knowledge/index.d.ts +43 -43
  4. package/dist/organization-model/index.d.ts +162 -105
  5. package/dist/organization-model/index.js +280 -174
  6. package/dist/test-utils/index.d.ts +20 -20
  7. package/dist/test-utils/index.js +184 -126
  8. package/package.json +3 -3
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +976 -1063
  10. package/src/business/acquisition/api-schemas.test.ts +1962 -1841
  11. package/src/business/acquisition/api-schemas.ts +1461 -1464
  12. package/src/business/acquisition/crm-next-action.test.ts +45 -25
  13. package/src/business/acquisition/crm-next-action.ts +227 -220
  14. package/src/business/acquisition/crm-priority.test.ts +41 -8
  15. package/src/business/acquisition/crm-priority.ts +365 -349
  16. package/src/business/acquisition/crm-state-actions.test.ts +208 -153
  17. package/src/business/acquisition/derive-actions.test.ts +90 -13
  18. package/src/business/acquisition/derive-actions.ts +8 -139
  19. package/src/business/acquisition/ontology-validation.ts +72 -158
  20. package/src/business/pdf/sections/investment.ts +1 -1
  21. package/src/business/pdf/sections/summary-investment.ts +1 -1
  22. package/src/execution/engine/tools/tool-maps.ts +872 -831
  23. package/src/organization-model/__tests__/cross-ref.test.ts +167 -0
  24. package/src/organization-model/__tests__/define-domain-record.test.ts +289 -0
  25. package/src/organization-model/__tests__/om-spine-doc-contract.test.ts +56 -0
  26. package/src/organization-model/__tests__/published-zero-leak.test.ts +60 -1
  27. package/src/organization-model/__tests__/resolve.test.ts +1 -1
  28. package/src/organization-model/__tests__/schema-refinements.test.ts +72 -0
  29. package/src/organization-model/cross-ref.ts +175 -0
  30. package/src/organization-model/domains/actions.ts +13 -0
  31. package/src/organization-model/domains/branding.ts +6 -6
  32. package/src/organization-model/domains/customers.ts +95 -78
  33. package/src/organization-model/domains/entities.ts +157 -144
  34. package/src/organization-model/domains/goals.ts +100 -83
  35. package/src/organization-model/domains/knowledge.ts +106 -93
  36. package/src/organization-model/domains/offerings.ts +88 -71
  37. package/src/organization-model/domains/policies.ts +115 -102
  38. package/src/organization-model/domains/roles.ts +109 -96
  39. package/src/organization-model/domains/sales.test.ts +104 -218
  40. package/src/organization-model/domains/sales.ts +212 -375
  41. package/src/organization-model/domains/statuses.ts +351 -339
  42. package/src/organization-model/domains/systems.ts +176 -164
  43. package/src/organization-model/helpers.ts +331 -306
  44. package/src/organization-model/index.ts +43 -0
  45. package/src/organization-model/published.ts +27 -2
  46. package/src/organization-model/schema-refinements.ts +667 -0
  47. package/src/organization-model/schema.ts +8 -715
  48. package/src/platform/constants/versions.ts +1 -1
  49. package/src/reference/_generated/contracts.md +1000 -1087
@@ -1,26 +1,20 @@
1
1
  import { z } from 'zod'
2
- import { OrganizationModelBrandingSchema } from './domains/branding'
3
- import { OrganizationModelNavigationSchema, type SidebarNode } from './domains/navigation'
2
+ import { OrganizationModelBrandingSchema, DEFAULT_ORGANIZATION_MODEL_BRANDING } from './domains/branding'
3
+ import { OrganizationModelNavigationSchema } from './domains/navigation'
4
4
  import { IdentityDomainSchema, DEFAULT_ORGANIZATION_MODEL_IDENTITY } from './domains/identity'
5
5
  import { CustomersDomainSchema, DEFAULT_ORGANIZATION_MODEL_CUSTOMERS } from './domains/customers'
6
6
  import { OfferingsDomainSchema, DEFAULT_ORGANIZATION_MODEL_OFFERINGS } from './domains/offerings'
7
7
  import { RolesDomainSchema, DEFAULT_ORGANIZATION_MODEL_ROLES } from './domains/roles'
8
8
  import { GoalsDomainSchema, DEFAULT_ORGANIZATION_MODEL_GOALS } from './domains/goals'
9
9
  import { KnowledgeDomainSchema } from './domains/knowledge'
10
- import { SystemsDomainSchema, DEFAULT_ORGANIZATION_MODEL_SYSTEMS, type SystemEntry } from './domains/systems'
10
+ import { SystemsDomainSchema, DEFAULT_ORGANIZATION_MODEL_SYSTEMS } from './domains/systems'
11
11
  import { ResourcesDomainSchema, DEFAULT_ORGANIZATION_MODEL_RESOURCES } from './domains/resources'
12
- import { OmTopologyDomainSchema, DEFAULT_ORGANIZATION_MODEL_TOPOLOGY, type OmTopologyNodeRef } from './domains/topology'
12
+ import { OmTopologyDomainSchema, DEFAULT_ORGANIZATION_MODEL_TOPOLOGY } from './domains/topology'
13
13
  import { ActionsDomainSchema, DEFAULT_ORGANIZATION_MODEL_ACTIONS } from './domains/actions'
14
14
  import { EntitiesDomainSchema, DEFAULT_ORGANIZATION_MODEL_ENTITIES } from './domains/entities'
15
15
  import { PoliciesDomainSchema, DEFAULT_ORGANIZATION_MODEL_POLICIES } from './domains/policies'
16
- import {
17
- compileOrganizationOntology,
18
- DEFAULT_ONTOLOGY_SCOPE,
19
- listResolvedOntologyRecords,
20
- OntologyScopeSchema,
21
- type OntologyKind
22
- } from './ontology'
23
- import { ContractRefSchema } from './domains/resources'
16
+ import { DEFAULT_ONTOLOGY_SCOPE, OntologyScopeSchema } from './ontology'
17
+ import { refineOrganizationModel } from './schema-refinements'
24
18
 
25
19
  // Phase 4 cut: 'sales', 'prospecting', 'projects', 'statuses' removed.
26
20
  // domainMetadata.knowledge covers versioning for the knowledge flat-map (D7).
@@ -98,7 +92,7 @@ export const OrganizationModelDomainMetadataByDomainSchema = z
98
92
  const OrganizationModelSchemaBase = z.object({
99
93
  version: z.literal(1).default(1),
100
94
  domainMetadata: OrganizationModelDomainMetadataByDomainSchema,
101
- branding: OrganizationModelBrandingSchema,
95
+ branding: OrganizationModelBrandingSchema.default(DEFAULT_ORGANIZATION_MODEL_BRANDING),
102
96
  navigation: OrganizationModelNavigationSchema,
103
97
  identity: IdentityDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_IDENTITY),
104
98
  customers: CustomersDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_CUSTOMERS),
@@ -116,705 +110,4 @@ const OrganizationModelSchemaBase = z.object({
116
110
  knowledge: KnowledgeDomainSchema.default({})
117
111
  })
118
112
 
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
- function isKnowledgeKindCompatibleWithTarget(knowledgeKind: string, targetKind: string): boolean {
143
- if (knowledgeKind === 'reference') return true
144
- if (knowledgeKind === 'playbook') {
145
- return ['system', 'resource', 'stage', 'action', 'ontology'].includes(targetKind)
146
- }
147
- if (knowledgeKind === 'strategy') {
148
- return ['system', 'goal', 'offering', 'customer-segment', 'ontology'].includes(targetKind)
149
- }
150
- return false
151
- }
152
-
153
- function isRecord(value: unknown): value is Record<string, unknown> {
154
- return typeof value === 'object' && value !== null && !Array.isArray(value)
155
- }
156
-
157
- export const OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((model, ctx) => {
158
- // Collect ALL system entries recursively — top-level systems plus any nested subsystems.
159
- // Wave 2 canonical OM authors nested subsystems (e.g. sys → subsystems → 'lead-gen' with id
160
- // 'sys.lead-gen'). Resource systemPath cross-refs must resolve against the full flattened set.
161
- type SystemWithPath = { path: string; schemaPath: Array<string | number>; system: SystemEntry }
162
-
163
- function collectAllSystems(
164
- systems: Record<string, SystemEntry>,
165
- prefix = '',
166
- schemaPath: Array<string | number> = ['systems']
167
- ): SystemWithPath[] {
168
- const result: SystemWithPath[] = []
169
- for (const [key, system] of Object.entries(systems)) {
170
- const path = prefix ? `${prefix}.${key}` : key
171
- const currentSchemaPath = [...schemaPath, key]
172
- result.push({ path, schemaPath: currentSchemaPath, system })
173
- const childSystems = system.systems ?? system.subsystems
174
- if (childSystems !== undefined) {
175
- result.push(
176
- ...collectAllSystems(childSystems, path, [
177
- ...currentSchemaPath,
178
- system.systems !== undefined ? 'systems' : 'subsystems'
179
- ])
180
- )
181
- }
182
- }
183
- return result
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
-
203
- const hasChildren =
204
- Object.keys(system.systems ?? system.subsystems ?? {}).length > 0 ||
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)) {
224
- const hasEnabledDescendant =
225
- Object.values(system.systems ?? system.subsystems ?? {}).some((candidate) =>
226
- isLifecycleEnabled(candidate.lifecycle, candidate.enabled)
227
- ) ||
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
- })
511
- const actionIds = new Set(Object.keys(model.actions))
512
- const offeringsById = new Map(Object.entries(model.offerings))
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
- }
521
- const ontologyIndexByKind = {
522
- object: ontologyCompilation.ontology.objectTypes,
523
- link: ontologyCompilation.ontology.linkTypes,
524
- action: ontologyCompilation.ontology.actionTypes,
525
- catalog: ontologyCompilation.ontology.catalogTypes,
526
- event: ontologyCompilation.ontology.eventTypes,
527
- interface: ontologyCompilation.ontology.interfaceTypes,
528
- 'value-type': ontologyCompilation.ontology.valueTypes,
529
- property: ontologyCompilation.ontology.sharedProperties,
530
- group: ontologyCompilation.ontology.groups,
531
- surface: ontologyCompilation.ontology.surfaces
532
- } satisfies Record<OntologyKind, Record<string, unknown>>
533
- const ontologyIds = new Set(Object.values(ontologyIndexByKind).flatMap((index) => Object.keys(index)))
534
-
535
- function topologyTargetExists(ref: OmTopologyNodeRef): boolean {
536
- if (ref.kind === 'system') return systemsById.has(ref.id)
537
- if (ref.kind === 'resource') return resourcesById.has(ref.id)
538
- if (ref.kind === 'ontology') return ontologyIds.has(ref.id)
539
- if (ref.kind === 'policy') return policiesById.has(ref.id)
540
- if (ref.kind === 'role') return rolesById.has(ref.id)
541
-
542
- // Trigger, human checkpoint, and external resource refs are projected
543
- // topology nodes during the bridge period; their owning runtime indexes are
544
- // validated by deployment projection in later waves.
545
- return true
546
- }
547
-
548
- Object.entries(model.topology.relationships).forEach(([relationshipId, relationship]) => {
549
- ;(['from', 'to'] as const).forEach((side) => {
550
- const ref = relationship[side]
551
- if (topologyTargetExists(ref)) return
552
-
553
- addIssue(
554
- ctx,
555
- ['topology', 'relationships', relationshipId, side],
556
- `Topology relationship "${relationshipId}" ${side} references unknown ${ref.kind} "${ref.id}"`
557
- )
558
- })
559
- })
560
-
561
- const ontologyReferenceKeyKinds = {
562
- valueType: 'value-type',
563
- catalogType: 'catalog',
564
- objectType: 'object',
565
- eventType: 'event',
566
- actionType: 'action',
567
- linkType: 'link',
568
- interfaceType: 'interface',
569
- propertyType: 'property',
570
- groupType: 'group',
571
- surfaceType: 'surface',
572
- stepCatalog: 'catalog'
573
- } satisfies Record<string, OntologyKind>
574
-
575
- function validateKnownOntologyReferences(
576
- ownerId: string,
577
- value: unknown,
578
- path: Array<string | number>,
579
- seen = new WeakSet<object>()
580
- ): void {
581
- if (Array.isArray(value)) {
582
- value.forEach((entry, index) => validateKnownOntologyReferences(ownerId, entry, [...path, index], seen))
583
- return
584
- }
585
-
586
- if (!isRecord(value)) return
587
- if (seen.has(value)) return
588
- seen.add(value)
589
-
590
- Object.entries(value).forEach(([key, entry]) => {
591
- const expectedKind = ontologyReferenceKeyKinds[key as keyof typeof ontologyReferenceKeyKinds]
592
- if (expectedKind !== undefined) {
593
- if (typeof entry !== 'string') {
594
- addIssue(ctx, [...path, key], `Ontology record "${ownerId}" ${key} must be an ontology ID string`)
595
- } else if (ontologyIndexByKind[expectedKind][entry] === undefined) {
596
- addIssue(
597
- ctx,
598
- [...path, key],
599
- `Ontology record "${ownerId}" ${key} references unknown ${expectedKind} ontology ID "${entry}"`
600
- )
601
- }
602
- }
603
-
604
- validateKnownOntologyReferences(ownerId, entry, [...path, key], seen)
605
- })
606
- }
607
-
608
- for (const { id, record } of listResolvedOntologyRecords(ontologyCompilation.ontology)) {
609
- validateKnownOntologyReferences(id, record, record.origin.path)
610
- }
611
-
612
- Object.values(model.policies).forEach((policy) => {
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)
670
- if (kind === 'goal') return goalsById.has(id)
671
- if (kind === 'customer-segment') return segmentsById.has(id)
672
- if (kind === 'offering') return offeringsById.has(id)
673
- if (kind === 'ontology') return ontologyIds.has(id)
674
- return false
675
- }
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
-
701
- Object.values(model.resources).forEach((resource) => {
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
- )
724
- }
725
- })
726
-
727
- function validateResourceOntologyBinding(
728
- resourceId: string,
729
- bindingKey: 'actions' | 'primaryAction' | 'reads' | 'writes' | 'usesCatalogs' | 'emits',
730
- expectedKind: OntologyKind,
731
- ids: string[] | string | undefined
732
- ): void {
733
- const ontologyIds = ids === undefined ? [] : Array.isArray(ids) ? ids : [ids]
734
-
735
- ontologyIds.forEach((ontologyId, ontologyIndex) => {
736
- if (ontologyIndexByKind[expectedKind][ontologyId] === undefined) {
737
- addIssue(
738
- ctx,
739
- ['resources', resourceId, 'ontology', bindingKey, ...(Array.isArray(ids) ? [ontologyIndex] : [])],
740
- `Resource "${resourceId}" ontology binding "${bindingKey}" references unknown ${expectedKind} ontology ID "${ontologyId}"`
741
- )
742
- }
743
- })
744
- }
745
-
746
- Object.values(model.resources).forEach((resource) => {
747
- const binding = resource.ontology
748
- if (binding === undefined) return
749
-
750
- validateResourceOntologyBinding(resource.id, 'actions', 'action', binding.actions)
751
- validateResourceOntologyBinding(resource.id, 'primaryAction', 'action', binding.primaryAction)
752
- validateResourceOntologyBinding(resource.id, 'reads', 'object', binding.reads)
753
- validateResourceOntologyBinding(resource.id, 'writes', 'object', binding.writes)
754
- validateResourceOntologyBinding(resource.id, 'usesCatalogs', 'catalog', binding.usesCatalogs)
755
- validateResourceOntologyBinding(resource.id, 'emits', 'event', binding.emits)
756
-
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
- }
774
- }
775
- }
776
- })
777
-
778
- Object.values(model.roles).forEach((role) => {
779
- if (role.heldBy === undefined) return
780
-
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
- })
815
- })
816
-
817
- for (const diagnostic of ontologyCompilation.diagnostics) {
818
- addIssue(ctx, diagnostic.path, diagnostic.message)
819
- }
820
- })
113
+ export const OrganizationModelSchema = OrganizationModelSchemaBase.superRefine(refineOrganizationModel)