@elevasis/core 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/index.d.ts +166 -85
  2. package/dist/index.js +146 -1346
  3. package/dist/knowledge/index.d.ts +27 -38
  4. package/dist/knowledge/index.js +1 -1
  5. package/dist/organization-model/index.d.ts +166 -85
  6. package/dist/organization-model/index.js +146 -1346
  7. package/dist/test-utils/index.d.ts +23 -31
  8. package/dist/test-utils/index.js +75 -1238
  9. package/package.json +1 -1
  10. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +14 -2
  11. package/src/business/acquisition/api-schemas.test.ts +70 -77
  12. package/src/business/acquisition/api-schemas.ts +21 -42
  13. package/src/business/acquisition/derive-actions.test.ts +11 -21
  14. package/src/business/acquisition/derive-actions.ts +61 -14
  15. package/src/business/acquisition/ontology-validation.ts +4 -4
  16. package/src/business/acquisition/types.ts +7 -8
  17. package/src/knowledge/queries.ts +0 -1
  18. package/src/organization-model/__tests__/content-kinds-registry.test.ts +35 -210
  19. package/src/organization-model/__tests__/defaults.test.ts +4 -4
  20. package/src/organization-model/__tests__/domains/actions.test.ts +12 -36
  21. package/src/organization-model/__tests__/domains/offerings.test.ts +13 -6
  22. package/src/organization-model/__tests__/domains/resources.test.ts +497 -350
  23. package/src/organization-model/__tests__/domains/systems.test.ts +6 -7
  24. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +68 -80
  25. package/src/organization-model/__tests__/foundation.test.ts +81 -14
  26. package/src/organization-model/__tests__/graph.test.ts +662 -694
  27. package/src/organization-model/__tests__/knowledge.test.ts +31 -17
  28. package/src/organization-model/__tests__/lookup-helpers.test.ts +128 -438
  29. package/src/organization-model/__tests__/migration-helpers.test.ts +362 -591
  30. package/src/organization-model/__tests__/prospecting-ssot.test.ts +68 -103
  31. package/src/organization-model/__tests__/published-zero-leak.test.ts +17 -0
  32. package/src/organization-model/__tests__/recursive-system-schema.test.ts +159 -532
  33. package/src/organization-model/__tests__/resolve.test.ts +79 -42
  34. package/src/organization-model/__tests__/schema.test.ts +65 -56
  35. package/src/organization-model/catalogs/lead-gen.ts +0 -103
  36. package/src/organization-model/defaults.ts +17 -702
  37. package/src/organization-model/domains/actions.ts +116 -333
  38. package/src/organization-model/domains/knowledge.ts +15 -7
  39. package/src/organization-model/domains/projects.ts +4 -4
  40. package/src/organization-model/domains/prospecting.ts +405 -395
  41. package/src/organization-model/domains/resources.ts +206 -135
  42. package/src/organization-model/domains/sales.ts +5 -5
  43. package/src/organization-model/domains/systems.ts +8 -23
  44. package/src/organization-model/graph/build.ts +223 -294
  45. package/src/organization-model/graph/schema.ts +2 -3
  46. package/src/organization-model/graph/types.ts +12 -14
  47. package/src/organization-model/helpers.ts +130 -218
  48. package/src/organization-model/index.ts +104 -124
  49. package/src/organization-model/migration-helpers.ts +211 -249
  50. package/src/organization-model/ontology.ts +0 -60
  51. package/src/organization-model/organization-graph.mdx +4 -5
  52. package/src/organization-model/organization-model.mdx +1 -1
  53. package/src/organization-model/published.ts +236 -226
  54. package/src/organization-model/resolve.ts +4 -5
  55. package/src/organization-model/schema.ts +610 -704
  56. package/src/organization-model/types.ts +167 -161
  57. package/src/platform/registry/__tests__/validation.test.ts +23 -0
  58. package/src/platform/registry/validation.ts +13 -2
  59. package/src/reference/_generated/contracts.md +14 -2
  60. package/src/organization-model/content-kinds/config.ts +0 -36
  61. package/src/organization-model/content-kinds/index.ts +0 -78
  62. package/src/organization-model/content-kinds/pipeline.ts +0 -68
  63. package/src/organization-model/content-kinds/registry.ts +0 -44
  64. package/src/organization-model/content-kinds/status.ts +0 -71
  65. package/src/organization-model/content-kinds/template.ts +0 -83
  66. 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 { resolveOrganizationModel } from '../resolve'
15
- import { getSystem, listAllSystems } from '../helpers'
2
+ import { DEFAULT_ORGANIZATION_MODEL_BRANDING } from '../domains/branding'
16
3
  import { OrganizationModelSchema } from '../schema'
17
- import { DEFAULT_ORGANIZATION_MODEL_BRANDING } from '../domains/branding'
18
-
19
- // Phase 4 (D8): sales, prospecting, projects top-level OM fields removed.
20
- // DEFAULT_ORGANIZATION_MODEL_SALES / _PROSPECTING / _PROJECTS no longer exported.
21
- // makeMinimalModel no longer includes those fields — they were extra props that
22
- // the schema stripped anyway. Removing them has no effect on the test fixtures.
23
-
24
- // ---------------------------------------------------------------------------
25
- // Minimal model factory (shared with schema.test.ts convention)
26
- // ---------------------------------------------------------------------------
27
-
28
- function makeSystem(id: string, path = `/${id.replaceAll('.', '/')}`) {
29
- return {
30
- id,
31
- order: 10,
32
- label: id,
33
- enabled: true,
34
- lifecycle: 'active' as const,
35
- path
36
- }
37
- }
38
-
39
- function makeMinimalModel(systems: Record<string, unknown> = {}, extra: Record<string, unknown> = {}) {
40
- const entityOwnerId = Object.keys(systems)[0] ?? 'dashboard'
41
- return {
42
- version: 1 as const,
43
- branding: DEFAULT_ORGANIZATION_MODEL_BRANDING,
44
- entities: {
45
- 'crm.deal': { id: 'crm.deal', order: 10, label: 'Deal', ownedBySystemId: entityOwnerId },
46
- 'leadgen.list': { id: 'leadgen.list', order: 20, label: 'Lead List', ownedBySystemId: entityOwnerId },
47
- 'leadgen.company': { id: 'leadgen.company', order: 30, label: 'Lead Company', ownedBySystemId: entityOwnerId },
48
- 'leadgen.contact': { id: 'leadgen.contact', order: 40, label: 'Lead Contact', ownedBySystemId: entityOwnerId },
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('parses canonical systems children and keeps subsystems as a compatibility alias', () => {
143
- const model = resolveOrganizationModel({
144
- systems: {
38
+ it('keeps subsystems as a compatibility alias for recursive traversal', () => {
39
+ const model = resolveOrganizationModel(
40
+ minimalModel({
145
41
  parent: {
146
- ...makeSystem('parent', '/parent'),
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
- ...makeSystem('child', '/parent/child'),
95
+ id: 'parent.child',
96
+ order: 20,
97
+ label: 'Child',
150
98
  content: {
151
- pipeline: {
152
- kind: 'schema',
153
- type: 'pipeline',
154
- label: 'Pipeline',
155
- data: { entityId: 'crm.deal' }
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
- expect(model.systems.parent?.systems?.child).toBeDefined()
165
- expect(model.systems.parent?.subsystems?.child).toBeDefined()
166
- expect(getSystem(model, 'parent.child')?.label).toBe('child')
167
- expect(listAllSystems(model).map((entry) => entry.path)).toContain('parent.child')
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
- })