@elevasis/core 0.37.0 → 0.39.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.
@@ -23,13 +23,15 @@ import {
23
23
  computeInterfaceReadiness,
24
24
  type SystemInterfaceReadinessIssueFamily
25
25
  } from '../../business/acquisition/ontology-validation'
26
+ import { SYSTEM_INTERFACE_PROFILES } from '../../organization-model/domains/systems'
26
27
  import type { OmTopologyNodeRef } from '../../organization-model/domains/topology'
27
28
  import type {
28
29
  TriggerDefinition,
29
30
  ResourceRelationships,
30
31
  ExternalResourceDefinition,
31
32
  HumanCheckpointDefinition,
32
- ResourceType
33
+ ResourceType,
34
+ ApiInterfaceConformanceGap
33
35
  } from './types'
34
36
 
35
37
  // ============================================================================
@@ -612,6 +614,77 @@ function addSystemInterfaceIssue(
612
614
  })
613
615
  }
614
616
 
617
+ /**
618
+ * Detects systems that are API-backed (have resources with non-empty ontology
619
+ * bindings), CAN adopt a platform-cataloged `apiInterface` readinessProfile
620
+ * (system path appears in `SYSTEM_INTERFACE_PROFILES`), but have not declared
621
+ * `apiInterface` in the Organization Model.
622
+ *
623
+ * Originally broad-by-design (flagged any system with ontology actions/writes
624
+ * lacking apiInterface). Tightened in @elevasis/sdk@1.30.1 after the template
625
+ * surfaced the unactionable-paradox case: custom systems (e.g. `sys.operations`,
626
+ * `sys.notifications`) have valid `ontology.actions` bindings but cannot adopt
627
+ * one of the closed-catalog readiness profiles, AND the documented guidance
628
+ * ("Custom Systems should not declare apiInterface") is incompatible with the
629
+ * broad heuristic. The gate now only flags systems where adoption is even
630
+ * possible — i.e. the system path matches a cataloged profile's `systemPath`.
631
+ *
632
+ * A system is flagged when ALL of:
633
+ * (a) `system.apiInterface === undefined`, AND
634
+ * (b) at least one resource in `organizationModel.resources` has
635
+ * `resource.systemPath` matching the system path AND
636
+ * `resource.ontology` non-empty (has `actions` and/or `writes` defined), AND
637
+ * (c) the system path appears as a cataloged `systemPath` in
638
+ * `SYSTEM_INTERFACE_PROFILES` (so adopting a profile is actually possible).
639
+ *
640
+ * Returns `[]` when `organizationModel` is undefined or no gaps are found.
641
+ */
642
+ export function detectMissingApiInterfaceDeclarations(
643
+ orgName: string,
644
+ organizationModel: OrganizationModel | undefined
645
+ ): ApiInterfaceConformanceGap[] {
646
+ if (organizationModel === undefined) return []
647
+
648
+ // Build a map of system path → resource IDs for resources with non-empty ontology.
649
+ const apiBoundResourcesBySystemPath = new Map<string, string[]>()
650
+ for (const resource of Object.values(organizationModel.resources ?? {})) {
651
+ const ontology = resource.ontology
652
+ if (ontology === undefined) continue
653
+ const hasOntologyBindings =
654
+ (ontology.actions !== undefined && ontology.actions.length > 0) ||
655
+ (ontology.writes !== undefined && ontology.writes.length > 0)
656
+ if (!hasOntologyBindings) continue
657
+ const existing = apiBoundResourcesBySystemPath.get(resource.systemPath)
658
+ if (existing) {
659
+ existing.push(resource.id)
660
+ } else {
661
+ apiBoundResourcesBySystemPath.set(resource.systemPath, [resource.id])
662
+ }
663
+ }
664
+
665
+ if (apiBoundResourcesBySystemPath.size === 0) return []
666
+
667
+ // Closed-catalog filter: a system is only a candidate for missing apiInterface
668
+ // if it could actually adopt one of the cataloged platform profiles.
669
+ const catalogedSystemPaths = new Set<string>(SYSTEM_INTERFACE_PROFILES.map((profile) => profile.systemPath))
670
+
671
+ const gaps: ApiInterfaceConformanceGap[] = []
672
+
673
+ for (const { path, system } of listAllSystems(organizationModel)) {
674
+ if (system.apiInterface !== undefined) continue
675
+ if (!catalogedSystemPaths.has(path)) continue
676
+ const resourceIds = apiBoundResourcesBySystemPath.get(path)
677
+ if (!resourceIds || resourceIds.length === 0) continue
678
+ gaps.push({
679
+ systemPath: path,
680
+ resourceIds,
681
+ message: `[${orgName}] System '${path}' has ${resourceIds.length} API-backed resource(s) (${resourceIds.join(', ')}) but no apiInterface declaration.`
682
+ })
683
+ }
684
+
685
+ return gaps
686
+ }
687
+
615
688
  /**
616
689
  * Validates every explicitly declared API System Interface by deriving
617
690
  * readiness from its scoped resources and ontology bindings.