@elevasis/core 0.34.1 → 0.35.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 (33) hide show
  1. package/dist/auth/index.d.ts +74 -2
  2. package/dist/auth/index.js +65 -30
  3. package/dist/index.d.ts +60 -2
  4. package/dist/index.js +52 -1
  5. package/dist/knowledge/index.d.ts +12 -0
  6. package/dist/organization-model/index.d.ts +60 -2
  7. package/dist/organization-model/index.js +52 -1
  8. package/dist/test-utils/index.d.ts +12 -0
  9. package/dist/test-utils/index.js +51 -0
  10. package/package.json +1 -1
  11. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +69 -30
  12. package/src/auth/multi-tenancy/index.ts +29 -26
  13. package/src/auth/multi-tenancy/org-id.test.ts +139 -0
  14. package/src/auth/multi-tenancy/org-id.ts +112 -0
  15. package/src/business/acquisition/api-schemas.test.ts +456 -28
  16. package/src/business/acquisition/ontology-validation.ts +715 -23
  17. package/src/execution/engine/tools/platform/storage/__tests__/storage.test.ts +997 -998
  18. package/src/organization-model/__tests__/domains/systems.test.ts +61 -15
  19. package/src/organization-model/__tests__/domains/topology.test.ts +23 -0
  20. package/src/organization-model/__tests__/schema.test.ts +112 -0
  21. package/src/organization-model/domains/systems.ts +44 -0
  22. package/src/organization-model/domains/topology.ts +18 -1
  23. package/src/organization-model/published.ts +19 -1
  24. package/src/organization-model/schema-refinements.ts +23 -0
  25. package/src/organization-model/types.ts +17 -1
  26. package/src/platform/constants/versions.ts +1 -1
  27. package/src/platform/registry/__tests__/validation.test.ts +254 -15
  28. package/src/platform/registry/index.ts +28 -15
  29. package/src/platform/registry/validation.ts +180 -2
  30. package/src/reference/_generated/contracts.md +44 -0
  31. package/src/supabase/__tests__/helpers.test.ts +92 -51
  32. package/src/supabase/helpers.ts +40 -20
  33. package/src/supabase/index.ts +52 -52
@@ -121,8 +121,15 @@ import {
121
121
  } from './api-schemas'
122
122
  import { createBuildPlanSnapshotFromTemplate } from './build-templates'
123
123
  import {
124
+ compileCrmApiOntologyValidationIndex,
124
125
  compileBusinessOntologyValidationIndex,
126
+ computeInterfaceReadiness,
127
+ compileLeadGenApiOntologyValidationIndex,
128
+ compileLeadGenCrmHandoffOntologyValidationIndex,
129
+ CRM_API_INTERFACE,
130
+ LEAD_GEN_CRM_HANDOFF_INTERFACE,
125
131
  CRM_PIPELINE_CATALOG_ONTOLOGY_ID,
132
+ LEAD_GEN_API_INTERFACE,
126
133
  LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID
127
134
  } from './ontology-validation'
128
135
 
@@ -177,11 +184,142 @@ const DEFAULT_CRM_PRIORITY_RULE_CONFIG: CrmPriorityRuleConfig = {
177
184
  // ---------------------------------------------------------------------------
178
185
  // Minimal test model fixture
179
186
  // ---------------------------------------------------------------------------
180
- // Builds an OrganizationModel that contains the CRM pipeline catalog exactly as
181
- // @repo/elevasis-core authors it in canonicalOrganizationModel (sales.crm system,
182
- // ontology.catalogTypes['sales.crm:catalog/crm.pipeline']). Derived inline from
183
- // CRM_PIPELINE_DEFINITION so packages/core tests have no dependency on @repo/elevasis-core.
184
- function buildMinimalCrmModel(): OrganizationModel {
187
+ // Builds OrganizationModel fixtures inline so packages/core tests have no
188
+ // dependency on @repo/elevasis-core canonical tenant data.
189
+ function buildMinimalLeadGenModel(): OrganizationModel {
190
+ return {
191
+ ...DEFAULT_ORGANIZATION_MODEL,
192
+ systems: {
193
+ sales: {
194
+ id: 'sales',
195
+ label: 'Sales',
196
+ order: 60,
197
+ systems: {
198
+ 'lead-gen': {
199
+ id: 'sales.lead-gen',
200
+ label: 'Lead Gen',
201
+ order: 70,
202
+ apiInterface: {
203
+ lifecycle: 'active',
204
+ readinessProfile: LEAD_GEN_API_INTERFACE.readinessProfile,
205
+ resourceIds: ['lead-gen-api-workflow']
206
+ },
207
+ ontology: {
208
+ objectTypes: {
209
+ 'sales.lead-gen:object/list': {
210
+ id: 'sales.lead-gen:object/list',
211
+ label: 'Lead List',
212
+ ownerSystemId: 'sales.lead-gen'
213
+ },
214
+ 'sales.lead-gen:object/company': {
215
+ id: 'sales.lead-gen:object/company',
216
+ label: 'Lead Company',
217
+ ownerSystemId: 'sales.lead-gen'
218
+ },
219
+ 'sales.lead-gen:object/contact': {
220
+ id: 'sales.lead-gen:object/contact',
221
+ label: 'Lead Contact',
222
+ ownerSystemId: 'sales.lead-gen'
223
+ }
224
+ },
225
+ catalogTypes: {
226
+ 'sales.lead-gen:catalog/build-template': {
227
+ id: 'sales.lead-gen:catalog/build-template',
228
+ label: 'Build Templates',
229
+ ownerSystemId: 'sales.lead-gen',
230
+ kind: 'template',
231
+ appliesTo: 'sales.lead-gen:object/list',
232
+ entries: {
233
+ 'local-services': {
234
+ label: 'Local Services',
235
+ order: 10,
236
+ stepCatalog: 'sales.lead-gen:catalog/template-step.local-services'
237
+ }
238
+ }
239
+ },
240
+ 'sales.lead-gen:catalog/template-step.local-services': {
241
+ id: 'sales.lead-gen:catalog/template-step.local-services',
242
+ label: 'Local Services Steps',
243
+ ownerSystemId: 'sales.lead-gen',
244
+ kind: 'template-step',
245
+ appliesTo: 'sales.lead-gen:object/list',
246
+ entries: {
247
+ 'source-companies': {
248
+ label: 'Source Companies',
249
+ order: 10,
250
+ primaryEntity: 'company',
251
+ outputs: ['company'],
252
+ stageKey: 'populated',
253
+ dependencyMode: 'per-record-eligibility',
254
+ actionKey: 'lead-gen.company.source'
255
+ },
256
+ 'discover-contacts': {
257
+ label: 'Discover Contacts',
258
+ order: 20,
259
+ primaryEntity: 'contact',
260
+ outputs: ['contact'],
261
+ stageKey: 'discovered',
262
+ dependencyMode: 'per-record-eligibility',
263
+ actionKey: 'lead-gen.contact.discover'
264
+ }
265
+ }
266
+ },
267
+ 'sales.lead-gen:catalog/company-stage': {
268
+ id: 'sales.lead-gen:catalog/company-stage',
269
+ label: 'Company Stages',
270
+ ownerSystemId: 'sales.lead-gen',
271
+ kind: 'stage',
272
+ appliesTo: 'sales.lead-gen:object/company',
273
+ entries: {
274
+ populated: { label: 'Populated', description: 'Companies imported.', order: 10 },
275
+ qualified: { label: 'Qualified', description: 'Companies qualified.', order: 20 }
276
+ }
277
+ },
278
+ 'sales.lead-gen:catalog/contact-stage': {
279
+ id: 'sales.lead-gen:catalog/contact-stage',
280
+ label: 'Contact Stages',
281
+ ownerSystemId: 'sales.lead-gen',
282
+ kind: 'stage',
283
+ appliesTo: 'sales.lead-gen:object/contact',
284
+ entries: {
285
+ discovered: { label: 'Discovered', description: 'Contacts found.', order: 10 },
286
+ verified: { label: 'Verified', description: 'Contacts verified.', order: 20 }
287
+ }
288
+ }
289
+ }
290
+ }
291
+ }
292
+ }
293
+ }
294
+ },
295
+ resources: {
296
+ 'lead-gen-api-workflow': {
297
+ id: 'lead-gen-api-workflow',
298
+ kind: 'workflow',
299
+ systemPath: 'sales.lead-gen',
300
+ status: 'active',
301
+ order: 10,
302
+ ontology: {
303
+ actions: ['sales.lead-gen:action/api'],
304
+ primaryAction: 'sales.lead-gen:action/api',
305
+ reads: [
306
+ 'sales.lead-gen:object/list',
307
+ 'sales.lead-gen:object/company',
308
+ 'sales.lead-gen:object/contact'
309
+ ],
310
+ usesCatalogs: [
311
+ 'sales.lead-gen:catalog/build-template',
312
+ 'sales.lead-gen:catalog/template-step.local-services',
313
+ 'sales.lead-gen:catalog/company-stage',
314
+ 'sales.lead-gen:catalog/contact-stage'
315
+ ]
316
+ }
317
+ }
318
+ }
319
+ }
320
+ }
321
+
322
+ function buildMinimalLeadGenAndCrmModel(options: { includeHandoff?: boolean } = {}): OrganizationModel {
185
323
  const crmCatalogEntries = Object.fromEntries(
186
324
  CRM_PIPELINE_DEFINITION.stages.map((stage, idx) => [
187
325
  stage.stageKey,
@@ -194,34 +332,119 @@ function buildMinimalCrmModel(): OrganizationModel {
194
332
  }
195
333
  ])
196
334
  )
197
- return {
198
- ...DEFAULT_ORGANIZATION_MODEL,
199
- systems: {
200
- sales: {
201
- id: 'sales',
202
- label: 'Sales',
203
- order: 60,
204
- systems: {
205
- crm: {
206
- id: 'sales.crm',
335
+ const model = buildMinimalLeadGenModel()
336
+ const salesSystem = model.systems.sales
337
+ if (salesSystem === undefined) throw new Error('Minimal lead-gen fixture is missing sales system')
338
+
339
+ salesSystem.systems = {
340
+ ...(salesSystem.systems ?? {}),
341
+ crm: {
342
+ id: 'sales.crm',
343
+ label: 'CRM',
344
+ order: 80,
345
+ apiInterface: {
346
+ lifecycle: 'active',
347
+ readinessProfile: CRM_API_INTERFACE.readinessProfile,
348
+ resourceIds: ['crm-api-workflow']
349
+ },
350
+ ontology: {
351
+ objectTypes: {
352
+ 'sales.crm:object/deal': {
353
+ id: 'sales.crm:object/deal',
354
+ label: 'Deal',
355
+ ownerSystemId: 'sales.crm'
356
+ }
357
+ },
358
+ catalogTypes: {
359
+ 'sales.crm:catalog/crm.pipeline': {
360
+ id: 'sales.crm:catalog/crm.pipeline',
207
361
  label: 'CRM',
208
- order: 80,
209
- ontology: {
210
- catalogTypes: {
211
- 'sales.crm:catalog/crm.pipeline': {
212
- id: 'sales.crm:catalog/crm.pipeline',
213
- label: 'CRM',
214
- ownerSystemId: 'sales.crm',
215
- kind: 'pipeline',
216
- entries: crmCatalogEntries
217
- }
218
- }
362
+ ownerSystemId: 'sales.crm',
363
+ kind: 'pipeline',
364
+ entries: crmCatalogEntries
365
+ }
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ model.resources = {
372
+ ...model.resources,
373
+ 'crm-api-workflow': {
374
+ id: 'crm-api-workflow',
375
+ kind: 'workflow',
376
+ systemPath: 'sales.crm',
377
+ status: 'active',
378
+ order: 20,
379
+ ontology: {
380
+ actions: ['sales.crm:action/api'],
381
+ primaryAction: 'sales.crm:action/api',
382
+ usesCatalogs: ['sales.crm:catalog/crm.pipeline']
383
+ }
384
+ }
385
+ }
386
+
387
+ if (options.includeHandoff === true) {
388
+ const leadGenSystem = salesSystem.systems['lead-gen']
389
+ if (leadGenSystem === undefined) throw new Error('Minimal fixture is missing lead-gen system')
390
+ model.resources = {
391
+ ...model.resources,
392
+ 'lead-gen-crm-handoff-workflow': {
393
+ id: 'lead-gen-crm-handoff-workflow',
394
+ kind: 'workflow',
395
+ systemPath: 'sales.lead-gen',
396
+ status: 'active',
397
+ order: 30,
398
+ ontology: {
399
+ actions: ['sales.lead-gen:action/handoff'],
400
+ primaryAction: 'sales.lead-gen:action/handoff',
401
+ reads: [
402
+ 'sales.lead-gen:object/list',
403
+ 'sales.lead-gen:object/company',
404
+ 'sales.lead-gen:object/contact'
405
+ ],
406
+ usesCatalogs: [
407
+ 'sales.lead-gen:catalog/build-template',
408
+ 'sales.lead-gen:catalog/template-step.local-services',
409
+ 'sales.lead-gen:catalog/company-stage',
410
+ 'sales.lead-gen:catalog/contact-stage',
411
+ 'sales.crm:catalog/crm.pipeline'
412
+ ]
413
+ }
414
+ }
415
+ }
416
+ model.topology = {
417
+ version: 1,
418
+ relationships: {
419
+ 'lead-gen-crm-handoff': {
420
+ from: { kind: 'resource', id: 'lead-gen-crm-handoff-workflow' },
421
+ kind: 'uses',
422
+ to: { kind: 'resource', id: 'crm-api-workflow' },
423
+ required: true,
424
+ metadata: {
425
+ systemInterfaceGrant: {
426
+ consumer: {
427
+ systemPath: LEAD_GEN_CRM_HANDOFF_INTERFACE.systemPath,
428
+ interfaceKey: LEAD_GEN_CRM_HANDOFF_INTERFACE.interfaceKey
429
+ },
430
+ provider: {
431
+ systemPath: CRM_API_INTERFACE.systemPath,
432
+ interfaceKey: CRM_API_INTERFACE.interfaceKey
433
+ },
434
+ resourceIds: ['lead-gen-crm-handoff-workflow'],
435
+ ontologyIds: ['sales.crm:catalog/crm.pipeline']
219
436
  }
220
437
  }
221
438
  }
222
439
  }
223
440
  }
224
441
  }
442
+
443
+ return model
444
+ }
445
+
446
+ function buildMinimalCrmModel(): OrganizationModel {
447
+ return buildMinimalLeadGenAndCrmModel()
225
448
  }
226
449
 
227
450
  // ---------------------------------------------------------------------------
@@ -276,10 +499,215 @@ describe('DealStageSchema', () => {
276
499
  })
277
500
 
278
501
  // ---------------------------------------------------------------------------
279
- // Ontology validation bridge
502
+ // Ontology validation contracts
280
503
  // ---------------------------------------------------------------------------
281
504
 
282
- describe('business ontology validation bridge', () => {
505
+ describe('business ontology validation contracts', () => {
506
+ it('computes structured readiness for a ready lead-gen API interface without CRM', () => {
507
+ const model = buildMinimalLeadGenModel()
508
+ const readiness = computeInterfaceReadiness(model, LEAD_GEN_API_INTERFACE)
509
+
510
+ expect(readiness.ready).toBe(true)
511
+ expect(readiness.readinessProfile).toBe(LEAD_GEN_API_INTERFACE.readinessProfile)
512
+ expect(readiness.scopedResourceIds).toEqual(['lead-gen-api-workflow'])
513
+ expect(readiness.issues).toEqual([])
514
+ })
515
+
516
+ it('lead-gen API validation passes without CRM when list/company/contact readiness exists', () => {
517
+ const model = buildMinimalLeadGenModel()
518
+ const index = compileLeadGenApiOntologyValidationIndex(model)
519
+
520
+ expect(index.ontology.catalogTypes[CRM_PIPELINE_CATALOG_ONTOLOGY_ID]).toBeUndefined()
521
+ expect(index.ontology.catalogTypes[LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID]).toBeDefined()
522
+ expect(index.leadGenStageCatalog).toBeDefined()
523
+ })
524
+
525
+ it('CRM API validation supplies pipeline readiness without requiring lead-gen contracts', () => {
526
+ const model = buildMinimalCrmModel()
527
+ const index = compileCrmApiOntologyValidationIndex(model)
528
+
529
+ expect(index.crmPipelineCatalog).toBeDefined()
530
+ expect(index.crmPipelineCatalog.entries).toHaveProperty('interested')
531
+ })
532
+
533
+ it('computes structured diagnostics when an interface marker is missing', () => {
534
+ const model = buildMinimalLeadGenAndCrmModel()
535
+ delete model.systems.sales?.systems?.['lead-gen']?.apiInterface
536
+
537
+ const readiness = computeInterfaceReadiness(model, LEAD_GEN_API_INTERFACE)
538
+
539
+ expect(readiness.ready).toBe(false)
540
+ expect(readiness.issues).toMatchObject([{ family: 'SYSTEM_INTERFACE_MISSING', code: 'missing-interface' }])
541
+ })
542
+
543
+ it('computes structured diagnostics when an interface marker is inactive', () => {
544
+ const model = buildMinimalLeadGenModel()
545
+ const apiInterface = model.systems.sales?.systems?.['lead-gen']?.apiInterface
546
+ if (apiInterface === undefined) throw new Error('Minimal fixture is missing lead-gen API interface')
547
+ apiInterface.lifecycle = 'disabled'
548
+
549
+ const readiness = computeInterfaceReadiness(model, LEAD_GEN_API_INTERFACE)
550
+
551
+ expect(readiness.ready).toBe(false)
552
+ expect(readiness.issues).toEqual(
553
+ expect.arrayContaining([expect.objectContaining({ family: 'SYSTEM_INTERFACE_DISABLED', code: 'inactive-interface' })])
554
+ )
555
+ })
556
+
557
+ it('computes structured diagnostics when scoped API resources are absent', () => {
558
+ const model = buildMinimalLeadGenModel()
559
+ const apiInterface = model.systems.sales?.systems?.['lead-gen']?.apiInterface
560
+ if (apiInterface === undefined) throw new Error('Minimal fixture is missing lead-gen API interface')
561
+ apiInterface.resourceIds = []
562
+
563
+ const readiness = computeInterfaceReadiness(model, LEAD_GEN_API_INTERFACE)
564
+
565
+ expect(readiness.ready).toBe(false)
566
+ expect(readiness.issues).toEqual(
567
+ expect.arrayContaining([
568
+ expect.objectContaining({ family: 'SYSTEM_INTERFACE_NOT_READY', code: 'missing-scoped-resources' })
569
+ ])
570
+ )
571
+ })
572
+
573
+ it('computes structured diagnostics when a scoped API resource id is missing', () => {
574
+ const model = buildMinimalLeadGenModel()
575
+ const apiInterface = model.systems.sales?.systems?.['lead-gen']?.apiInterface
576
+ if (apiInterface === undefined) throw new Error('Minimal fixture is missing lead-gen API interface')
577
+ apiInterface.resourceIds = ['missing-lead-gen-api-workflow']
578
+
579
+ const readiness = computeInterfaceReadiness(model, LEAD_GEN_API_INTERFACE)
580
+
581
+ expect(readiness.ready).toBe(false)
582
+ expect(readiness.issues).toEqual(
583
+ expect.arrayContaining([
584
+ expect.objectContaining({
585
+ family: 'SYSTEM_INTERFACE_NOT_READY',
586
+ code: 'missing-resource',
587
+ ref: 'missing-lead-gen-api-workflow',
588
+ path: 'systems.sales.lead-gen.apiInterface.resourceIds.0'
589
+ })
590
+ ])
591
+ )
592
+ })
593
+
594
+ it('computes structured diagnostics when a scoped API resource belongs to another System', () => {
595
+ const model = buildMinimalLeadGenAndCrmModel()
596
+ const apiInterface = model.systems.sales?.systems?.['lead-gen']?.apiInterface
597
+ if (apiInterface === undefined) throw new Error('Minimal fixture is missing lead-gen API interface')
598
+ apiInterface.resourceIds = ['crm-api-workflow']
599
+
600
+ const readiness = computeInterfaceReadiness(model, LEAD_GEN_API_INTERFACE)
601
+
602
+ expect(readiness.ready).toBe(false)
603
+ expect(readiness.issues).toEqual(
604
+ expect.arrayContaining([
605
+ expect.objectContaining({
606
+ family: 'SYSTEM_INTERFACE_INVALID',
607
+ code: 'resource-system-mismatch',
608
+ ref: 'crm-api-workflow',
609
+ path: 'systems.sales.lead-gen.apiInterface.resourceIds.0'
610
+ })
611
+ ])
612
+ )
613
+ })
614
+
615
+ it('computes structured diagnostics when required ontology exists but no scoped resource binds it', () => {
616
+ const model = buildMinimalLeadGenModel()
617
+ const resource = model.resources['lead-gen-api-workflow']
618
+ if (resource === undefined) throw new Error('Minimal fixture is missing lead-gen API resource')
619
+ resource.ontology = {
620
+ ...resource.ontology,
621
+ reads: ['sales.lead-gen:object/list', 'sales.lead-gen:object/company']
622
+ }
623
+
624
+ const readiness = computeInterfaceReadiness(model, LEAD_GEN_API_INTERFACE)
625
+
626
+ expect(readiness.ready).toBe(false)
627
+ expect(readiness.issues).toEqual(
628
+ expect.arrayContaining([
629
+ expect.objectContaining({
630
+ family: 'SYSTEM_INTERFACE_NOT_READY',
631
+ code: 'missing-resource-binding',
632
+ ref: 'sales.lead-gen:object/contact'
633
+ })
634
+ ])
635
+ )
636
+ })
637
+
638
+ it('computes structured diagnostics when required content is empty', () => {
639
+ const model = buildMinimalLeadGenModel()
640
+ const catalog = model.systems.sales?.systems?.['lead-gen']?.ontology?.catalogTypes?.[
641
+ 'sales.lead-gen:catalog/build-template'
642
+ ]
643
+ if (catalog === undefined) throw new Error('Minimal fixture is missing build-template catalog')
644
+ catalog.entries = {}
645
+
646
+ const readiness = computeInterfaceReadiness(model, LEAD_GEN_API_INTERFACE)
647
+
648
+ expect(readiness.ready).toBe(false)
649
+ expect(readiness.issues).toEqual(
650
+ expect.arrayContaining([expect.objectContaining({ family: 'SYSTEM_INTERFACE_NOT_READY', code: 'empty-catalog' })])
651
+ )
652
+ })
653
+
654
+ it('computes structured diagnostics for an unknown readiness profile', () => {
655
+ const model = buildMinimalLeadGenModel()
656
+ const apiInterface = model.systems.sales?.systems?.['lead-gen']?.apiInterface
657
+ if (apiInterface === undefined) throw new Error('Minimal fixture is missing lead-gen API interface')
658
+ apiInterface.readinessProfile = 'sales.lead-gen.unknown'
659
+
660
+ const readiness = computeInterfaceReadiness(model, LEAD_GEN_API_INTERFACE)
661
+
662
+ expect(readiness.ready).toBe(false)
663
+ expect(readiness.issues).toMatchObject([{ family: 'SYSTEM_INTERFACE_INVALID', code: 'unknown-readiness-profile' }])
664
+ })
665
+
666
+ it('lead-gen-to-CRM handoff validation fails when the scoped topology grant is absent', () => {
667
+ const model = buildMinimalLeadGenAndCrmModel({ includeHandoff: true })
668
+ model.topology.relationships = {}
669
+
670
+ expect(() => compileLeadGenCrmHandoffOntologyValidationIndex(model)).toThrow('missing-topology-grant')
671
+ })
672
+
673
+ it('lead-gen-to-CRM handoff readiness reports provider interface failures as bridge diagnostics', () => {
674
+ const model = buildMinimalLeadGenAndCrmModel({ includeHandoff: true })
675
+ const crmInterface = model.systems.sales?.systems?.crm?.apiInterface
676
+ if (crmInterface === undefined) throw new Error('Minimal fixture is missing CRM API interface')
677
+ crmInterface.lifecycle = 'disabled'
678
+
679
+ const readiness = computeInterfaceReadiness(model, LEAD_GEN_CRM_HANDOFF_INTERFACE)
680
+
681
+ expect(readiness.ready).toBe(false)
682
+ expect(readiness.issues).toEqual(
683
+ expect.arrayContaining([
684
+ expect.objectContaining({
685
+ family: 'SYSTEM_BRIDGE_NOT_READY',
686
+ code: 'inactive-interface',
687
+ path: 'systems.sales.crm.apiInterface.lifecycle'
688
+ })
689
+ ])
690
+ )
691
+ })
692
+
693
+ it('lead-gen-to-CRM handoff validation requires the scoped topology grant and CRM pipeline readiness', () => {
694
+ const model = buildMinimalLeadGenAndCrmModel({ includeHandoff: true })
695
+ const index = compileLeadGenCrmHandoffOntologyValidationIndex(model)
696
+
697
+ expect(index.bridgeGrant).toBeDefined()
698
+ expect(index.leadGenStageCatalog).toBeDefined()
699
+ expect(index.crmPipelineCatalog).toBeDefined()
700
+ })
701
+
702
+ it('lead-gen-to-CRM handoff validation fails when CRM pipeline readiness is absent', () => {
703
+ const model = buildMinimalLeadGenAndCrmModel({ includeHandoff: true })
704
+ const crmSystem = model.systems.sales?.systems?.crm
705
+ if (crmSystem?.ontology?.catalogTypes === undefined) throw new Error('Minimal fixture is missing CRM catalog types')
706
+ delete crmSystem.ontology.catalogTypes['sales.crm:catalog/crm.pipeline']
707
+
708
+ expect(() => compileLeadGenCrmHandoffOntologyValidationIndex(model)).toThrow(CRM_PIPELINE_CATALOG_ONTOLOGY_ID)
709
+ })
710
+
283
711
  // compileBusinessOntologyValidationIndex now requires a model arg — no default singleton.
284
712
  // The canonical Elevasis CRM pipeline catalog is model-owned (authored in @repo/elevasis-core
285
713
  // canonicalOrganizationModel at 'sales.crm:catalog/crm.pipeline'). Tests here use a