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