@elevasis/core 0.21.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 (132) hide show
  1. package/dist/index.d.ts +2518 -2169
  2. package/dist/index.js +2495 -1095
  3. package/dist/knowledge/index.d.ts +706 -1044
  4. package/dist/knowledge/index.js +9 -9
  5. package/dist/organization-model/index.d.ts +2518 -2169
  6. package/dist/organization-model/index.js +2495 -1095
  7. package/dist/test-utils/index.d.ts +826 -1014
  8. package/dist/test-utils/index.js +1894 -1032
  9. package/package.json +3 -3
  10. package/src/__tests__/template-core-compatibility.test.ts +11 -79
  11. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +852 -397
  12. package/src/auth/multi-tenancy/permissions.ts +20 -8
  13. package/src/business/README.md +2 -2
  14. package/src/business/acquisition/api-schemas.test.ts +175 -2
  15. package/src/business/acquisition/api-schemas.ts +132 -16
  16. package/src/business/acquisition/build-templates.test.ts +4 -4
  17. package/src/business/acquisition/build-templates.ts +72 -30
  18. package/src/business/acquisition/crm-state-actions.test.ts +13 -11
  19. package/src/business/acquisition/index.ts +12 -0
  20. package/src/business/acquisition/types.ts +7 -3
  21. package/src/business/clients/api-schemas.test.ts +115 -0
  22. package/src/business/clients/api-schemas.ts +158 -0
  23. package/src/business/clients/index.ts +1 -0
  24. package/src/business/deals/api-schemas.ts +8 -0
  25. package/src/business/index.ts +5 -2
  26. package/src/business/projects/types.ts +19 -0
  27. package/src/execution/engine/__tests__/fixtures/test-agents.ts +10 -8
  28. package/src/execution/engine/agent/core/__tests__/agent.test.ts +16 -12
  29. package/src/execution/engine/agent/core/__tests__/error-passthrough.test.ts +4 -3
  30. package/src/execution/engine/agent/core/types.ts +25 -15
  31. package/src/execution/engine/agent/index.ts +6 -4
  32. package/src/execution/engine/agent/reasoning/__tests__/request-builder.test.ts +24 -18
  33. package/src/execution/engine/index.ts +3 -0
  34. package/src/execution/engine/workflow/types.ts +9 -2
  35. package/src/knowledge/README.md +8 -7
  36. package/src/knowledge/__tests__/queries.test.ts +74 -73
  37. package/src/knowledge/format.ts +10 -9
  38. package/src/knowledge/index.ts +1 -1
  39. package/src/knowledge/published.ts +1 -1
  40. package/src/knowledge/queries.ts +26 -25
  41. package/src/organization-model/README.md +73 -26
  42. package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -0
  43. package/src/organization-model/__tests__/defaults.test.ts +76 -96
  44. package/src/organization-model/__tests__/domains/actions.test.ts +56 -0
  45. package/src/organization-model/__tests__/domains/customers.test.ts +299 -295
  46. package/src/organization-model/__tests__/domains/entities.test.ts +56 -0
  47. package/src/organization-model/__tests__/domains/goals.test.ts +493 -479
  48. package/src/organization-model/__tests__/domains/identity.test.ts +280 -279
  49. package/src/organization-model/__tests__/domains/navigation.test.ts +268 -212
  50. package/src/organization-model/__tests__/domains/offerings.test.ts +414 -419
  51. package/src/organization-model/__tests__/domains/policies.test.ts +323 -0
  52. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +271 -271
  53. package/src/organization-model/__tests__/domains/resources.test.ts +310 -0
  54. package/src/organization-model/__tests__/domains/roles.test.ts +463 -347
  55. package/src/organization-model/__tests__/domains/statuses.test.ts +246 -243
  56. package/src/organization-model/__tests__/domains/systems.test.ts +209 -0
  57. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +361 -0
  58. package/src/organization-model/__tests__/foundation.test.ts +74 -102
  59. package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -0
  60. package/src/organization-model/__tests__/graph.test.ts +899 -71
  61. package/src/organization-model/__tests__/knowledge.test.ts +209 -49
  62. package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -0
  63. package/src/organization-model/__tests__/migration-helpers.test.ts +591 -0
  64. package/src/organization-model/__tests__/prospecting-ssot.test.ts +36 -27
  65. package/src/organization-model/__tests__/recursive-system-schema.test.ts +520 -0
  66. package/src/organization-model/__tests__/resolve.test.ts +174 -23
  67. package/src/organization-model/__tests__/schema.test.ts +291 -114
  68. package/src/organization-model/__tests__/surface-projection.test.ts +207 -97
  69. package/src/organization-model/catalogs/lead-gen.ts +144 -0
  70. package/src/organization-model/content-kinds/config.ts +36 -0
  71. package/src/organization-model/content-kinds/index.ts +74 -0
  72. package/src/organization-model/content-kinds/pipeline.ts +68 -0
  73. package/src/organization-model/content-kinds/registry.ts +44 -0
  74. package/src/organization-model/content-kinds/status.ts +71 -0
  75. package/src/organization-model/content-kinds/template.ts +83 -0
  76. package/src/organization-model/content-kinds/types.ts +117 -0
  77. package/src/organization-model/contracts.ts +13 -3
  78. package/src/organization-model/defaults.ts +499 -86
  79. package/src/organization-model/domains/actions.ts +239 -0
  80. package/src/organization-model/domains/customers.ts +78 -75
  81. package/src/organization-model/domains/entities.ts +144 -0
  82. package/src/organization-model/domains/goals.ts +83 -80
  83. package/src/organization-model/domains/knowledge.ts +76 -17
  84. package/src/organization-model/domains/navigation.ts +107 -384
  85. package/src/organization-model/domains/offerings.ts +71 -66
  86. package/src/organization-model/domains/policies.ts +102 -0
  87. package/src/organization-model/domains/projects.ts +14 -48
  88. package/src/organization-model/domains/prospecting.ts +62 -181
  89. package/src/organization-model/domains/resources.ts +145 -0
  90. package/src/organization-model/domains/roles.ts +96 -55
  91. package/src/organization-model/domains/sales.ts +10 -219
  92. package/src/organization-model/domains/shared.ts +57 -57
  93. package/src/organization-model/domains/statuses.ts +339 -130
  94. package/src/organization-model/domains/systems.ts +203 -0
  95. package/src/organization-model/foundation.ts +54 -67
  96. package/src/organization-model/graph/build.ts +682 -54
  97. package/src/organization-model/graph/link.ts +1 -1
  98. package/src/organization-model/graph/schema.ts +24 -9
  99. package/src/organization-model/graph/types.ts +20 -7
  100. package/src/organization-model/helpers.ts +231 -26
  101. package/src/organization-model/icons.ts +1 -0
  102. package/src/organization-model/index.ts +118 -5
  103. package/src/organization-model/migration-helpers.ts +249 -0
  104. package/src/organization-model/organization-graph.mdx +16 -15
  105. package/src/organization-model/organization-model.mdx +111 -44
  106. package/src/organization-model/published.ts +172 -19
  107. package/src/organization-model/resolve.ts +117 -54
  108. package/src/organization-model/schema.ts +654 -112
  109. package/src/organization-model/surface-projection.ts +116 -122
  110. package/src/organization-model/types.ts +146 -20
  111. package/src/platform/api/types.ts +38 -35
  112. package/src/platform/constants/versions.ts +1 -1
  113. package/src/platform/registry/__tests__/command-view.test.ts +6 -8
  114. package/src/platform/registry/__tests__/resource-link.test.ts +13 -8
  115. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +16 -31
  116. package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -0
  117. package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2005
  118. package/src/platform/registry/__tests__/validation.test.ts +1347 -1086
  119. package/src/platform/registry/index.ts +14 -0
  120. package/src/platform/registry/resource-registry.ts +52 -2
  121. package/src/platform/registry/serialization.ts +241 -202
  122. package/src/platform/registry/serialized-types.ts +1 -0
  123. package/src/platform/registry/types.ts +411 -361
  124. package/src/platform/registry/validation.ts +745 -513
  125. package/src/projects/api-schemas.ts +290 -267
  126. package/src/reference/_generated/contracts.md +853 -397
  127. package/src/reference/glossary.md +23 -18
  128. package/src/supabase/database.types.ts +181 -0
  129. package/src/test-utils/test-utils.test.ts +1 -6
  130. package/src/organization-model/__tests__/domains/operations.test.ts +0 -203
  131. package/src/organization-model/domains/features.ts +0 -31
  132. 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
+ })