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