@elevasis/core 0.24.1 → 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 +239 -86
- package/dist/index.js +474 -1346
- package/dist/knowledge/index.d.ts +57 -39
- package/dist/knowledge/index.js +1 -1
- package/dist/organization-model/index.d.ts +239 -86
- package/dist/organization-model/index.js +474 -1346
- package/dist/test-utils/index.d.ts +24 -31
- package/dist/test-utils/index.js +76 -1238
- package/package.json +1 -1
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +108 -96
- 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/execution/engine/llm/adapters/__tests__/openrouter.integration.test.ts +10 -10
- package/src/knowledge/__tests__/queries.test.ts +960 -546
- package/src/knowledge/format.ts +322 -100
- package/src/knowledge/index.ts +18 -5
- package/src/knowledge/queries.ts +1004 -240
- 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__/deprecate-helpers.test.ts +71 -0
- 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 +88 -49
- package/src/organization-model/__tests__/scaffolders.test.ts +93 -0
- 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 +120 -141
- package/src/organization-model/icons.ts +1 -0
- package/src/organization-model/index.ts +107 -126
- 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 +251 -228
- package/src/organization-model/resolve.ts +4 -5
- package/src/organization-model/scaffolders/helpers.ts +84 -0
- package/src/organization-model/scaffolders/index.ts +19 -0
- package/src/organization-model/scaffolders/scaffoldKnowledgeNode.ts +48 -0
- package/src/organization-model/scaffolders/scaffoldOntologyRecord.ts +38 -0
- package/src/organization-model/scaffolders/scaffoldResource.ts +59 -0
- package/src/organization-model/scaffolders/scaffoldSystem.ts +110 -0
- package/src/organization-model/scaffolders/types.ts +81 -0
- package/src/organization-model/schema.ts +610 -704
- package/src/organization-model/types.ts +167 -161
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/registry/__tests__/validation.test.ts +23 -0
- package/src/platform/registry/validation.ts +13 -2
- package/src/reference/_generated/contracts.md +108 -96
- package/src/reference/glossary.md +71 -69
- 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,549 +1,176 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* recursive-system-schema.test.ts
|
|
3
|
-
*
|
|
4
|
-
* Tests for B3, B4, B5, L19, and D2 content-node refines in OrganizationModelSchema.
|
|
5
|
-
* Each constraint has ≥1 positive (passes) and ≥1 negative (error) assertion.
|
|
6
|
-
*
|
|
7
|
-
* B3 — Cycle detection: parentContentId chain must not form a cycle.
|
|
8
|
-
* B4 — Same-system-only: parentContentId must resolve within the same content map.
|
|
9
|
-
* B5 — Payload validation: registered (kind, type) pairs validate data.
|
|
10
|
-
* L19 — Same-meta-kind parent constraint.
|
|
11
|
-
* D2 — Unregistered (kind, type) passes through without error.
|
|
12
|
-
*/
|
|
13
1
|
import { describe, expect, it } from 'vitest'
|
|
14
|
-
import {
|
|
15
|
-
import { getSystem, listAllSystems } from '../helpers'
|
|
2
|
+
import { DEFAULT_ORGANIZATION_MODEL_BRANDING } from '../domains/branding'
|
|
16
3
|
import { OrganizationModelSchema } from '../schema'
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
'delivery.project': { id: 'delivery.project', order: 50, label: 'Project', ownedBySystemId: entityOwnerId },
|
|
50
|
-
'delivery.milestone': {
|
|
51
|
-
id: 'delivery.milestone',
|
|
52
|
-
order: 60,
|
|
53
|
-
label: 'Milestone',
|
|
54
|
-
ownedBySystemId: entityOwnerId
|
|
55
|
-
},
|
|
56
|
-
'delivery.task': { id: 'delivery.task', order: 70, label: 'Task', ownedBySystemId: entityOwnerId }
|
|
57
|
-
},
|
|
58
|
-
systems,
|
|
59
|
-
...extra
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function getIssueMessages(data: unknown): string[] {
|
|
64
|
-
const result = OrganizationModelSchema.safeParse(data)
|
|
65
|
-
if (result.success) return []
|
|
66
|
-
return result.error.issues.map((issue) => issue.message)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function parseSucceeds(data: unknown): boolean {
|
|
70
|
-
return OrganizationModelSchema.safeParse(data).success
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// 2-level subsystem + content fixture round-trips
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
describe('2-level subsystems + content (positive, structural)', () => {
|
|
78
|
-
it('parses a model with a top-level system that has a subsystem via resolveOrganizationModel', () => {
|
|
79
|
-
const model = resolveOrganizationModel({
|
|
80
|
-
systems: {
|
|
81
|
-
ops: { id: 'ops', order: 99, label: 'Ops', enabled: true, path: '/ops' },
|
|
82
|
-
'ops.sub': {
|
|
83
|
-
id: 'ops.sub',
|
|
84
|
-
order: 100,
|
|
85
|
-
label: 'Sub',
|
|
86
|
-
enabled: true,
|
|
87
|
-
path: '/ops/sub',
|
|
88
|
-
parentSystemId: 'ops'
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
})
|
|
92
|
-
expect(model.systems['ops']).toBeDefined()
|
|
93
|
-
expect(model.systems['ops.sub']).toBeDefined()
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('parses a system with a content map carrying a valid registered node', () => {
|
|
97
|
-
const data = makeMinimalModel({
|
|
98
|
-
hub: {
|
|
99
|
-
...makeSystem('hub', '/hub'),
|
|
100
|
-
content: {
|
|
101
|
-
'main-pipeline': {
|
|
102
|
-
kind: 'schema',
|
|
103
|
-
type: 'pipeline',
|
|
104
|
-
label: 'Main Pipeline',
|
|
105
|
-
data: { entityId: 'crm.deal' }
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
})
|
|
110
|
-
expect(parseSucceeds(data)).toBe(true)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('parses a 2-level subsystem hierarchy with content at each level', () => {
|
|
114
|
-
const data = makeMinimalModel({
|
|
115
|
-
parent: {
|
|
116
|
-
...makeSystem('parent', '/parent'),
|
|
117
|
-
content: {
|
|
118
|
-
'parent-config': {
|
|
119
|
-
kind: 'config',
|
|
120
|
-
type: 'kv',
|
|
121
|
-
label: 'Parent Config',
|
|
122
|
-
data: { entries: { maxBatch: 10 } }
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
'parent.child': {
|
|
127
|
-
...makeSystem('parent.child', '/parent/child'),
|
|
128
|
-
parentSystemId: 'parent',
|
|
129
|
-
content: {
|
|
130
|
-
'child-config': {
|
|
131
|
-
kind: 'config',
|
|
132
|
-
type: 'kv',
|
|
133
|
-
label: 'Child Config',
|
|
134
|
-
data: { entries: { timeout: 5000 } }
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
|
-
expect(parseSucceeds(data)).toBe(true)
|
|
4
|
+
import { resolveOrganizationModel } from '../resolve'
|
|
5
|
+
|
|
6
|
+
function minimalModel(systems: Record<string, unknown>) {
|
|
7
|
+
return {
|
|
8
|
+
version: 1,
|
|
9
|
+
branding: DEFAULT_ORGANIZATION_MODEL_BRANDING,
|
|
10
|
+
entities: {},
|
|
11
|
+
systems
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('recursive System schema after System.content retirement', () => {
|
|
16
|
+
it('parses recursively authored systems with canonical systems children', () => {
|
|
17
|
+
const model = resolveOrganizationModel(
|
|
18
|
+
minimalModel({
|
|
19
|
+
parent: {
|
|
20
|
+
id: 'parent',
|
|
21
|
+
order: 10,
|
|
22
|
+
label: 'Parent',
|
|
23
|
+
systems: {
|
|
24
|
+
child: {
|
|
25
|
+
id: 'parent.child',
|
|
26
|
+
order: 20,
|
|
27
|
+
label: 'Child'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
expect(model.systems.parent?.systems?.child?.id).toBe('parent.child')
|
|
35
|
+
expect(model.systems.parent?.subsystems?.child?.id).toBe('parent.child')
|
|
140
36
|
})
|
|
141
37
|
|
|
142
|
-
it('
|
|
143
|
-
const model = resolveOrganizationModel(
|
|
144
|
-
|
|
38
|
+
it('keeps subsystems as a compatibility alias for recursive traversal', () => {
|
|
39
|
+
const model = resolveOrganizationModel(
|
|
40
|
+
minimalModel({
|
|
145
41
|
parent: {
|
|
146
|
-
|
|
42
|
+
id: 'parent',
|
|
43
|
+
order: 10,
|
|
44
|
+
label: 'Parent',
|
|
45
|
+
subsystems: {
|
|
46
|
+
child: {
|
|
47
|
+
id: 'parent.child',
|
|
48
|
+
order: 20,
|
|
49
|
+
label: 'Child'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
expect(model.systems.parent?.subsystems?.child?.id).toBe('parent.child')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('rejects content maps on top-level systems', () => {
|
|
60
|
+
const result = OrganizationModelSchema.safeParse(
|
|
61
|
+
minimalModel({
|
|
62
|
+
dashboard: {
|
|
63
|
+
id: 'dashboard',
|
|
64
|
+
order: 10,
|
|
65
|
+
label: 'Dashboard',
|
|
66
|
+
content: {
|
|
67
|
+
pipeline: {
|
|
68
|
+
kind: 'schema',
|
|
69
|
+
type: 'pipeline',
|
|
70
|
+
label: 'Pipeline',
|
|
71
|
+
data: { entityId: 'crm.deal' }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
expect(result.success).toBe(false)
|
|
79
|
+
const contentIssue = result.error?.issues.find((issue) => issue.path.join('.') === 'systems.dashboard')
|
|
80
|
+
expect(contentIssue).toMatchObject({
|
|
81
|
+
code: 'unrecognized_keys',
|
|
82
|
+
path: ['systems', 'dashboard']
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('rejects content maps on nested systems', () => {
|
|
87
|
+
const result = OrganizationModelSchema.safeParse(
|
|
88
|
+
minimalModel({
|
|
89
|
+
parent: {
|
|
90
|
+
id: 'parent',
|
|
91
|
+
order: 10,
|
|
92
|
+
label: 'Parent',
|
|
147
93
|
systems: {
|
|
148
94
|
child: {
|
|
149
|
-
|
|
95
|
+
id: 'parent.child',
|
|
96
|
+
order: 20,
|
|
97
|
+
label: 'Child',
|
|
150
98
|
content: {
|
|
151
|
-
|
|
152
|
-
kind: '
|
|
153
|
-
type: '
|
|
154
|
-
label: '
|
|
155
|
-
data: {
|
|
99
|
+
settings: {
|
|
100
|
+
kind: 'config',
|
|
101
|
+
type: 'kv',
|
|
102
|
+
label: 'Settings',
|
|
103
|
+
data: { entries: { mode: 'bridge' } }
|
|
156
104
|
}
|
|
157
105
|
}
|
|
158
106
|
}
|
|
159
107
|
}
|
|
160
108
|
}
|
|
161
|
-
}
|
|
109
|
+
})
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
expect(result.success).toBe(false)
|
|
113
|
+
const contentIssue = result.error?.issues.find((issue) => issue.path.join('.') === 'systems.parent.systems.child')
|
|
114
|
+
expect(contentIssue).toMatchObject({
|
|
115
|
+
code: 'unrecognized_keys',
|
|
116
|
+
path: ['systems', 'parent', 'systems', 'child']
|
|
162
117
|
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('accepts equivalent catalog authoring in system ontology', () => {
|
|
121
|
+
const result = OrganizationModelSchema.safeParse(
|
|
122
|
+
minimalModel({
|
|
123
|
+
dashboard: {
|
|
124
|
+
id: 'dashboard',
|
|
125
|
+
order: 10,
|
|
126
|
+
label: 'Dashboard',
|
|
127
|
+
ontology: {
|
|
128
|
+
objectTypes: {
|
|
129
|
+
'dashboard:object/deal': {
|
|
130
|
+
id: 'dashboard:object/deal',
|
|
131
|
+
label: 'Deal',
|
|
132
|
+
ownerSystemId: 'dashboard'
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
catalogTypes: {
|
|
136
|
+
'dashboard:catalog/pipeline': {
|
|
137
|
+
id: 'dashboard:catalog/pipeline',
|
|
138
|
+
label: 'Pipeline',
|
|
139
|
+
kind: 'pipeline',
|
|
140
|
+
appliesTo: 'dashboard:object/deal',
|
|
141
|
+
entries: {
|
|
142
|
+
open: { label: 'Open', order: 10, semanticClass: 'open' }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
expect(result.success).toBe(true)
|
|
152
|
+
})
|
|
163
153
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
154
|
+
it('accepts equivalent local settings in first-class system config', () => {
|
|
155
|
+
const model = resolveOrganizationModel(
|
|
156
|
+
minimalModel({
|
|
157
|
+
dashboard: {
|
|
158
|
+
id: 'dashboard',
|
|
159
|
+
order: 10,
|
|
160
|
+
label: 'Dashboard',
|
|
161
|
+
config: {
|
|
162
|
+
mode: 'direct',
|
|
163
|
+
retries: 3,
|
|
164
|
+
nested: { enabled: true }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
expect(model.systems.dashboard?.config).toEqual({
|
|
171
|
+
mode: 'direct',
|
|
172
|
+
retries: 3,
|
|
173
|
+
nested: { enabled: true }
|
|
174
|
+
})
|
|
168
175
|
})
|
|
169
176
|
})
|
|
170
|
-
|
|
171
|
-
// ---------------------------------------------------------------------------
|
|
172
|
-
// B3 — parentContentId cycle detection
|
|
173
|
-
// ---------------------------------------------------------------------------
|
|
174
|
-
|
|
175
|
-
describe('B3 — parentContentId cycle detection', () => {
|
|
176
|
-
it('POSITIVE: linear parentContentId chain (A → B, B has no parent) parses', () => {
|
|
177
|
-
const data = makeMinimalModel({
|
|
178
|
-
hub: {
|
|
179
|
-
...makeSystem('hub', '/hub'),
|
|
180
|
-
content: {
|
|
181
|
-
flow: {
|
|
182
|
-
kind: 'schema',
|
|
183
|
-
type: 'status-flow',
|
|
184
|
-
label: 'Flow',
|
|
185
|
-
data: { appliesTo: 'project' }
|
|
186
|
-
},
|
|
187
|
-
status: {
|
|
188
|
-
kind: 'schema',
|
|
189
|
-
type: 'status',
|
|
190
|
-
label: 'Active',
|
|
191
|
-
parentContentId: 'flow',
|
|
192
|
-
data: { semanticClass: 'active' }
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
expect(parseSucceeds(data)).toBe(true)
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
it('NEGATIVE: direct self-cycle (node points to itself) produces cycle error', () => {
|
|
201
|
-
const data = makeMinimalModel({
|
|
202
|
-
hub: {
|
|
203
|
-
...makeSystem('hub', '/hub'),
|
|
204
|
-
content: {
|
|
205
|
-
'self-ref': {
|
|
206
|
-
kind: 'schema',
|
|
207
|
-
type: 'pipeline',
|
|
208
|
-
label: 'Self Ref',
|
|
209
|
-
parentContentId: 'self-ref'
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
})
|
|
214
|
-
const messages = getIssueMessages(data)
|
|
215
|
-
expect(messages.some((m) => m.includes('parentContentId cycle'))).toBe(true)
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
it('NEGATIVE: indirect 2-node cycle (A → B → A) produces cycle error', () => {
|
|
219
|
-
const data = makeMinimalModel({
|
|
220
|
-
hub: {
|
|
221
|
-
...makeSystem('hub', '/hub'),
|
|
222
|
-
content: {
|
|
223
|
-
alpha: {
|
|
224
|
-
kind: 'schema',
|
|
225
|
-
type: 'pipeline',
|
|
226
|
-
label: 'Alpha',
|
|
227
|
-
parentContentId: 'beta'
|
|
228
|
-
},
|
|
229
|
-
beta: {
|
|
230
|
-
kind: 'schema',
|
|
231
|
-
type: 'stage',
|
|
232
|
-
label: 'Beta',
|
|
233
|
-
parentContentId: 'alpha'
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
})
|
|
238
|
-
const messages = getIssueMessages(data)
|
|
239
|
-
expect(messages.some((m) => m.includes('parentContentId cycle'))).toBe(true)
|
|
240
|
-
})
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
// ---------------------------------------------------------------------------
|
|
244
|
-
// B4 — parentContentId must resolve within the same system content map
|
|
245
|
-
// ---------------------------------------------------------------------------
|
|
246
|
-
|
|
247
|
-
describe('B4 — parentContentId cross-system reference error', () => {
|
|
248
|
-
it('POSITIVE: parentContentId referencing a sibling in the same content map parses', () => {
|
|
249
|
-
const data = makeMinimalModel({
|
|
250
|
-
hub: {
|
|
251
|
-
...makeSystem('hub', '/hub'),
|
|
252
|
-
content: {
|
|
253
|
-
parent: {
|
|
254
|
-
kind: 'schema',
|
|
255
|
-
type: 'status-flow',
|
|
256
|
-
label: 'Status Flow',
|
|
257
|
-
data: { appliesTo: 'task' }
|
|
258
|
-
},
|
|
259
|
-
child: {
|
|
260
|
-
kind: 'schema',
|
|
261
|
-
type: 'status',
|
|
262
|
-
label: 'Open',
|
|
263
|
-
parentContentId: 'parent',
|
|
264
|
-
data: { semanticClass: 'active' }
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
})
|
|
269
|
-
expect(parseSucceeds(data)).toBe(true)
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
it('NEGATIVE: parentContentId referencing a non-existent localId produces B4 error', () => {
|
|
273
|
-
const data = makeMinimalModel({
|
|
274
|
-
hub: {
|
|
275
|
-
...makeSystem('hub', '/hub'),
|
|
276
|
-
content: {
|
|
277
|
-
orphan: {
|
|
278
|
-
kind: 'schema',
|
|
279
|
-
type: 'stage',
|
|
280
|
-
label: 'Orphan',
|
|
281
|
-
parentContentId: 'does-not-exist-in-this-system'
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
})
|
|
286
|
-
const messages = getIssueMessages(data)
|
|
287
|
-
expect(
|
|
288
|
-
messages.some((m) => m.includes('does not resolve within the same system') || m.includes('parentContentId'))
|
|
289
|
-
).toBe(true)
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
it('NEGATIVE: cross-system localId reference (sibling system content is a different map)', () => {
|
|
293
|
-
// "other-system" has "shared-node" but "hub" does NOT — so parentContentId is cross-system
|
|
294
|
-
const data = makeMinimalModel({
|
|
295
|
-
hub: {
|
|
296
|
-
...makeSystem('hub', '/hub'),
|
|
297
|
-
content: {
|
|
298
|
-
child: {
|
|
299
|
-
kind: 'schema',
|
|
300
|
-
type: 'stage',
|
|
301
|
-
label: 'Child',
|
|
302
|
-
parentContentId: 'shared-node' // exists in other-system, not hub
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
},
|
|
306
|
-
other: {
|
|
307
|
-
...makeSystem('other', '/other'),
|
|
308
|
-
content: {
|
|
309
|
-
'shared-node': {
|
|
310
|
-
kind: 'schema',
|
|
311
|
-
type: 'pipeline',
|
|
312
|
-
label: 'Shared Node'
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
})
|
|
317
|
-
const messages = getIssueMessages(data)
|
|
318
|
-
expect(messages.some((m) => m.includes('parentContentId'))).toBe(true)
|
|
319
|
-
})
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
// ---------------------------------------------------------------------------
|
|
323
|
-
// B5 — Payload validation for registered (kind, type) pairs
|
|
324
|
-
// ---------------------------------------------------------------------------
|
|
325
|
-
|
|
326
|
-
describe('B5 — payload validation for registered kinds', () => {
|
|
327
|
-
it('POSITIVE: valid schema:pipeline data passes payload validation', () => {
|
|
328
|
-
const data = makeMinimalModel({
|
|
329
|
-
hub: {
|
|
330
|
-
...makeSystem('hub', '/hub'),
|
|
331
|
-
content: {
|
|
332
|
-
pipe: {
|
|
333
|
-
kind: 'schema',
|
|
334
|
-
type: 'pipeline',
|
|
335
|
-
label: 'Pipeline',
|
|
336
|
-
data: { entityId: 'crm.deal' }
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
})
|
|
341
|
-
expect(parseSucceeds(data)).toBe(true)
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
it('NEGATIVE: invalid schema:pipeline data (missing required entityId) produces payload error', () => {
|
|
345
|
-
const data = makeMinimalModel({
|
|
346
|
-
hub: {
|
|
347
|
-
...makeSystem('hub', '/hub'),
|
|
348
|
-
content: {
|
|
349
|
-
pipe: {
|
|
350
|
-
kind: 'schema',
|
|
351
|
-
type: 'pipeline',
|
|
352
|
-
label: 'Pipeline',
|
|
353
|
-
data: { kanbanColor: 'blue' } // entityId is required
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
})
|
|
358
|
-
const messages = getIssueMessages(data)
|
|
359
|
-
expect(messages.some((m) => m.includes('data failed payload validation') || m.includes('payload'))).toBe(true)
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
it('POSITIVE: valid config:kv data passes payload validation', () => {
|
|
363
|
-
const data = makeMinimalModel({
|
|
364
|
-
hub: {
|
|
365
|
-
...makeSystem('hub', '/hub'),
|
|
366
|
-
content: {
|
|
367
|
-
cfg: {
|
|
368
|
-
kind: 'config',
|
|
369
|
-
type: 'kv',
|
|
370
|
-
label: 'Config',
|
|
371
|
-
data: { entries: { flag: true, limit: 100 } }
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
})
|
|
376
|
-
expect(parseSucceeds(data)).toBe(true)
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
it('NEGATIVE: invalid config:kv data (entries contains an object value) produces payload error', () => {
|
|
380
|
-
const data = makeMinimalModel({
|
|
381
|
-
hub: {
|
|
382
|
-
...makeSystem('hub', '/hub'),
|
|
383
|
-
content: {
|
|
384
|
-
cfg: {
|
|
385
|
-
kind: 'config',
|
|
386
|
-
type: 'kv',
|
|
387
|
-
label: 'Config',
|
|
388
|
-
data: { entries: { nested: { key: 'value' } } } // object not allowed, only primitives
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
})
|
|
393
|
-
const messages = getIssueMessages(data)
|
|
394
|
-
expect(messages.some((m) => m.includes('payload') || m.includes('data'))).toBe(true)
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
it('POSITIVE: valid schema:status-flow data passes', () => {
|
|
398
|
-
const data = makeMinimalModel({
|
|
399
|
-
hub: {
|
|
400
|
-
...makeSystem('hub', '/hub'),
|
|
401
|
-
content: {
|
|
402
|
-
flow: {
|
|
403
|
-
kind: 'schema',
|
|
404
|
-
type: 'status-flow',
|
|
405
|
-
label: 'Flow',
|
|
406
|
-
data: { appliesTo: 'project' }
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
})
|
|
411
|
-
expect(parseSucceeds(data)).toBe(true)
|
|
412
|
-
})
|
|
413
|
-
|
|
414
|
-
it('NEGATIVE: invalid schema:status-flow data (bad appliesTo value)', () => {
|
|
415
|
-
const data = makeMinimalModel({
|
|
416
|
-
hub: {
|
|
417
|
-
...makeSystem('hub', '/hub'),
|
|
418
|
-
content: {
|
|
419
|
-
flow: {
|
|
420
|
-
kind: 'schema',
|
|
421
|
-
type: 'status-flow',
|
|
422
|
-
label: 'Flow',
|
|
423
|
-
data: { appliesTo: 'organization' } // not in enum
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
})
|
|
428
|
-
const messages = getIssueMessages(data)
|
|
429
|
-
expect(messages.some((m) => m.includes('payload') || m.includes('data'))).toBe(true)
|
|
430
|
-
})
|
|
431
|
-
})
|
|
432
|
-
|
|
433
|
-
// ---------------------------------------------------------------------------
|
|
434
|
-
// D2 — Unregistered (kind, type) passes through without error
|
|
435
|
-
// ---------------------------------------------------------------------------
|
|
436
|
-
|
|
437
|
-
describe('D2 — unregistered (kind, type) passes through (no error)', () => {
|
|
438
|
-
it('unregistered tenant kind passes schema validation without error', () => {
|
|
439
|
-
const data = makeMinimalModel({
|
|
440
|
-
hub: {
|
|
441
|
-
...makeSystem('hub', '/hub'),
|
|
442
|
-
content: {
|
|
443
|
-
custom: {
|
|
444
|
-
kind: 'tenant',
|
|
445
|
-
type: 'custom-thing',
|
|
446
|
-
label: 'Custom Thing',
|
|
447
|
-
data: { anything: 'goes', nested: { ok: true } }
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
})
|
|
452
|
-
expect(parseSucceeds(data)).toBe(true)
|
|
453
|
-
})
|
|
454
|
-
|
|
455
|
-
it('unregistered kind with invalid-looking data still passes through (no payload schema to validate against)', () => {
|
|
456
|
-
const data = makeMinimalModel({
|
|
457
|
-
hub: {
|
|
458
|
-
...makeSystem('hub', '/hub'),
|
|
459
|
-
content: {
|
|
460
|
-
unknown: {
|
|
461
|
-
kind: 'experimental',
|
|
462
|
-
type: 'prototype',
|
|
463
|
-
label: 'Prototype',
|
|
464
|
-
data: { malformed: [1, 2, 3], extra: null }
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
})
|
|
469
|
-
expect(parseSucceeds(data)).toBe(true)
|
|
470
|
-
})
|
|
471
|
-
|
|
472
|
-
it('unregistered kind with parentContentId that resolves in same system also passes', () => {
|
|
473
|
-
const data = makeMinimalModel({
|
|
474
|
-
hub: {
|
|
475
|
-
...makeSystem('hub', '/hub'),
|
|
476
|
-
content: {
|
|
477
|
-
parent: {
|
|
478
|
-
kind: 'tenant',
|
|
479
|
-
type: 'group',
|
|
480
|
-
label: 'Parent Group'
|
|
481
|
-
},
|
|
482
|
-
child: {
|
|
483
|
-
kind: 'tenant',
|
|
484
|
-
type: 'item',
|
|
485
|
-
label: 'Child Item',
|
|
486
|
-
parentContentId: 'parent'
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
})
|
|
491
|
-
expect(parseSucceeds(data)).toBe(true)
|
|
492
|
-
})
|
|
493
|
-
})
|
|
494
|
-
|
|
495
|
-
// ---------------------------------------------------------------------------
|
|
496
|
-
// L19 — Same-meta-kind parent constraint
|
|
497
|
-
// ---------------------------------------------------------------------------
|
|
498
|
-
|
|
499
|
-
describe('L19 — same-meta-kind parent constraint', () => {
|
|
500
|
-
it('POSITIVE: schema:stage under schema:pipeline shares same kind (schema)', () => {
|
|
501
|
-
const data = makeMinimalModel({
|
|
502
|
-
hub: {
|
|
503
|
-
...makeSystem('hub', '/hub'),
|
|
504
|
-
content: {
|
|
505
|
-
pipe: {
|
|
506
|
-
kind: 'schema',
|
|
507
|
-
type: 'pipeline',
|
|
508
|
-
label: 'Pipeline',
|
|
509
|
-
data: { entityId: 'crm.deal' }
|
|
510
|
-
},
|
|
511
|
-
stage: {
|
|
512
|
-
kind: 'schema',
|
|
513
|
-
type: 'stage',
|
|
514
|
-
label: 'Stage',
|
|
515
|
-
parentContentId: 'pipe',
|
|
516
|
-
data: { semanticClass: 'open' }
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
})
|
|
521
|
-
expect(parseSucceeds(data)).toBe(true)
|
|
522
|
-
})
|
|
523
|
-
|
|
524
|
-
it('NEGATIVE: registered child under registered parent with different meta-kind produces L19 error', () => {
|
|
525
|
-
// schema:stage (meta-kind: schema) trying to parent under config:kv (meta-kind: config)
|
|
526
|
-
const data = makeMinimalModel({
|
|
527
|
-
hub: {
|
|
528
|
-
...makeSystem('hub', '/hub'),
|
|
529
|
-
content: {
|
|
530
|
-
'my-config': {
|
|
531
|
-
kind: 'config',
|
|
532
|
-
type: 'kv',
|
|
533
|
-
label: 'Config',
|
|
534
|
-
data: { entries: { enabled: true } }
|
|
535
|
-
},
|
|
536
|
-
'my-stage': {
|
|
537
|
-
kind: 'schema',
|
|
538
|
-
type: 'stage',
|
|
539
|
-
label: 'Stage',
|
|
540
|
-
parentContentId: 'my-config',
|
|
541
|
-
data: { semanticClass: 'open' }
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
})
|
|
546
|
-
const messages = getIssueMessages(data)
|
|
547
|
-
expect(messages.some((m) => m.includes('same-meta-kind') || m.includes('parentContentId'))).toBe(true)
|
|
548
|
-
})
|
|
549
|
-
})
|