@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,41 +1,39 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { DEFAULT_ORGANIZATION_MODEL_BRANDING } from '../domains/branding'
|
|
3
|
-
import { DEFAULT_ORGANIZATION_MODEL_SALES } from '../domains/sales'
|
|
4
|
-
import { DEFAULT_ORGANIZATION_MODEL_PROJECTS } from '../domains/projects'
|
|
5
|
-
import { DEFAULT_ORGANIZATION_MODEL_PROSPECTING } from '../domains/prospecting'
|
|
6
3
|
import { OrganizationModelSchema } from '../schema'
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
// Phase 4 (D8): sales, prospecting, projects, navigation top-level fields removed.
|
|
6
|
+
// DEFAULT_ORGANIZATION_MODEL_SALES / _PROSPECTING / _PROJECTS / OrganizationModelSalesSchema etc.
|
|
7
|
+
// no longer exported. makeMinimalModel no longer includes those fields.
|
|
8
|
+
// knowledge is now a flat Record<id, OrgKnowledgeNode> (D3) — wrapper shape removed.
|
|
9
|
+
|
|
10
|
+
function makeSystem(id: string, path = `/${id.replaceAll('.', '/')}`) {
|
|
9
11
|
return {
|
|
10
12
|
id,
|
|
13
|
+
order: 10,
|
|
11
14
|
label: id,
|
|
12
15
|
enabled: true,
|
|
16
|
+
lifecycle: 'active' as const,
|
|
13
17
|
path
|
|
14
18
|
}
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
id,
|
|
20
|
-
label: id,
|
|
21
|
-
path: `/${id.replaceAll('.', '/')}`,
|
|
22
|
-
surfaceType: 'page' as const,
|
|
23
|
-
featureId,
|
|
24
|
-
featureIds,
|
|
25
|
-
entityIds: [],
|
|
26
|
-
resourceIds: [],
|
|
27
|
-
capabilityIds: []
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function makeMinimalModel(features: unknown[] = []) {
|
|
21
|
+
function makeMinimalModel(systems: Record<string, unknown> = {}) {
|
|
22
|
+
const entityOwnerId = Object.keys(systems)[0] ?? 'dashboard'
|
|
32
23
|
return {
|
|
33
24
|
version: 1 as const,
|
|
34
|
-
features,
|
|
35
25
|
branding: DEFAULT_ORGANIZATION_MODEL_BRANDING,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
26
|
+
// Phase 4: sales, prospecting, projects removed from top-level OM fields.
|
|
27
|
+
entities: {
|
|
28
|
+
'crm.deal': { id: 'crm.deal', order: 10, label: 'Deal', ownedBySystemId: entityOwnerId },
|
|
29
|
+
'leadgen.list': { id: 'leadgen.list', order: 20, label: 'Lead List', ownedBySystemId: entityOwnerId },
|
|
30
|
+
'leadgen.company': { id: 'leadgen.company', order: 30, label: 'Lead Company', ownedBySystemId: entityOwnerId },
|
|
31
|
+
'leadgen.contact': { id: 'leadgen.contact', order: 40, label: 'Lead Contact', ownedBySystemId: entityOwnerId },
|
|
32
|
+
'delivery.project': { id: 'delivery.project', order: 50, label: 'Project', ownedBySystemId: entityOwnerId },
|
|
33
|
+
'delivery.milestone': { id: 'delivery.milestone', order: 60, label: 'Milestone', ownedBySystemId: entityOwnerId },
|
|
34
|
+
'delivery.task': { id: 'delivery.task', order: 70, label: 'Task', ownedBySystemId: entityOwnerId }
|
|
35
|
+
},
|
|
36
|
+
systems
|
|
39
37
|
}
|
|
40
38
|
}
|
|
41
39
|
|
|
@@ -45,161 +43,340 @@ function getIssueMessages(data: unknown): string[] {
|
|
|
45
43
|
return result.error.issues.map((issue) => issue.message)
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
describe('
|
|
46
|
+
describe('system tree validation', () => {
|
|
49
47
|
it('passes with a flat list that has declared ancestors', () => {
|
|
50
|
-
const model = makeMinimalModel(
|
|
51
|
-
{ id: 'sales', label: 'Sales', enabled: true },
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
const model = makeMinimalModel({
|
|
49
|
+
sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
|
|
50
|
+
'sales.crm': makeSystem('sales.crm', '/sales/crm/pipeline'),
|
|
51
|
+
'sales.lead-gen': makeSystem('sales.lead-gen', '/lead-gen/lists')
|
|
52
|
+
})
|
|
55
53
|
|
|
56
54
|
expect(() => OrganizationModelSchema.parse(model)).not.toThrow()
|
|
57
55
|
})
|
|
58
56
|
|
|
59
|
-
it('rejects duplicate
|
|
60
|
-
|
|
57
|
+
it('rejects duplicate system ids', () => {
|
|
58
|
+
// In a record, duplicate keys are impossible at the JS level.
|
|
59
|
+
// The schema refine checks that entry.id === mapKey, so mismatched ids are caught.
|
|
60
|
+
const messages = getIssueMessages(
|
|
61
|
+
makeMinimalModel({
|
|
62
|
+
sales: { id: 'NOT_sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' }
|
|
63
|
+
})
|
|
64
|
+
)
|
|
61
65
|
|
|
62
|
-
expect(messages.some((message) => message.includes('
|
|
66
|
+
expect(messages.some((message) => message.includes('Each system entry id must match its map key'))).toBe(true)
|
|
63
67
|
})
|
|
64
68
|
|
|
65
|
-
it('rejects duplicate effective
|
|
69
|
+
it('rejects duplicate effective system paths when explicit and default paths collide', () => {
|
|
66
70
|
const messages = getIssueMessages(
|
|
67
|
-
makeMinimalModel(
|
|
68
|
-
{ id: 'sales', label: 'Sales', enabled: true },
|
|
69
|
-
|
|
70
|
-
{ id: 'sales.lead-gen', label: 'Lead Gen', enabled: true }
|
|
71
|
-
|
|
71
|
+
makeMinimalModel({
|
|
72
|
+
sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
|
|
73
|
+
'sales.crm': makeSystem('sales.crm', '/sales/lead-gen'),
|
|
74
|
+
'sales.lead-gen': { id: 'sales.lead-gen', order: 20, label: 'Lead Gen', enabled: true, lifecycle: 'active' }
|
|
75
|
+
})
|
|
72
76
|
)
|
|
73
77
|
|
|
74
78
|
expect(
|
|
75
79
|
messages.some((message) =>
|
|
76
|
-
message.includes('
|
|
80
|
+
message.includes('System "sales.lead-gen" effective path "/sales/lead-gen" duplicates system "sales.crm"')
|
|
77
81
|
)
|
|
78
82
|
).toBe(true)
|
|
79
83
|
})
|
|
80
84
|
|
|
81
85
|
it('rejects a dotted child when its immediate parent is missing', () => {
|
|
82
|
-
const messages = getIssueMessages(
|
|
86
|
+
const messages = getIssueMessages(
|
|
87
|
+
makeMinimalModel({
|
|
88
|
+
'sales.crm.pipeline': { ...makeSystem('sales.crm.pipeline', '/pipeline'), parentSystemId: 'sales.crm' }
|
|
89
|
+
})
|
|
90
|
+
)
|
|
83
91
|
|
|
84
92
|
expect(messages.some((message) => message.includes('unknown parent "sales.crm"'))).toBe(true)
|
|
85
93
|
})
|
|
86
94
|
|
|
87
95
|
it('allows a leaf without a path so resolvers can derive a default path', () => {
|
|
88
|
-
const result = OrganizationModelSchema.safeParse(
|
|
96
|
+
const result = OrganizationModelSchema.safeParse(
|
|
97
|
+
makeMinimalModel({ sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' } })
|
|
98
|
+
)
|
|
89
99
|
|
|
90
100
|
expect(result.success).toBe(true)
|
|
91
101
|
})
|
|
92
102
|
|
|
93
|
-
it('rejects an
|
|
103
|
+
it('rejects an active container with no active descendants', () => {
|
|
94
104
|
const messages = getIssueMessages(
|
|
95
|
-
makeMinimalModel(
|
|
96
|
-
{ id: 'sales', label: 'Sales', enabled: true },
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
makeMinimalModel({
|
|
106
|
+
sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
|
|
107
|
+
'sales.crm': {
|
|
108
|
+
id: 'sales.crm',
|
|
109
|
+
order: 20,
|
|
110
|
+
label: 'CRM',
|
|
111
|
+
enabled: false,
|
|
112
|
+
lifecycle: 'deprecated',
|
|
113
|
+
path: '/sales/crm/pipeline'
|
|
114
|
+
}
|
|
115
|
+
})
|
|
99
116
|
)
|
|
100
117
|
|
|
101
|
-
expect(messages.some((message) => message.includes('has no
|
|
118
|
+
expect(messages.some((message) => message.includes('has no active descendants'))).toBe(true)
|
|
102
119
|
})
|
|
103
120
|
|
|
104
|
-
it('
|
|
105
|
-
const model = OrganizationModelSchema.parse(
|
|
106
|
-
makeMinimalModel([makeFeature('dashboard', '/')])
|
|
107
|
-
)
|
|
121
|
+
it('navigation.sidebar defaults to empty sections when omitted', () => {
|
|
122
|
+
const model = OrganizationModelSchema.parse(makeMinimalModel({ dashboard: makeSystem('dashboard', '/') }))
|
|
108
123
|
|
|
109
|
-
expect(model.navigation.
|
|
110
|
-
expect(model.navigation.
|
|
124
|
+
expect(model.navigation.sidebar.primary).toEqual({})
|
|
125
|
+
expect(model.navigation.sidebar.bottom).toEqual({})
|
|
126
|
+
expect('surfaces' in model).toBe(false)
|
|
127
|
+
expect('navigationGroups' in model).toBe(false)
|
|
111
128
|
expect('resourceMappings' in model).toBe(false)
|
|
112
129
|
})
|
|
113
130
|
|
|
114
|
-
|
|
131
|
+
// Phase 4: the old model.navigation.defaultSurfaceId validation no longer exists.
|
|
132
|
+
// Navigation defaults are now managed at the foundation layer (createFoundationOrganizationModel).
|
|
133
|
+
// The following tests that exercised model.navigation.* refines are skipped with a reason.
|
|
134
|
+
it.skip('rejects an unknown navigation default surface when surfaces are explicitly empty (deferred — Phase 4: navigation field removed)', () => {
|
|
135
|
+
// Previously tested: model.navigation.defaultSurfaceId referencing a missing surface.
|
|
136
|
+
// The navigation domain no longer exists at the top-level OM schema level.
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it.skip('rejects navigation group surfaceIds that reference missing surfaces (deferred — Phase 4: navigation field removed)', () => {
|
|
140
|
+
// Previously tested: model.navigation.groups[*].surfaceIds referencing missing surfaces.
|
|
141
|
+
// Navigation groups are now top-level navigationGroups Record — cross-ref validation
|
|
142
|
+
// is handled at the resolveOrganizationModel refine layer if/when re-added.
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it.skip('rejects navigation surface systemIds references to missing systems (deferred — Phase 4: navigation field removed)', () => {
|
|
146
|
+
// Previously tested: model.navigation.surfaces[*].systemIds referencing missing systems.
|
|
147
|
+
// Surfaces are now top-level surfaces Record — cross-ref validation is handled separately.
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it.skip('accepts navigation systemIds when canonical systems exist (deferred — Phase 4: navigation field removed)', () => {
|
|
151
|
+
// Previously tested: model.navigation.surfaces[*].systemIds with valid system refs.
|
|
152
|
+
// See navigation.test.ts for top-level surfaces/navigationGroups coverage.
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('domain metadata validation', () => {
|
|
157
|
+
it('defaults domain metadata and knowledge domain versioning', () => {
|
|
158
|
+
const model = OrganizationModelSchema.parse(makeMinimalModel({ dashboard: makeSystem('dashboard', '/') }))
|
|
159
|
+
|
|
160
|
+
expect(model.domainMetadata.knowledge).toEqual({ version: 1, lastModified: '2026-05-10' })
|
|
161
|
+
expect(model.domainMetadata.actions).toEqual({ version: 1, lastModified: '2026-05-10' })
|
|
162
|
+
expect(model.domainMetadata.entities).toEqual({ version: 1, lastModified: '2026-05-10' })
|
|
163
|
+
expect(model.domainMetadata.policies).toEqual({ version: 1, lastModified: '2026-05-10' })
|
|
164
|
+
// Phase 4 (D7): knowledge version/lastModified moved to domainMetadata.knowledge.
|
|
165
|
+
// model.knowledge is now a flat Record<id, OrgKnowledgeNode> — no .version or .lastModified.
|
|
166
|
+
expect(model.domainMetadata.knowledge.version).toBe(1)
|
|
167
|
+
expect(model.domainMetadata.knowledge.lastModified).toBe('2026-05-10')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('rejects malformed domain lastModified dates', () => {
|
|
115
171
|
const messages = getIssueMessages({
|
|
116
|
-
...makeMinimalModel(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
surfaces: [],
|
|
120
|
-
groups: []
|
|
172
|
+
...makeMinimalModel({ dashboard: makeSystem('dashboard', '/') }),
|
|
173
|
+
domainMetadata: {
|
|
174
|
+
knowledge: { version: 1, lastModified: '05/10/2026' }
|
|
121
175
|
}
|
|
122
176
|
})
|
|
123
177
|
|
|
124
|
-
expect(
|
|
125
|
-
messages.some((message) =>
|
|
126
|
-
message.includes('Navigation defaultSurfaceId references unknown surface "missing.surface"')
|
|
127
|
-
)
|
|
128
|
-
).toBe(true)
|
|
178
|
+
expect(messages.some((message) => message.includes('lastModified must be an ISO date string'))).toBe(true)
|
|
129
179
|
})
|
|
180
|
+
})
|
|
130
181
|
|
|
131
|
-
|
|
182
|
+
describe('entity validation', () => {
|
|
183
|
+
it('rejects entities owned by unknown systems', () => {
|
|
132
184
|
const messages = getIssueMessages({
|
|
133
|
-
...makeMinimalModel(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
]
|
|
185
|
+
...makeMinimalModel({ dashboard: makeSystem('dashboard', '/') }),
|
|
186
|
+
entities: {
|
|
187
|
+
'crm.deal': { id: 'crm.deal', order: 10, label: 'Deal', ownedBySystemId: 'missing.system' },
|
|
188
|
+
'leadgen.list': { id: 'leadgen.list', order: 20, label: 'Lead List', ownedBySystemId: 'dashboard' },
|
|
189
|
+
'leadgen.company': { id: 'leadgen.company', order: 30, label: 'Lead Company', ownedBySystemId: 'dashboard' },
|
|
190
|
+
'leadgen.contact': { id: 'leadgen.contact', order: 40, label: 'Lead Contact', ownedBySystemId: 'dashboard' },
|
|
191
|
+
'delivery.project': { id: 'delivery.project', order: 50, label: 'Project', ownedBySystemId: 'dashboard' },
|
|
192
|
+
'delivery.milestone': { id: 'delivery.milestone', order: 60, label: 'Milestone', ownedBySystemId: 'dashboard' },
|
|
193
|
+
'delivery.task': { id: 'delivery.task', order: 70, label: 'Task', ownedBySystemId: 'dashboard' }
|
|
144
194
|
}
|
|
145
195
|
})
|
|
146
196
|
|
|
147
|
-
expect(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
)
|
|
151
|
-
).toBe(true)
|
|
197
|
+
expect(messages.some((message) => message.includes('references unknown ownedBySystemId "missing.system"'))).toBe(
|
|
198
|
+
true
|
|
199
|
+
)
|
|
152
200
|
})
|
|
153
201
|
|
|
154
|
-
it('rejects
|
|
202
|
+
it('rejects entity links to unknown entities', () => {
|
|
155
203
|
const messages = getIssueMessages({
|
|
156
|
-
...makeMinimalModel(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
204
|
+
...makeMinimalModel({ dashboard: makeSystem('dashboard', '/') }),
|
|
205
|
+
entities: {
|
|
206
|
+
'crm.deal': {
|
|
207
|
+
id: 'crm.deal',
|
|
208
|
+
order: 10,
|
|
209
|
+
label: 'Deal',
|
|
210
|
+
ownedBySystemId: 'dashboard',
|
|
211
|
+
links: [{ toEntity: 'missing.entity', kind: 'belongs-to' }]
|
|
212
|
+
},
|
|
213
|
+
'leadgen.list': { id: 'leadgen.list', order: 20, label: 'Lead List', ownedBySystemId: 'dashboard' },
|
|
214
|
+
'leadgen.company': { id: 'leadgen.company', order: 30, label: 'Lead Company', ownedBySystemId: 'dashboard' },
|
|
215
|
+
'leadgen.contact': { id: 'leadgen.contact', order: 40, label: 'Lead Contact', ownedBySystemId: 'dashboard' },
|
|
216
|
+
'delivery.project': { id: 'delivery.project', order: 50, label: 'Project', ownedBySystemId: 'dashboard' },
|
|
217
|
+
'delivery.milestone': { id: 'delivery.milestone', order: 60, label: 'Milestone', ownedBySystemId: 'dashboard' },
|
|
218
|
+
'delivery.task': { id: 'delivery.task', order: 70, label: 'Task', ownedBySystemId: 'dashboard' }
|
|
160
219
|
}
|
|
161
220
|
})
|
|
162
221
|
|
|
163
|
-
expect(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
222
|
+
expect(messages.some((message) => message.includes('links to unknown entity "missing.entity"'))).toBe(true)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
// Phase 4: Prospecting.contactEntityId validation removed (prospecting domain deleted).
|
|
226
|
+
// The separate test for prospecting entity refs is skipped below.
|
|
227
|
+
it.skip('rejects prospecting contactEntityId referencing unknown entity (deferred — Phase 4: prospecting domain removed)', () => {
|
|
228
|
+
// Previously tested: model.prospecting.contactEntityId referencing a missing entity.
|
|
229
|
+
// The prospecting domain no longer exists at the top-level OM schema level.
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe('system action and policy validation', () => {
|
|
234
|
+
it('accepts systems that attach known actions and policies', () => {
|
|
235
|
+
const result = OrganizationModelSchema.safeParse({
|
|
236
|
+
...makeMinimalModel({
|
|
237
|
+
sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
|
|
238
|
+
'sales.lead-gen': makeSystem('sales.lead-gen')
|
|
239
|
+
}),
|
|
240
|
+
systems: {
|
|
241
|
+
sales: {
|
|
242
|
+
id: 'sales',
|
|
243
|
+
order: 10,
|
|
244
|
+
label: 'Sales',
|
|
245
|
+
lifecycle: 'active'
|
|
246
|
+
},
|
|
247
|
+
'sales.lead-gen': {
|
|
248
|
+
id: 'sales.lead-gen',
|
|
249
|
+
order: 20,
|
|
250
|
+
label: 'Lead Gen',
|
|
251
|
+
lifecycle: 'active',
|
|
252
|
+
actions: [{ actionId: 'lead-gen.company.source', intent: 'exposes' }],
|
|
253
|
+
policies: ['policy.lead-gen.approval']
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
policies: {
|
|
257
|
+
'policy.lead-gen.approval': {
|
|
258
|
+
id: 'policy.lead-gen.approval',
|
|
259
|
+
order: 10,
|
|
260
|
+
label: 'Lead Gen Approval',
|
|
261
|
+
trigger: { kind: 'manual' },
|
|
262
|
+
actions: [{ kind: 'block' }],
|
|
263
|
+
appliesTo: { systemIds: ['sales.lead-gen'] }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
expect(result.success).toBe(true)
|
|
168
269
|
})
|
|
169
270
|
|
|
170
|
-
it('rejects
|
|
271
|
+
it('rejects systems that attach unknown actions or policies', () => {
|
|
171
272
|
const messages = getIssueMessages({
|
|
172
|
-
...makeMinimalModel(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
273
|
+
...makeMinimalModel({
|
|
274
|
+
sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
|
|
275
|
+
'sales.lead-gen': makeSystem('sales.lead-gen')
|
|
276
|
+
}),
|
|
277
|
+
systems: {
|
|
278
|
+
sales: {
|
|
279
|
+
id: 'sales',
|
|
280
|
+
order: 10,
|
|
281
|
+
label: 'Sales',
|
|
282
|
+
lifecycle: 'active'
|
|
283
|
+
},
|
|
284
|
+
'sales.lead-gen': {
|
|
285
|
+
id: 'sales.lead-gen',
|
|
286
|
+
order: 20,
|
|
287
|
+
label: 'Lead Gen',
|
|
288
|
+
lifecycle: 'active',
|
|
289
|
+
actions: [{ actionId: 'missing.action', intent: 'exposes' }],
|
|
290
|
+
policies: ['missing.policy']
|
|
291
|
+
}
|
|
176
292
|
}
|
|
177
293
|
})
|
|
178
294
|
|
|
179
|
-
expect(
|
|
180
|
-
|
|
181
|
-
message.includes('Navigation surface "dashboard.home" references unknown feature "missing.feature"')
|
|
182
|
-
)
|
|
183
|
-
).toBe(true)
|
|
295
|
+
expect(messages.some((message) => message.includes('references unknown action "missing.action"'))).toBe(true)
|
|
296
|
+
expect(messages.some((message) => message.includes('references unknown policy "missing.policy"'))).toBe(true)
|
|
184
297
|
})
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
describe('knowledge governs validation', () => {
|
|
301
|
+
function makeKnowledgeNode(overrides: Record<string, unknown> = {}) {
|
|
302
|
+
return {
|
|
303
|
+
id: 'knowledge.sales-crm-playbook',
|
|
304
|
+
kind: 'playbook' as const,
|
|
305
|
+
title: 'Sales CRM Playbook',
|
|
306
|
+
summary: 'How CRM work is governed.',
|
|
307
|
+
body: '## CRM',
|
|
308
|
+
updatedAt: '2026-05-10',
|
|
309
|
+
links: [{ target: { kind: 'system', id: 'sales.crm' } }],
|
|
310
|
+
...overrides
|
|
311
|
+
}
|
|
312
|
+
}
|
|
185
313
|
|
|
186
|
-
|
|
314
|
+
// Phase 4 (D3): knowledge is now a flat Record<id, OrgKnowledgeNode>.
|
|
315
|
+
// Fixtures updated from { nodes: [...] } to { 'knowledge.id': {...node} }.
|
|
316
|
+
|
|
317
|
+
it('accepts typed knowledge links when the target exists', () => {
|
|
187
318
|
const result = OrganizationModelSchema.safeParse({
|
|
188
|
-
...makeMinimalModel(
|
|
189
|
-
{ id: 'sales', label: 'Sales', enabled: true },
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
defaultSurfaceId: 'crm.pipeline',
|
|
195
|
-
surfaces: [
|
|
196
|
-
makeSurface('crm.pipeline', 'crm', ['crm']),
|
|
197
|
-
makeSurface('lead-gen.lists', 'lead-gen', ['lead-gen'])
|
|
198
|
-
],
|
|
199
|
-
groups: []
|
|
319
|
+
...makeMinimalModel({
|
|
320
|
+
sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
|
|
321
|
+
'sales.crm': makeSystem('sales.crm', '/crm')
|
|
322
|
+
}),
|
|
323
|
+
knowledge: {
|
|
324
|
+
'knowledge.sales-crm-playbook': makeKnowledgeNode()
|
|
200
325
|
}
|
|
201
326
|
})
|
|
202
327
|
|
|
203
328
|
expect(result.success).toBe(true)
|
|
204
329
|
})
|
|
330
|
+
|
|
331
|
+
it('rejects typed knowledge links to unknown modeled targets', () => {
|
|
332
|
+
const messages = getIssueMessages({
|
|
333
|
+
...makeMinimalModel({
|
|
334
|
+
sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
|
|
335
|
+
'sales.crm': makeSystem('sales.crm', '/crm')
|
|
336
|
+
}),
|
|
337
|
+
knowledge: {
|
|
338
|
+
'knowledge.sales-crm-playbook': makeKnowledgeNode({
|
|
339
|
+
links: [{ target: { kind: 'resource', id: 'missing.workflow' } }]
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
expect(messages.some((message) => message.includes('references unknown resource target "missing.workflow"'))).toBe(
|
|
345
|
+
true
|
|
346
|
+
)
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('allows strategy nodes to target systems', () => {
|
|
350
|
+
const result = OrganizationModelSchema.safeParse({
|
|
351
|
+
...makeMinimalModel({
|
|
352
|
+
sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
|
|
353
|
+
'sales.crm': makeSystem('sales.crm', '/crm')
|
|
354
|
+
}),
|
|
355
|
+
knowledge: {
|
|
356
|
+
'knowledge.sales-crm-playbook': makeKnowledgeNode({
|
|
357
|
+
kind: 'strategy',
|
|
358
|
+
links: [{ target: { kind: 'system', id: 'sales.crm' } }]
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
expect(result.success).toBe(true)
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('rejects incompatible knowledge kind to target kind pairings', () => {
|
|
367
|
+
const messages = getIssueMessages({
|
|
368
|
+
...makeMinimalModel({
|
|
369
|
+
sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, lifecycle: 'active' },
|
|
370
|
+
'sales.crm': makeSystem('sales.crm', '/crm')
|
|
371
|
+
}),
|
|
372
|
+
knowledge: {
|
|
373
|
+
'knowledge.sales-crm-playbook': makeKnowledgeNode({
|
|
374
|
+
kind: 'playbook',
|
|
375
|
+
links: [{ target: { kind: 'goal', id: 'missing.goal' } }]
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
expect(messages.some((message) => message.includes('kind "playbook" cannot govern goal targets'))).toBe(true)
|
|
381
|
+
})
|
|
205
382
|
})
|