@elevasis/core 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2330 -2391
- package/dist/index.js +2322 -1147
- package/dist/knowledge/index.d.ts +702 -1136
- package/dist/knowledge/index.js +9 -9
- package/dist/organization-model/index.d.ts +2330 -2391
- package/dist/organization-model/index.js +2322 -1147
- package/dist/test-utils/index.d.ts +703 -1106
- package/dist/test-utils/index.js +1735 -1089
- package/package.json +1 -1
- package/src/__tests__/template-core-compatibility.test.ts +11 -79
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +360 -98
- package/src/business/acquisition/api-schemas.test.ts +2 -2
- package/src/business/acquisition/api-schemas.ts +7 -9
- package/src/business/acquisition/build-templates.test.ts +4 -4
- package/src/business/acquisition/build-templates.ts +72 -30
- package/src/business/acquisition/crm-state-actions.test.ts +13 -11
- package/src/business/acquisition/types.ts +7 -3
- package/src/execution/engine/agent/core/types.ts +1 -1
- package/src/execution/engine/workflow/types.ts +2 -2
- package/src/knowledge/README.md +8 -7
- package/src/knowledge/__tests__/queries.test.ts +74 -73
- package/src/knowledge/format.ts +10 -9
- package/src/knowledge/index.ts +1 -1
- package/src/knowledge/published.ts +1 -1
- package/src/knowledge/queries.ts +26 -25
- package/src/organization-model/README.md +66 -26
- package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -0
- package/src/organization-model/__tests__/defaults.test.ts +72 -98
- package/src/organization-model/__tests__/domains/actions.test.ts +56 -0
- package/src/organization-model/__tests__/domains/customers.test.ts +299 -295
- package/src/organization-model/__tests__/domains/entities.test.ts +56 -0
- package/src/organization-model/__tests__/domains/goals.test.ts +493 -479
- package/src/organization-model/__tests__/domains/identity.test.ts +280 -279
- package/src/organization-model/__tests__/domains/navigation.test.ts +268 -212
- package/src/organization-model/__tests__/domains/offerings.test.ts +414 -419
- package/src/organization-model/__tests__/domains/policies.test.ts +323 -0
- package/src/organization-model/__tests__/domains/resource-mappings.test.ts +271 -271
- package/src/organization-model/__tests__/domains/resources.test.ts +159 -37
- package/src/organization-model/__tests__/domains/roles.test.ts +147 -86
- package/src/organization-model/__tests__/domains/statuses.test.ts +246 -243
- package/src/organization-model/__tests__/domains/systems.test.ts +67 -51
- package/src/organization-model/__tests__/flatten-additive-merge.test.ts +361 -0
- package/src/organization-model/__tests__/foundation.test.ts +74 -102
- package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -0
- package/src/organization-model/__tests__/graph.test.ts +899 -71
- package/src/organization-model/__tests__/knowledge.test.ts +173 -52
- package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -0
- package/src/organization-model/__tests__/migration-helpers.test.ts +591 -0
- package/src/organization-model/__tests__/prospecting-ssot.test.ts +36 -27
- package/src/organization-model/__tests__/recursive-system-schema.test.ts +520 -0
- package/src/organization-model/__tests__/resolve.test.ts +174 -23
- package/src/organization-model/__tests__/schema.test.ts +291 -114
- package/src/organization-model/__tests__/surface-projection.test.ts +207 -97
- package/src/organization-model/catalogs/lead-gen.ts +144 -0
- package/src/organization-model/content-kinds/config.ts +36 -0
- package/src/organization-model/content-kinds/index.ts +74 -0
- package/src/organization-model/content-kinds/pipeline.ts +68 -0
- package/src/organization-model/content-kinds/registry.ts +44 -0
- package/src/organization-model/content-kinds/status.ts +71 -0
- package/src/organization-model/content-kinds/template.ts +83 -0
- package/src/organization-model/content-kinds/types.ts +117 -0
- package/src/organization-model/contracts.ts +13 -3
- package/src/organization-model/defaults.ts +488 -96
- package/src/organization-model/domains/actions.ts +239 -0
- package/src/organization-model/domains/customers.ts +78 -75
- package/src/organization-model/domains/entities.ts +144 -0
- package/src/organization-model/domains/goals.ts +83 -80
- package/src/organization-model/domains/knowledge.ts +74 -16
- package/src/organization-model/domains/navigation.ts +107 -384
- package/src/organization-model/domains/offerings.ts +71 -66
- package/src/organization-model/domains/policies.ts +102 -0
- package/src/organization-model/domains/projects.ts +14 -48
- package/src/organization-model/domains/prospecting.ts +62 -181
- package/src/organization-model/domains/resources.ts +81 -24
- package/src/organization-model/domains/roles.ts +13 -10
- package/src/organization-model/domains/sales.ts +10 -219
- package/src/organization-model/domains/shared.ts +57 -57
- package/src/organization-model/domains/statuses.ts +339 -130
- package/src/organization-model/domains/systems.ts +186 -29
- package/src/organization-model/foundation.ts +54 -67
- package/src/organization-model/graph/build.ts +682 -54
- package/src/organization-model/graph/link.ts +1 -1
- package/src/organization-model/graph/schema.ts +24 -9
- package/src/organization-model/graph/types.ts +20 -7
- package/src/organization-model/helpers.ts +231 -26
- package/src/organization-model/index.ts +116 -5
- package/src/organization-model/migration-helpers.ts +249 -0
- package/src/organization-model/organization-graph.mdx +16 -15
- package/src/organization-model/organization-model.mdx +89 -41
- package/src/organization-model/published.ts +120 -18
- package/src/organization-model/resolve.ts +117 -54
- package/src/organization-model/schema.ts +561 -140
- package/src/organization-model/surface-projection.ts +116 -122
- package/src/organization-model/types.ts +102 -21
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/registry/__tests__/command-view.test.ts +6 -8
- package/src/platform/registry/__tests__/resource-link.test.ts +13 -8
- package/src/platform/registry/__tests__/resource-registry.integration.test.ts +16 -31
- package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -0
- package/src/platform/registry/__tests__/resource-registry.test.ts +9 -7
- package/src/platform/registry/__tests__/validation.test.ts +15 -11
- package/src/platform/registry/resource-registry.ts +20 -8
- package/src/platform/registry/serialization.ts +7 -7
- package/src/platform/registry/types.ts +3 -3
- package/src/platform/registry/validation.ts +17 -15
- package/src/reference/_generated/contracts.md +362 -99
- package/src/reference/glossary.md +18 -18
- package/src/supabase/database.types.ts +60 -0
- package/src/test-utils/test-utils.test.ts +1 -6
- package/src/organization-model/__tests__/domains/operations.test.ts +0 -203
- package/src/organization-model/domains/features.ts +0 -31
- package/src/organization-model/domains/operations.ts +0 -85
|
@@ -1,29 +1,91 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
+
import { lookupContentType } from './content-kinds/index'
|
|
2
3
|
import { OrganizationModelBrandingSchema } from './domains/branding'
|
|
3
|
-
import {
|
|
4
|
-
import { OrganizationModelProjectsSchema } from './domains/projects'
|
|
5
|
-
import { FeatureSchema } from './domains/features'
|
|
6
|
-
import { OrganizationModelProspectingSchema } from './domains/prospecting'
|
|
7
|
-
import { OrganizationModelNavigationSchema } from './domains/navigation'
|
|
4
|
+
import { OrganizationModelNavigationSchema, type SidebarNode } from './domains/navigation'
|
|
8
5
|
import { IdentityDomainSchema, DEFAULT_ORGANIZATION_MODEL_IDENTITY } from './domains/identity'
|
|
9
6
|
import { CustomersDomainSchema, DEFAULT_ORGANIZATION_MODEL_CUSTOMERS } from './domains/customers'
|
|
10
7
|
import { OfferingsDomainSchema, DEFAULT_ORGANIZATION_MODEL_OFFERINGS } from './domains/offerings'
|
|
11
8
|
import { RolesDomainSchema, DEFAULT_ORGANIZATION_MODEL_ROLES } from './domains/roles'
|
|
12
9
|
import { GoalsDomainSchema, DEFAULT_ORGANIZATION_MODEL_GOALS } from './domains/goals'
|
|
13
|
-
import { OperationsDomainSchema } from './domains/operations'
|
|
14
|
-
import { StatusesDomainSchema } from './domains/statuses'
|
|
15
10
|
import { KnowledgeDomainSchema } from './domains/knowledge'
|
|
16
|
-
import { SystemsDomainSchema, DEFAULT_ORGANIZATION_MODEL_SYSTEMS } from './domains/systems'
|
|
11
|
+
import { SystemsDomainSchema, DEFAULT_ORGANIZATION_MODEL_SYSTEMS, type SystemEntry } from './domains/systems'
|
|
17
12
|
import { ResourcesDomainSchema, DEFAULT_ORGANIZATION_MODEL_RESOURCES } from './domains/resources'
|
|
13
|
+
import { ActionsDomainSchema, DEFAULT_ORGANIZATION_MODEL_ACTIONS } from './domains/actions'
|
|
14
|
+
import { EntitiesDomainSchema, DEFAULT_ORGANIZATION_MODEL_ENTITIES } from './domains/entities'
|
|
15
|
+
import { PoliciesDomainSchema, DEFAULT_ORGANIZATION_MODEL_POLICIES } from './domains/policies'
|
|
16
|
+
|
|
17
|
+
// Phase 4 cut: 'sales', 'prospecting', 'projects', 'statuses' removed.
|
|
18
|
+
// domainMetadata.knowledge covers versioning for the knowledge flat-map (D7).
|
|
19
|
+
export const OrganizationModelDomainKeySchema = z.enum([
|
|
20
|
+
'branding',
|
|
21
|
+
'identity',
|
|
22
|
+
'customers',
|
|
23
|
+
'offerings',
|
|
24
|
+
'roles',
|
|
25
|
+
'goals',
|
|
26
|
+
'systems',
|
|
27
|
+
'resources',
|
|
28
|
+
'actions',
|
|
29
|
+
'entities',
|
|
30
|
+
'policies',
|
|
31
|
+
'knowledge'
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
export const OrganizationModelDomainMetadataSchema = z.object({
|
|
35
|
+
version: z.literal(1).default(1),
|
|
36
|
+
lastModified: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'lastModified must be an ISO date string (YYYY-MM-DD)')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
export const DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA: Record<
|
|
40
|
+
z.infer<typeof OrganizationModelDomainKeySchema>,
|
|
41
|
+
z.infer<typeof OrganizationModelDomainMetadataSchema>
|
|
42
|
+
> = {
|
|
43
|
+
branding: { version: 1, lastModified: '2026-05-10' },
|
|
44
|
+
identity: { version: 1, lastModified: '2026-05-10' },
|
|
45
|
+
customers: { version: 1, lastModified: '2026-05-10' },
|
|
46
|
+
offerings: { version: 1, lastModified: '2026-05-10' },
|
|
47
|
+
roles: { version: 1, lastModified: '2026-05-10' },
|
|
48
|
+
goals: { version: 1, lastModified: '2026-05-10' },
|
|
49
|
+
systems: { version: 1, lastModified: '2026-05-10' },
|
|
50
|
+
resources: { version: 1, lastModified: '2026-05-10' },
|
|
51
|
+
actions: { version: 1, lastModified: '2026-05-10' },
|
|
52
|
+
entities: { version: 1, lastModified: '2026-05-10' },
|
|
53
|
+
policies: { version: 1, lastModified: '2026-05-10' },
|
|
54
|
+
knowledge: { version: 1, lastModified: '2026-05-10' }
|
|
55
|
+
}
|
|
18
56
|
|
|
57
|
+
export const OrganizationModelDomainMetadataByDomainSchema = z
|
|
58
|
+
.object({
|
|
59
|
+
branding: OrganizationModelDomainMetadataSchema,
|
|
60
|
+
identity: OrganizationModelDomainMetadataSchema,
|
|
61
|
+
customers: OrganizationModelDomainMetadataSchema,
|
|
62
|
+
offerings: OrganizationModelDomainMetadataSchema,
|
|
63
|
+
roles: OrganizationModelDomainMetadataSchema,
|
|
64
|
+
goals: OrganizationModelDomainMetadataSchema,
|
|
65
|
+
systems: OrganizationModelDomainMetadataSchema,
|
|
66
|
+
resources: OrganizationModelDomainMetadataSchema,
|
|
67
|
+
actions: OrganizationModelDomainMetadataSchema,
|
|
68
|
+
entities: OrganizationModelDomainMetadataSchema,
|
|
69
|
+
policies: OrganizationModelDomainMetadataSchema,
|
|
70
|
+
knowledge: OrganizationModelDomainMetadataSchema
|
|
71
|
+
})
|
|
72
|
+
.partial()
|
|
73
|
+
.default(DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA)
|
|
74
|
+
.transform((metadata) => ({ ...DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA, ...metadata }))
|
|
75
|
+
|
|
76
|
+
// Phase 4 schema cut (D8):
|
|
77
|
+
// REMOVED top-level fields: sales, prospecting, projects, statuses
|
|
78
|
+
// ADDED navigation.sidebar as the authored shell tree
|
|
79
|
+
// knowledge: now Record<id, OrgKnowledgeNode> flat map (D3); version/lastModified in domainMetadata.knowledge (D7)
|
|
80
|
+
//
|
|
81
|
+
// Clean migration rule:
|
|
82
|
+
// NO top-level surfaces / navigationGroups compatibility fields.
|
|
83
|
+
// Surfaces are derived from navigation.sidebar routeable leaves.
|
|
19
84
|
const OrganizationModelSchemaBase = z.object({
|
|
20
85
|
version: z.literal(1).default(1),
|
|
21
|
-
|
|
86
|
+
domainMetadata: OrganizationModelDomainMetadataByDomainSchema,
|
|
22
87
|
branding: OrganizationModelBrandingSchema,
|
|
23
|
-
navigation: OrganizationModelNavigationSchema
|
|
24
|
-
sales: OrganizationModelSalesSchema,
|
|
25
|
-
prospecting: OrganizationModelProspectingSchema,
|
|
26
|
-
projects: OrganizationModelProjectsSchema,
|
|
88
|
+
navigation: OrganizationModelNavigationSchema,
|
|
27
89
|
identity: IdentityDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_IDENTITY),
|
|
28
90
|
customers: CustomersDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_CUSTOMERS),
|
|
29
91
|
offerings: OfferingsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_OFFERINGS),
|
|
@@ -31,9 +93,11 @@ const OrganizationModelSchemaBase = z.object({
|
|
|
31
93
|
goals: GoalsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_GOALS),
|
|
32
94
|
systems: SystemsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_SYSTEMS),
|
|
33
95
|
resources: ResourcesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_RESOURCES),
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
96
|
+
actions: ActionsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_ACTIONS),
|
|
97
|
+
entities: EntitiesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_ENTITIES),
|
|
98
|
+
policies: PoliciesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_POLICIES),
|
|
99
|
+
// D3: flat Record<id, OrgKnowledgeNode> — no wrapper object
|
|
100
|
+
knowledge: KnowledgeDomainSchema.default({})
|
|
37
101
|
})
|
|
38
102
|
|
|
39
103
|
function addIssue(ctx: z.RefinementCtx, path: Array<string | number>, message: string): void {
|
|
@@ -44,243 +108,493 @@ function addIssue(ctx: z.RefinementCtx, path: Array<string | number>, message: s
|
|
|
44
108
|
})
|
|
45
109
|
}
|
|
46
110
|
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
collectionPath: Array<string | number>,
|
|
51
|
-
label: string
|
|
52
|
-
): Map<string, T> {
|
|
53
|
-
const itemsById = new Map<string, T>()
|
|
54
|
-
|
|
55
|
-
items.forEach((item, index) => {
|
|
56
|
-
if (itemsById.has(item.id)) {
|
|
57
|
-
addIssue(ctx, [...collectionPath, index, 'id'], `${label} id "${item.id}" must be unique`)
|
|
58
|
-
return
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
itemsById.set(item.id, item)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
return itemsById
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const LEGACY_FEATURE_ALIASES = new Map<string, string>([
|
|
68
|
-
['crm', 'sales.crm'],
|
|
69
|
-
['lead-gen', 'sales.lead-gen'],
|
|
70
|
-
['submitted-requests', 'monitoring.submitted-requests']
|
|
71
|
-
])
|
|
72
|
-
|
|
73
|
-
function hasFeature(featuresById: Map<string, unknown>, featureId: string): boolean {
|
|
74
|
-
return featuresById.has(featureId) || featuresById.has(LEGACY_FEATURE_ALIASES.get(featureId) ?? '')
|
|
111
|
+
function isLifecycleEnabled(lifecycle: string | undefined, enabled: boolean | undefined): boolean {
|
|
112
|
+
if (enabled === false) return false
|
|
113
|
+
return lifecycle !== 'deprecated' && lifecycle !== 'archived'
|
|
75
114
|
}
|
|
76
115
|
|
|
77
|
-
function
|
|
116
|
+
function defaultSystemPathFor(id: string): string {
|
|
78
117
|
return `/${id.replaceAll('.', '/')}`
|
|
79
118
|
}
|
|
80
119
|
|
|
81
120
|
function asRoleHolderArray(
|
|
82
|
-
heldBy: NonNullable<z.infer<typeof OrganizationModelSchemaBase>['roles'][
|
|
121
|
+
heldBy: NonNullable<z.infer<typeof OrganizationModelSchemaBase>['roles'][string]['heldBy']>
|
|
83
122
|
) {
|
|
84
123
|
return Array.isArray(heldBy) ? heldBy : [heldBy]
|
|
85
124
|
}
|
|
86
125
|
|
|
126
|
+
function isKnowledgeKindCompatibleWithTarget(knowledgeKind: string, targetKind: string): boolean {
|
|
127
|
+
if (knowledgeKind === 'reference') return true
|
|
128
|
+
if (knowledgeKind === 'playbook') {
|
|
129
|
+
return ['system', 'resource', 'stage', 'action'].includes(targetKind)
|
|
130
|
+
}
|
|
131
|
+
if (knowledgeKind === 'strategy') {
|
|
132
|
+
return ['system', 'goal', 'offering', 'customer-segment'].includes(targetKind)
|
|
133
|
+
}
|
|
134
|
+
return false
|
|
135
|
+
}
|
|
136
|
+
|
|
87
137
|
export const OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((model, ctx) => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
138
|
+
// Collect ALL system entries recursively — top-level systems plus any nested subsystems.
|
|
139
|
+
// Wave 2 canonical OM authors nested subsystems (e.g. sys → subsystems → 'lead-gen' with id
|
|
140
|
+
// 'sys.lead-gen'). Resource systemPath cross-refs must resolve against the full flattened set.
|
|
141
|
+
type SystemWithPath = { path: string; schemaPath: Array<string | number>; system: SystemEntry }
|
|
142
|
+
|
|
143
|
+
function collectAllSystems(
|
|
144
|
+
systems: Record<string, SystemEntry>,
|
|
145
|
+
prefix = '',
|
|
146
|
+
schemaPath: Array<string | number> = ['systems']
|
|
147
|
+
): SystemWithPath[] {
|
|
148
|
+
const result: SystemWithPath[] = []
|
|
149
|
+
for (const [key, system] of Object.entries(systems)) {
|
|
150
|
+
const path = prefix ? `${prefix}.${key}` : key
|
|
151
|
+
const currentSchemaPath = [...schemaPath, key]
|
|
152
|
+
result.push({ path, schemaPath: currentSchemaPath, system })
|
|
153
|
+
if (system.subsystems !== undefined) {
|
|
154
|
+
result.push(...collectAllSystems(system.subsystems, path, [...currentSchemaPath, 'subsystems']))
|
|
101
155
|
}
|
|
102
156
|
}
|
|
157
|
+
return result
|
|
158
|
+
}
|
|
103
159
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
|
|
177
|
+
const hasChildren =
|
|
178
|
+
Object.keys(system.subsystems ?? {}).length > 0 ||
|
|
179
|
+
allSystems.some((candidate) => candidate.path.startsWith(`${path}.`) && !candidate.path.slice(path.length + 1).includes('.'))
|
|
180
|
+
const contributesRoutePath = system.ui?.path !== undefined || system.path !== undefined || !hasChildren
|
|
108
181
|
if (contributesRoutePath) {
|
|
109
|
-
const effectivePath =
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
182
|
+
const effectivePath = system.ui?.path ?? system.path ?? defaultSystemPathFor(path)
|
|
183
|
+
const existingSystemId = systemIdsByEffectivePath.get(effectivePath)
|
|
184
|
+
if (existingSystemId !== undefined) {
|
|
112
185
|
addIssue(
|
|
113
186
|
ctx,
|
|
114
|
-
[
|
|
115
|
-
`
|
|
187
|
+
[...schemaPath, system.ui?.path !== undefined ? 'ui' : 'path'],
|
|
188
|
+
`System "${path}" effective path "${effectivePath}" duplicates system "${existingSystemId}"`
|
|
116
189
|
)
|
|
117
190
|
} else {
|
|
118
|
-
|
|
191
|
+
systemIdsByEffectivePath.set(effectivePath, path)
|
|
119
192
|
}
|
|
120
193
|
}
|
|
121
194
|
|
|
122
|
-
if (hasChildren &&
|
|
123
|
-
const hasEnabledDescendant =
|
|
124
|
-
(
|
|
125
|
-
|
|
195
|
+
if (hasChildren && isLifecycleEnabled(system.lifecycle, system.enabled)) {
|
|
196
|
+
const hasEnabledDescendant =
|
|
197
|
+
Object.values(system.subsystems ?? {}).some((candidate) =>
|
|
198
|
+
isLifecycleEnabled(candidate.lifecycle, candidate.enabled)
|
|
199
|
+
) ||
|
|
200
|
+
allSystems.some(
|
|
201
|
+
(candidate) =>
|
|
202
|
+
candidate.path.startsWith(`${path}.`) &&
|
|
203
|
+
!candidate.path.slice(path.length + 1).includes('.') &&
|
|
204
|
+
isLifecycleEnabled(candidate.system.lifecycle, candidate.system.enabled)
|
|
205
|
+
)
|
|
126
206
|
if (!hasEnabledDescendant) {
|
|
127
207
|
addIssue(
|
|
128
208
|
ctx,
|
|
129
|
-
[
|
|
130
|
-
`
|
|
209
|
+
[...schemaPath, 'lifecycle'],
|
|
210
|
+
`System "${path}" is active but has no active descendants`
|
|
131
211
|
)
|
|
132
212
|
}
|
|
133
213
|
}
|
|
134
214
|
})
|
|
135
215
|
|
|
136
|
-
|
|
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
|
+
}
|
|
137
225
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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>
|
|
144
235
|
}
|
|
145
236
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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)
|
|
154
256
|
}
|
|
155
|
-
})
|
|
156
|
-
})
|
|
157
257
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
['navigation', 'surfaces', surfaceIndex, 'featureId'],
|
|
163
|
-
`Navigation surface "${surface.id}" references unknown feature "${surface.featureId}"`
|
|
164
|
-
)
|
|
165
|
-
}
|
|
258
|
+
if (node.type === 'group') {
|
|
259
|
+
collectSidebarNodes(node.children, [...nodePath, 'children'])
|
|
260
|
+
return
|
|
261
|
+
}
|
|
166
262
|
|
|
167
|
-
|
|
168
|
-
|
|
263
|
+
sidebarSurfaces.push({ id: nodeId, node, path: nodePath })
|
|
264
|
+
const normalizedPath = normalizeRoutePath(node.path)
|
|
265
|
+
const existingSurfaceId = sidebarSurfacePaths.get(normalizedPath)
|
|
266
|
+
if (existingSurfaceId !== undefined) {
|
|
169
267
|
addIssue(
|
|
170
268
|
ctx,
|
|
171
|
-
[
|
|
172
|
-
`
|
|
269
|
+
[...nodePath, 'path'],
|
|
270
|
+
`Sidebar surface path "${node.path}" duplicates surface "${existingSurfaceId}"`
|
|
173
271
|
)
|
|
272
|
+
} else {
|
|
273
|
+
sidebarSurfacePaths.set(normalizedPath, nodeId)
|
|
174
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
|
+
})
|
|
175
285
|
})
|
|
176
|
-
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
collectSidebarNodes(model.navigation.sidebar.primary, ['navigation', 'sidebar', 'primary'])
|
|
289
|
+
collectSidebarNodes(model.navigation.sidebar.bottom, ['navigation', 'sidebar', 'bottom'])
|
|
177
290
|
|
|
178
291
|
// Offerings -> CustomerSegment cross-ref: targetSegmentIds must resolve
|
|
179
|
-
const segmentsById = new Map(model.customers
|
|
180
|
-
model.offerings.
|
|
292
|
+
const segmentsById = new Map(Object.entries(model.customers))
|
|
293
|
+
Object.values(model.offerings).forEach((product) => {
|
|
181
294
|
product.targetSegmentIds.forEach((segmentId, segmentIndex) => {
|
|
182
295
|
if (!segmentsById.has(segmentId)) {
|
|
183
296
|
addIssue(
|
|
184
297
|
ctx,
|
|
185
|
-
['offerings',
|
|
298
|
+
['offerings', product.id, 'targetSegmentIds', segmentIndex],
|
|
186
299
|
`Product "${product.id}" references unknown customer segment "${segmentId}"`
|
|
187
300
|
)
|
|
188
301
|
}
|
|
189
302
|
})
|
|
190
303
|
|
|
191
|
-
// Offerings ->
|
|
192
|
-
if (product.deliveryFeatureId !== undefined && !
|
|
304
|
+
// Offerings -> System cross-ref: deliveryFeatureId must resolve (when present)
|
|
305
|
+
if (product.deliveryFeatureId !== undefined && !systemsById.has(product.deliveryFeatureId)) {
|
|
193
306
|
addIssue(
|
|
194
307
|
ctx,
|
|
195
|
-
['offerings',
|
|
196
|
-
`Product "${product.id}" references unknown delivery
|
|
308
|
+
['offerings', product.id, 'deliveryFeatureId'],
|
|
309
|
+
`Product "${product.id}" references unknown delivery system "${product.deliveryFeatureId}"`
|
|
197
310
|
)
|
|
198
311
|
}
|
|
199
312
|
})
|
|
200
313
|
|
|
201
314
|
// Goals -> period-range validation: periodEnd must be strictly after periodStart
|
|
202
|
-
model.goals.
|
|
315
|
+
Object.values(model.goals).forEach((objective) => {
|
|
203
316
|
if (objective.periodEnd <= objective.periodStart) {
|
|
204
317
|
addIssue(
|
|
205
318
|
ctx,
|
|
206
|
-
['goals',
|
|
319
|
+
['goals', objective.id, 'periodEnd'],
|
|
207
320
|
`Goal "${objective.id}" has periodEnd "${objective.periodEnd}" which must be strictly after periodStart "${objective.periodStart}"`
|
|
208
321
|
)
|
|
209
322
|
}
|
|
210
323
|
})
|
|
211
324
|
|
|
212
|
-
const goalsById = new Map(model.goals
|
|
213
|
-
|
|
325
|
+
const goalsById = new Map(Object.entries(model.goals))
|
|
326
|
+
// Phase 4: knowledge is now a flat Record<id, OrgKnowledgeNode> — no .nodes array
|
|
327
|
+
const knowledgeById = new Map(Object.entries(model.knowledge))
|
|
328
|
+
const actionsById = new Map(Object.entries(model.actions))
|
|
329
|
+
const entitiesById = new Map(Object.entries(model.entities))
|
|
330
|
+
const policiesById = new Map(Object.entries(model.policies))
|
|
331
|
+
|
|
332
|
+
sidebarSurfaces.forEach(({ id, node, path }) => {
|
|
333
|
+
node.targets?.entities?.forEach((entityId, entityIndex) => {
|
|
334
|
+
if (!entitiesById.has(entityId)) {
|
|
335
|
+
addIssue(
|
|
336
|
+
ctx,
|
|
337
|
+
[...path, 'targets', 'entities', entityIndex],
|
|
338
|
+
`Sidebar surface "${id}" references unknown entity "${entityId}"`
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
node.targets?.actions?.forEach((actionId, actionIndex) => {
|
|
344
|
+
if (!actionsById.has(actionId)) {
|
|
345
|
+
addIssue(
|
|
346
|
+
ctx,
|
|
347
|
+
[...path, 'targets', 'actions', actionIndex],
|
|
348
|
+
`Sidebar surface "${id}" references unknown action "${actionId}"`
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
Object.values(model.entities).forEach((entity) => {
|
|
355
|
+
if (!systemsById.has(entity.ownedBySystemId)) {
|
|
356
|
+
addIssue(
|
|
357
|
+
ctx,
|
|
358
|
+
['entities', entity.id, 'ownedBySystemId'],
|
|
359
|
+
`Entity "${entity.id}" references unknown ownedBySystemId "${entity.ownedBySystemId}"`
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
entity.links?.forEach((link, linkIndex) => {
|
|
364
|
+
if (!entitiesById.has(link.toEntity)) {
|
|
365
|
+
addIssue(
|
|
366
|
+
ctx,
|
|
367
|
+
['entities', entity.id, 'links', linkIndex, 'toEntity'],
|
|
368
|
+
`Entity "${entity.id}" links to unknown entity "${link.toEntity}"`
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
})
|
|
214
373
|
|
|
215
374
|
// Roles -> reportsToId cross-ref: each reportsToId must resolve to another role in the same collection
|
|
216
|
-
const rolesById = new Map(model.roles
|
|
217
|
-
model.roles.
|
|
375
|
+
const rolesById = new Map(Object.entries(model.roles))
|
|
376
|
+
Object.values(model.roles).forEach((role) => {
|
|
218
377
|
if (role.reportsToId !== undefined && !rolesById.has(role.reportsToId)) {
|
|
219
378
|
addIssue(
|
|
220
379
|
ctx,
|
|
221
|
-
['roles',
|
|
380
|
+
['roles', role.id, 'reportsToId'],
|
|
222
381
|
`Role "${role.id}" references unknown reportsToId "${role.reportsToId}"`
|
|
223
382
|
)
|
|
224
383
|
}
|
|
225
384
|
})
|
|
226
385
|
|
|
227
|
-
|
|
228
|
-
|
|
386
|
+
Object.values(model.roles).forEach((role) => {
|
|
387
|
+
const visited = new Set<string>()
|
|
388
|
+
let currentReportsToId = role.reportsToId
|
|
389
|
+
|
|
390
|
+
while (currentReportsToId !== undefined) {
|
|
391
|
+
if (currentReportsToId === role.id || visited.has(currentReportsToId)) {
|
|
392
|
+
addIssue(ctx, ['roles', role.id, 'reportsToId'], `Role "${role.id}" has a reportsToId cycle`)
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
visited.add(currentReportsToId)
|
|
397
|
+
currentReportsToId = rolesById.get(currentReportsToId)?.reportsToId
|
|
398
|
+
}
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
Object.values(model.roles).forEach((role) => {
|
|
229
402
|
role.responsibleFor?.forEach((systemId, systemIndex) => {
|
|
230
403
|
if (!systemsById.has(systemId)) {
|
|
231
404
|
addIssue(
|
|
232
405
|
ctx,
|
|
233
|
-
['roles',
|
|
406
|
+
['roles', role.id, 'responsibleFor', systemIndex],
|
|
234
407
|
`Role "${role.id}" references unknown responsibleFor system "${systemId}"`
|
|
235
408
|
)
|
|
236
409
|
}
|
|
237
410
|
})
|
|
238
411
|
})
|
|
239
412
|
|
|
240
|
-
|
|
413
|
+
allSystems.forEach(({ schemaPath, system }) => {
|
|
241
414
|
if (system.responsibleRoleId !== undefined && !rolesById.has(system.responsibleRoleId)) {
|
|
242
415
|
addIssue(
|
|
243
416
|
ctx,
|
|
244
|
-
[
|
|
417
|
+
[...schemaPath, 'responsibleRoleId'],
|
|
245
418
|
`System "${system.id}" references unknown responsibleRoleId "${system.responsibleRoleId}"`
|
|
246
419
|
)
|
|
247
420
|
}
|
|
248
421
|
|
|
249
|
-
system.governedByKnowledge
|
|
422
|
+
system.governedByKnowledge?.forEach((nodeId, nodeIndex) => {
|
|
250
423
|
if (!knowledgeById.has(nodeId)) {
|
|
251
424
|
addIssue(
|
|
252
425
|
ctx,
|
|
253
|
-
[
|
|
426
|
+
[...schemaPath, 'governedByKnowledge', nodeIndex],
|
|
254
427
|
`System "${system.id}" references unknown knowledge node "${nodeId}"`
|
|
255
428
|
)
|
|
256
429
|
}
|
|
257
430
|
})
|
|
258
431
|
|
|
259
|
-
system.drivesGoals
|
|
432
|
+
system.drivesGoals?.forEach((goalId, goalIndex) => {
|
|
260
433
|
if (!goalsById.has(goalId)) {
|
|
261
434
|
addIssue(
|
|
262
435
|
ctx,
|
|
263
|
-
[
|
|
436
|
+
[...schemaPath, 'drivesGoals', goalIndex],
|
|
264
437
|
`System "${system.id}" references unknown goal "${goalId}"`
|
|
265
438
|
)
|
|
266
439
|
}
|
|
267
440
|
})
|
|
441
|
+
|
|
442
|
+
system.actions?.forEach((actionRef, actionIndex) => {
|
|
443
|
+
if (!actionsById.has(actionRef.actionId)) {
|
|
444
|
+
addIssue(
|
|
445
|
+
ctx,
|
|
446
|
+
[...schemaPath, 'actions', actionIndex, 'actionId'],
|
|
447
|
+
`System "${system.id}" references unknown action "${actionRef.actionId}"`
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
system.policies?.forEach((policyId, policyIndex) => {
|
|
453
|
+
if (!policiesById.has(policyId)) {
|
|
454
|
+
addIssue(
|
|
455
|
+
ctx,
|
|
456
|
+
[...schemaPath, 'policies', policyIndex],
|
|
457
|
+
`System "${system.id}" references unknown policy "${policyId}"`
|
|
458
|
+
)
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
Object.values(model.actions).forEach((action) => {
|
|
464
|
+
action.affects?.forEach((entityId, entityIndex) => {
|
|
465
|
+
if (!entitiesById.has(entityId)) {
|
|
466
|
+
addIssue(
|
|
467
|
+
ctx,
|
|
468
|
+
['actions', action.id, 'affects', entityIndex],
|
|
469
|
+
`Action "${action.id}" affects unknown entity "${entityId}"`
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
// Phase 4: sales / prospecting / projects compound-domain entity cross-ref checks removed.
|
|
476
|
+
// Those entity bindings now live in system.content (Wave 2 canonicalOrganizationModel).
|
|
477
|
+
|
|
478
|
+
const resourcesById = new Map(Object.entries(model.resources))
|
|
479
|
+
sidebarSurfaces.forEach(({ id, node, path }) => {
|
|
480
|
+
node.targets?.resources?.forEach((resourceId, resourceIndex) => {
|
|
481
|
+
if (!resourcesById.has(resourceId)) {
|
|
482
|
+
addIssue(
|
|
483
|
+
ctx,
|
|
484
|
+
[...path, 'targets', 'resources', resourceIndex],
|
|
485
|
+
`Sidebar surface "${id}" references unknown resource "${resourceId}"`
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
// Phase 4: stageIds previously sourced from model.prospecting.*Stages; stages now live in
|
|
491
|
+
// system.content as schema:stage nodes. knowledge 'stage' target validation is kept permissive
|
|
492
|
+
// (always false) — Wave 2 will wire a content-based stage lookup when canonical OM lands.
|
|
493
|
+
const stageIds = new Set<string>()
|
|
494
|
+
const actionIds = new Set(Object.keys(model.actions))
|
|
495
|
+
const offeringsById = new Map(Object.entries(model.offerings))
|
|
496
|
+
|
|
497
|
+
Object.values(model.policies).forEach((policy) => {
|
|
498
|
+
policy.appliesTo.systemIds.forEach((systemId, systemIndex) => {
|
|
499
|
+
if (!systemsById.has(systemId)) {
|
|
500
|
+
addIssue(
|
|
501
|
+
ctx,
|
|
502
|
+
['policies', policy.id, 'appliesTo', 'systemIds', systemIndex],
|
|
503
|
+
`Policy "${policy.id}" applies to unknown system "${systemId}"`
|
|
504
|
+
)
|
|
505
|
+
}
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
policy.appliesTo.actionIds.forEach((actionId, actionIndex) => {
|
|
509
|
+
if (!actionsById.has(actionId)) {
|
|
510
|
+
addIssue(
|
|
511
|
+
ctx,
|
|
512
|
+
['policies', policy.id, 'appliesTo', 'actionIds', actionIndex],
|
|
513
|
+
`Policy "${policy.id}" applies to unknown action "${actionId}"`
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
policy.actions.forEach((action, actionIndex) => {
|
|
519
|
+
if (action.kind === 'invoke-action' && !actionsById.has(action.actionId)) {
|
|
520
|
+
addIssue(
|
|
521
|
+
ctx,
|
|
522
|
+
['policies', policy.id, 'actions', actionIndex, 'actionId'],
|
|
523
|
+
`Policy "${policy.id}" invokes unknown action "${action.actionId}"`
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
if (
|
|
527
|
+
(action.kind === 'notify-role' || action.kind === 'require-approval') &&
|
|
528
|
+
action.roleId !== undefined &&
|
|
529
|
+
!rolesById.has(action.roleId)
|
|
530
|
+
) {
|
|
531
|
+
addIssue(
|
|
532
|
+
ctx,
|
|
533
|
+
['policies', policy.id, 'actions', actionIndex, 'roleId'],
|
|
534
|
+
`Policy "${policy.id}" references unknown role "${action.roleId}"`
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
if (policy.trigger.kind === 'action-invocation' && !actionsById.has(policy.trigger.actionId)) {
|
|
540
|
+
addIssue(
|
|
541
|
+
ctx,
|
|
542
|
+
['policies', policy.id, 'trigger', 'actionId'],
|
|
543
|
+
`Policy "${policy.id}" references unknown trigger action "${policy.trigger.actionId}"`
|
|
544
|
+
)
|
|
545
|
+
}
|
|
268
546
|
})
|
|
269
547
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (
|
|
548
|
+
function knowledgeTargetExists(kind: string, id: string): boolean {
|
|
549
|
+
if (kind === 'system') return systemsById.has(id)
|
|
550
|
+
if (kind === 'resource') return resourcesById.has(id)
|
|
551
|
+
if (kind === 'knowledge') return knowledgeById.has(id)
|
|
552
|
+
if (kind === 'stage') return stageIds.has(id)
|
|
553
|
+
if (kind === 'action') return actionIds.has(id)
|
|
554
|
+
if (kind === 'role') return rolesById.has(id)
|
|
555
|
+
if (kind === 'goal') return goalsById.has(id)
|
|
556
|
+
if (kind === 'customer-segment') return segmentsById.has(id)
|
|
557
|
+
if (kind === 'offering') return offeringsById.has(id)
|
|
558
|
+
return false
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Phase 4: model.knowledge is now Record<id, OrgKnowledgeNode> — iterate Object.values
|
|
562
|
+
Object.entries(model.knowledge).forEach(([nodeId, node]) => {
|
|
563
|
+
node.links.forEach((link, linkIndex) => {
|
|
564
|
+
if (!knowledgeTargetExists(link.target.kind, link.target.id)) {
|
|
565
|
+
addIssue(
|
|
566
|
+
ctx,
|
|
567
|
+
['knowledge', nodeId, 'links', linkIndex, 'target'],
|
|
568
|
+
`Knowledge node "${node.id}" references unknown ${link.target.kind} target "${link.target.id}"`
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (!isKnowledgeKindCompatibleWithTarget(node.kind, link.target.kind)) {
|
|
573
|
+
addIssue(
|
|
574
|
+
ctx,
|
|
575
|
+
['knowledge', nodeId, 'links', linkIndex, 'target', 'kind'],
|
|
576
|
+
`Knowledge node "${node.id}" kind "${node.kind}" cannot govern ${link.target.kind} targets`
|
|
577
|
+
)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// `governedByKnowledge` is validated one-way on target nodes above. Knowledge
|
|
581
|
+
// links may be authored first and remain valid as forward references.
|
|
582
|
+
})
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
Object.values(model.resources).forEach((resource) => {
|
|
586
|
+
if (!systemsById.has(resource.systemPath)) {
|
|
273
587
|
addIssue(
|
|
274
588
|
ctx,
|
|
275
|
-
['resources',
|
|
276
|
-
`Resource "${resource.id}" references unknown
|
|
589
|
+
['resources', resource.id, 'systemPath'],
|
|
590
|
+
`Resource "${resource.id}" references unknown system path "${resource.systemPath}"`
|
|
277
591
|
)
|
|
278
592
|
}
|
|
279
593
|
|
|
280
594
|
if (resource.ownerRoleId !== undefined && !rolesById.has(resource.ownerRoleId)) {
|
|
281
595
|
addIssue(
|
|
282
596
|
ctx,
|
|
283
|
-
['resources',
|
|
597
|
+
['resources', resource.id, 'ownerRoleId'],
|
|
284
598
|
`Resource "${resource.id}" references unknown ownerRoleId "${resource.ownerRoleId}"`
|
|
285
599
|
)
|
|
286
600
|
}
|
|
@@ -288,13 +602,13 @@ export const OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((
|
|
|
288
602
|
if (resource.kind === 'agent' && resource.actsAsRoleId !== undefined && !rolesById.has(resource.actsAsRoleId)) {
|
|
289
603
|
addIssue(
|
|
290
604
|
ctx,
|
|
291
|
-
['resources',
|
|
605
|
+
['resources', resource.id, 'actsAsRoleId'],
|
|
292
606
|
`Agent resource "${resource.id}" references unknown actsAsRoleId "${resource.actsAsRoleId}"`
|
|
293
607
|
)
|
|
294
608
|
}
|
|
295
609
|
})
|
|
296
610
|
|
|
297
|
-
model.roles.
|
|
611
|
+
Object.values(model.roles).forEach((role) => {
|
|
298
612
|
if (role.heldBy === undefined) return
|
|
299
613
|
|
|
300
614
|
asRoleHolderArray(role.heldBy).forEach((holder, holderIndex) => {
|
|
@@ -304,7 +618,7 @@ export const OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((
|
|
|
304
618
|
if (resource === undefined) {
|
|
305
619
|
addIssue(
|
|
306
620
|
ctx,
|
|
307
|
-
['roles',
|
|
621
|
+
['roles', role.id, 'heldBy', Array.isArray(role.heldBy) ? holderIndex : 'agentId'],
|
|
308
622
|
`Role "${role.id}" references unknown agent holder resource "${holder.agentId}"`
|
|
309
623
|
)
|
|
310
624
|
return
|
|
@@ -313,22 +627,129 @@ export const OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((
|
|
|
313
627
|
if (resource.kind !== 'agent') {
|
|
314
628
|
addIssue(
|
|
315
629
|
ctx,
|
|
316
|
-
['roles',
|
|
630
|
+
['roles', role.id, 'heldBy', Array.isArray(role.heldBy) ? holderIndex : 'agentId'],
|
|
317
631
|
`Role "${role.id}" agent holder "${holder.agentId}" must reference an agent resource`
|
|
318
632
|
)
|
|
319
633
|
}
|
|
320
634
|
})
|
|
321
635
|
})
|
|
322
636
|
|
|
323
|
-
model.knowledge
|
|
637
|
+
// Phase 4: model.knowledge is now Record<id, OrgKnowledgeNode>
|
|
638
|
+
Object.entries(model.knowledge).forEach(([nodeId, node]) => {
|
|
324
639
|
node.ownerIds.forEach((roleId, ownerIndex) => {
|
|
325
640
|
if (!rolesById.has(roleId)) {
|
|
326
641
|
addIssue(
|
|
327
642
|
ctx,
|
|
328
|
-
['knowledge',
|
|
643
|
+
['knowledge', nodeId, 'ownerIds', ownerIndex],
|
|
329
644
|
`Knowledge node "${node.id}" references unknown owner role "${roleId}"`
|
|
330
645
|
)
|
|
331
646
|
}
|
|
332
647
|
})
|
|
333
648
|
})
|
|
649
|
+
|
|
650
|
+
// ---------------------------------------------------------------------------
|
|
651
|
+
// B3, B4, B5, L19 — ContentNode refines (Phase 3, Wave 1A)
|
|
652
|
+
// ---------------------------------------------------------------------------
|
|
653
|
+
//
|
|
654
|
+
// These refines apply recursively to every system in the model tree
|
|
655
|
+
// (top-level systems + nested subsystems at any depth).
|
|
656
|
+
//
|
|
657
|
+
// B3 — Cycle detection: parentContentId chain must not form a cycle.
|
|
658
|
+
// B4 — Same-system-only: parentContentId must resolve within the same content map.
|
|
659
|
+
// B5 — Payload validation: registered (kind, type) pairs validate data against
|
|
660
|
+
// the registered payloadSchema; unregistered pairs pass through (per D2).
|
|
661
|
+
// L19 — Same-meta-kind parent: when both child and parent are registered, they
|
|
662
|
+
// must share the same `kind` (meta-category).
|
|
663
|
+
|
|
664
|
+
type SystemLike = {
|
|
665
|
+
id?: string
|
|
666
|
+
content?: Record<string, { kind: string; type: string; parentContentId?: string; data?: Record<string, unknown> }>
|
|
667
|
+
subsystems?: Record<string, SystemLike>
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function validateSystemContent(system: SystemLike, systemPath: Array<string | number>): void {
|
|
671
|
+
const content = system.content
|
|
672
|
+
if (content === undefined || Object.keys(content).length === 0) {
|
|
673
|
+
// Recurse into subsystems even when own content is absent.
|
|
674
|
+
if (system.subsystems !== undefined) {
|
|
675
|
+
Object.entries(system.subsystems).forEach(([childKey, child]) => {
|
|
676
|
+
validateSystemContent(child, [...systemPath, 'subsystems', childKey])
|
|
677
|
+
})
|
|
678
|
+
}
|
|
679
|
+
return
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// B4 — verify every parentContentId resolves within this content map.
|
|
683
|
+
Object.entries(content).forEach(([localId, node]) => {
|
|
684
|
+
if (node.parentContentId !== undefined && !(node.parentContentId in content)) {
|
|
685
|
+
addIssue(
|
|
686
|
+
ctx,
|
|
687
|
+
[...systemPath, 'content', localId, 'parentContentId'],
|
|
688
|
+
`Content node "${localId}" parentContentId "${node.parentContentId}" does not resolve within the same system`
|
|
689
|
+
)
|
|
690
|
+
}
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
// B3 — cycle detection on parentContentId chains within this content map.
|
|
694
|
+
Object.entries(content).forEach(([localId, node]) => {
|
|
695
|
+
const visited = new Set<string>()
|
|
696
|
+
let currentId: string | undefined = node.parentContentId
|
|
697
|
+
|
|
698
|
+
while (currentId !== undefined) {
|
|
699
|
+
if (currentId === localId || visited.has(currentId)) {
|
|
700
|
+
addIssue(
|
|
701
|
+
ctx,
|
|
702
|
+
[...systemPath, 'content', localId, 'parentContentId'],
|
|
703
|
+
`Content node "${localId}" has a parentContentId cycle`
|
|
704
|
+
)
|
|
705
|
+
break
|
|
706
|
+
}
|
|
707
|
+
visited.add(currentId)
|
|
708
|
+
currentId = content[currentId]?.parentContentId
|
|
709
|
+
}
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
// B5 + L19 — per-node payload validation and same-meta-kind parent constraint.
|
|
713
|
+
Object.entries(content).forEach(([localId, node]) => {
|
|
714
|
+
const childDef = lookupContentType(node.kind, node.type)
|
|
715
|
+
|
|
716
|
+
// B5 — validate data against registered payloadSchema when pair is known.
|
|
717
|
+
if (childDef !== undefined && node.data !== undefined) {
|
|
718
|
+
const result = childDef.payloadSchema.safeParse(node.data)
|
|
719
|
+
if (!result.success) {
|
|
720
|
+
addIssue(
|
|
721
|
+
ctx,
|
|
722
|
+
[...systemPath, 'content', localId, 'data'],
|
|
723
|
+
`Content node "${localId}" (${node.kind}:${node.type}) data failed payload validation: ${result.error.message}`
|
|
724
|
+
)
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// L19 — when both child and parent are registered, they must share the same kind.
|
|
729
|
+
if (node.parentContentId !== undefined && childDef !== undefined) {
|
|
730
|
+
const parentNode = content[node.parentContentId]
|
|
731
|
+
if (parentNode !== undefined) {
|
|
732
|
+
const parentDef = lookupContentType(parentNode.kind, parentNode.type)
|
|
733
|
+
if (parentDef !== undefined && childDef.kind !== parentDef.kind) {
|
|
734
|
+
addIssue(
|
|
735
|
+
ctx,
|
|
736
|
+
[...systemPath, 'content', localId, 'parentContentId'],
|
|
737
|
+
`Content node "${localId}" kind "${childDef.kind}" cannot parent under "${node.parentContentId}" kind "${parentDef.kind}": parentContentId must be same-meta-kind (per L19)`
|
|
738
|
+
)
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
// Recurse into subsystems.
|
|
745
|
+
if (system.subsystems !== undefined) {
|
|
746
|
+
Object.entries(system.subsystems).forEach(([childKey, child]) => {
|
|
747
|
+
validateSystemContent(child, [...systemPath, 'subsystems', childKey])
|
|
748
|
+
})
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
Object.entries(model.systems).forEach(([systemKey, system]) => {
|
|
753
|
+
validateSystemContent(system as SystemLike, ['systems', systemKey])
|
|
754
|
+
})
|
|
334
755
|
})
|