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