helixevo 0.5.0 → 0.6.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.
@@ -188,6 +188,8 @@ export interface PressureRouteRecommendation {
188
188
  governanceMode: GovernanceModeName
189
189
  triggeredBy: PressureRouteTrigger
190
190
  reasons: string[]
191
+ semanticInfluence?: OntologySemanticInfluence
192
+ semanticConceptIds?: string[]
191
193
  }
192
194
 
193
195
  export interface PressureIntervention {
@@ -310,6 +312,8 @@ export interface TopologyReviewDecision {
310
312
  export interface ResolvedTopologyReviewCandidate extends TopologyReviewCandidate {
311
313
  status: TopologyReviewStatus
312
314
  latestDecision?: TopologyReviewDecision
315
+ semanticBindings?: ResolvedOntologyBinding[]
316
+ semanticConceptIds?: string[]
313
317
  lastActivityAt: string
314
318
  }
315
319
 
@@ -401,6 +405,8 @@ export interface ResolvedPressureSignal extends PressureSignal {
401
405
  interventionTypes: PressureInterventionType[]
402
406
  motifIds?: string[]
403
407
  routeRecommendation?: PressureRouteRecommendation
408
+ semanticBindings?: ResolvedOntologyBinding[]
409
+ semanticConceptIds?: string[]
404
410
  lastActivityAt: string
405
411
  addressedAt?: string
406
412
  responseSummary: string
@@ -409,6 +415,8 @@ export interface ResolvedPressureSignal extends PressureSignal {
409
415
  export interface ResolvedPressureMotif extends PressureMotif {
410
416
  lifecycle: PressureLifecycleState
411
417
  interventionTypes: PressureInterventionType[]
418
+ semanticBindings?: ResolvedOntologyBinding[]
419
+ semanticConceptIds?: string[]
412
420
  lastActivityAt: string
413
421
  addressedAt?: string
414
422
  responseSummary: string
@@ -432,6 +440,11 @@ export interface ProjectResponseSummary extends ProjectPressureSummary {
432
440
  recentInterventionTypes: PressureInterventionType[]
433
441
  }
434
442
 
443
+ export interface ResolvedTransferEvent extends TransferEvent {
444
+ semanticBindings?: ResolvedOntologyBinding[]
445
+ semanticConceptIds?: string[]
446
+ }
447
+
435
448
  export interface CoEvolutionDashboardSummary {
436
449
  pressureSignals: {
437
450
  total: number
@@ -494,7 +507,7 @@ export interface CoEvolutionDashboardSummary {
494
507
  promoteQueue: ResolvedPressureMotif[]
495
508
  recentAddressed: ResolvedPressureSignal[]
496
509
  recentInterventions: PressureIntervention[]
497
- recentTransfers: TransferEvent[]
510
+ recentTransfers: ResolvedTransferEvent[]
498
511
  topologyBacklog: ResolvedTopologyReviewCandidate[]
499
512
  recentTopologyDecisions: ResolvedTopologyReviewCandidate[]
500
513
  }
@@ -515,6 +528,9 @@ export type OntologyConceptKind = 'entity' | 'relation-subtype' | 'capability' |
515
528
  export type OntologyLayer = 'kernel' | 'extension' | 'frontier'
516
529
  export type OntologyReviewDecisionStatus = 'promote' | 'reject' | 'defer'
517
530
  export type OntologyReviewStatus = 'open' | 'promoted' | 'rejected' | 'deferred'
531
+ export type OntologyBindingTargetType = 'pressure-signal' | 'pressure-motif' | 'route-recommendation' | 'transfer-event' | 'topology-review'
532
+ export type OntologyBindingSourceKind = 'capability-match' | 'pressure-region' | 'motif-match' | 'transfer-motif' | 'topology-descriptor'
533
+ export type OntologySemanticInfluence = 'none' | 'explanatory' | 'weighted'
518
534
 
519
535
  export interface OntologyKernelSnapshot {
520
536
  version: string
@@ -569,6 +585,52 @@ export interface ResolvedOntologyConcept extends OntologyConcept {
569
585
  lastActivityAt: string
570
586
  }
571
587
 
588
+ export interface ResolvedOntologyBinding {
589
+ id: string
590
+ conceptId: string
591
+ conceptName: string
592
+ conceptKind: OntologyConceptKind
593
+ targetType: OntologyBindingTargetType
594
+ targetId: string
595
+ sourceKind: OntologyBindingSourceKind
596
+ confidence: number
597
+ reasons: string[]
598
+ effectSummary?: string
599
+ }
600
+
601
+ export interface OntologyConsumerSummary {
602
+ conceptId: string
603
+ conceptName: string
604
+ conceptKind: OntologyConceptKind
605
+ status: OntologyConcept['status']
606
+ totalBindings: number
607
+ bindingsByTargetType: Partial<Record<OntologyBindingTargetType, number>>
608
+ activeTargetIds: string[]
609
+ lastActivityAt?: string
610
+ warning?: string
611
+ }
612
+
613
+ export interface ResolvedOntologyExtension extends OntologyConcept {
614
+ semanticBindings: ResolvedOntologyBinding[]
615
+ adoptionCount: number
616
+ bindingsByTargetType: Partial<Record<OntologyBindingTargetType, number>>
617
+ lastActivityAt: string
618
+ warning?: string
619
+ }
620
+
621
+ export interface OntologyAdoptionSummary {
622
+ activeConcepts: number
623
+ unusedExtensions: number
624
+ totalBindings: number
625
+ routesInfluenced: number
626
+ conceptsWithConsumers: number
627
+ conceptsAtDeprecationRisk: number
628
+ bindingsByTargetType: Partial<Record<OntologyBindingTargetType, number>>
629
+ usageByConceptKind: Partial<Record<OntologyConceptKind, number>>
630
+ topActiveConcepts: OntologyConsumerSummary[]
631
+ atRiskConcepts: OntologyConsumerSummary[]
632
+ }
633
+
572
634
  export interface OntologyChangeEvent {
573
635
  id: string
574
636
  timestamp: string
@@ -646,6 +708,15 @@ export interface OntologyDashboardSummary {
646
708
  deprecated: number
647
709
  changeEvents: number
648
710
  byConceptKind: Partial<Record<OntologyConceptKind, number>>
711
+ adoption: {
712
+ activeConcepts: number
713
+ unusedExtensions: number
714
+ totalBindings: number
715
+ routesInfluenced: number
716
+ conceptsAtDeprecationRisk: number
717
+ bindingsByTargetType: Partial<Record<OntologyBindingTargetType, number>>
718
+ usageByConceptKind: Partial<Record<OntologyConceptKind, number>>
719
+ }
649
720
  }
650
721
  governance: {
651
722
  activeMode: GovernanceModeName
@@ -690,8 +761,9 @@ export interface OntologyControlDashboardSummary {
690
761
  governance: ActiveGovernanceSummary
691
762
  kernel: OntologyKernelSnapshot
692
763
  summary: OntologyDashboardSummary['ontologyLoop']
764
+ adoption: OntologyAdoptionSummary
693
765
  frontier: ResolvedOntologyConcept[]
694
- extensions: OntologyConcept[]
766
+ extensions: ResolvedOntologyExtension[]
695
767
  recentChanges: OntologyChangeEvent[]
696
768
  }
697
769
 
@@ -701,6 +773,14 @@ function emptyCounts<T extends string>(values: readonly T[]): Record<T, number>
701
773
  return Object.fromEntries(values.map((value) => [value, 0])) as Record<T, number>
702
774
  }
703
775
 
776
+ function uniqueStrings(values: Array<string | undefined | null>): string[] {
777
+ return [...new Set(values.filter((value): value is string => Boolean(value && value.trim())))]
778
+ }
779
+
780
+ function slugFragment(value: string): string {
781
+ return value.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').slice(0, 80)
782
+ }
783
+
704
784
  function inferCognitiveRole(node: SkillNode): CognitiveRole {
705
785
  if (node.cognitiveRole) return node.cognitiveRole
706
786
  if (node.layer === 'project' || Boolean(node.project)) return 'specialist'
@@ -952,7 +1032,9 @@ export function getOntologyReviewSummary() {
952
1032
  const kernel = loadOntologyKernelSnapshot()
953
1033
  const frontier = loadResolvedOntologyFrontierConcepts()
954
1034
  const extensions = loadOntologyExtensions()
1035
+ const resolvedExtensions = loadResolvedOntologyExtensions()
955
1036
  const changes = loadOntologyChangeEvents()
1037
+ const adoption = getOntologyAdoptionSummary()
956
1038
  const byConceptKind = ontologyKindCounts()
957
1039
  for (const concept of [...frontier, ...extensions]) {
958
1040
  byConceptKind[concept.conceptKind] = (byConceptKind[concept.conceptKind] ?? 0) + 1
@@ -968,9 +1050,10 @@ export function getOntologyReviewSummary() {
968
1050
  deprecated: extensions.filter((concept) => concept.status === 'deprecated').length,
969
1051
  changeEvents: changes.length,
970
1052
  byConceptKind,
1053
+ adoption,
971
1054
  backlog: frontier.filter((concept) => concept.reviewStatus === 'open' || concept.reviewStatus === 'deferred').slice(0, 10),
972
1055
  recentChanges: changes.slice(0, 10),
973
- recentExtensions: extensions.slice(0, 8),
1056
+ recentExtensions: resolvedExtensions.slice(0, 8),
974
1057
  }
975
1058
  }
976
1059
 
@@ -979,8 +1062,9 @@ export function loadOntologyControlSummary(): OntologyControlDashboardSummary {
979
1062
  governance: getActiveGovernanceSummary(),
980
1063
  kernel: loadOntologyKernelSnapshot(),
981
1064
  summary: getOntologyReviewSummary(),
1065
+ adoption: getOntologyAdoptionSummary(),
982
1066
  frontier: loadResolvedOntologyFrontierConcepts(),
983
- extensions: loadOntologyExtensions(),
1067
+ extensions: loadResolvedOntologyExtensions(),
984
1068
  recentChanges: loadOntologyChangeEvents().slice(0, 10),
985
1069
  }
986
1070
  }
@@ -1133,9 +1217,18 @@ export function loadPressureInterventions(): PressureIntervention[] {
1133
1217
  .sort((a, b) => (b.completedAt ?? b.createdAt).localeCompare(a.completedAt ?? a.createdAt))
1134
1218
  }
1135
1219
 
1136
- export function loadTransferEvents(): TransferEvent[] {
1220
+ export function loadTransferEvents(): ResolvedTransferEvent[] {
1221
+ const concepts = activeOntologyExtensions()
1137
1222
  return readJsonl<TransferEvent>('transfer-events.jsonl')
1138
1223
  .slice()
1224
+ .map((event) => {
1225
+ const semanticBindings = matchTransferOntologyBindings(event, concepts)
1226
+ return {
1227
+ ...event,
1228
+ semanticBindings,
1229
+ semanticConceptIds: uniqueStrings(semanticBindings.map((binding) => binding.conceptId)),
1230
+ } satisfies ResolvedTransferEvent
1231
+ })
1139
1232
  .sort((a, b) => b.timestamp.localeCompare(a.timestamp))
1140
1233
  }
1141
1234
 
@@ -1193,6 +1286,7 @@ export function loadTopologyReviewDecisions(): TopologyReviewDecision[] {
1193
1286
 
1194
1287
  export function loadResolvedTopologyReviewCandidates(): ResolvedTopologyReviewCandidate[] {
1195
1288
  const candidates = loadStoredTopologyReviewCandidates()
1289
+ const concepts = activeOntologyExtensions()
1196
1290
  const latestDecisionByCandidate = new Map<string, TopologyReviewDecision>()
1197
1291
  for (const decision of loadTopologyReviewDecisions()) {
1198
1292
  const existing = latestDecisionByCandidate.get(decision.candidateId)
@@ -1202,10 +1296,18 @@ export function loadResolvedTopologyReviewCandidates(): ResolvedTopologyReviewCa
1202
1296
  .map((candidate) => {
1203
1297
  const latestDecision = latestDecisionByCandidate.get(candidate.id)
1204
1298
  const status: TopologyReviewStatus = latestDecision?.decision ?? 'open'
1299
+ const semanticBindings = matchTopologyOntologyBindings({
1300
+ ...candidate,
1301
+ status,
1302
+ latestDecision,
1303
+ lastActivityAt: maxTimestamp(candidate.lastObservedAt, latestDecision?.decidedAt),
1304
+ }, concepts)
1205
1305
  return {
1206
1306
  ...candidate,
1207
1307
  status,
1208
1308
  latestDecision,
1309
+ semanticBindings,
1310
+ semanticConceptIds: uniqueStrings(semanticBindings.map((binding) => binding.conceptId)),
1209
1311
  lastActivityAt: maxTimestamp(candidate.lastObservedAt, latestDecision?.decidedAt),
1210
1312
  } satisfies ResolvedTopologyReviewCandidate
1211
1313
  })
@@ -1471,22 +1573,356 @@ export function getActiveGovernanceSummary(): ActiveGovernanceSummary {
1471
1573
  return deriveGovernanceSummary()
1472
1574
  }
1473
1575
 
1576
+ function emptyOntologyBindingCounts(): Partial<Record<OntologyBindingTargetType, number>> {
1577
+ return {}
1578
+ }
1579
+
1580
+ function ontologyReviewKeyBody(reviewKey?: string): string {
1581
+ return normalizePressureValue((reviewKey ?? '').split(':').slice(1).join('-'))
1582
+ }
1583
+
1584
+ function ontologyMotifTerm(motifId: string): string {
1585
+ return normalizePressureValue(motifId.replace(/^motif_/, ''))
1586
+ }
1587
+
1588
+ function ontologyConceptTerms(concept: OntologyConcept): string[] {
1589
+ return uniqueStrings([
1590
+ normalizePressureValue(concept.name),
1591
+ ...((concept.aliases ?? []).map((alias) => normalizePressureValue(alias))),
1592
+ normalizePressureValue(concept.reviewKey),
1593
+ ontologyReviewKeyBody(concept.reviewKey),
1594
+ ...((concept.relatedMotifIds ?? []).map((id) => ontologyMotifTerm(id))),
1595
+ ...((concept.derivedFromKinds ?? []).map((value) => normalizePressureValue(value))),
1596
+ ].filter(Boolean))
1597
+ }
1598
+
1599
+ function ontologyBindingId(conceptId: string, targetType: OntologyBindingTargetType, targetId: string, sourceKind: OntologyBindingSourceKind): string {
1600
+ return `ontology_binding_${slugFragment(conceptId)}_${targetType}_${slugFragment(targetId)}_${sourceKind}`
1601
+ }
1602
+
1603
+ function buildOntologyBinding(params: {
1604
+ concept: OntologyConcept
1605
+ targetType: OntologyBindingTargetType
1606
+ targetId: string
1607
+ sourceKind: OntologyBindingSourceKind
1608
+ confidence: number
1609
+ reasons: string[]
1610
+ effectSummary?: string
1611
+ }): ResolvedOntologyBinding {
1612
+ return {
1613
+ id: ontologyBindingId(params.concept.id, params.targetType, params.targetId, params.sourceKind),
1614
+ conceptId: params.concept.id,
1615
+ conceptName: params.concept.name,
1616
+ conceptKind: params.concept.conceptKind,
1617
+ targetType: params.targetType,
1618
+ targetId: params.targetId,
1619
+ sourceKind: params.sourceKind,
1620
+ confidence: Math.min(0.95, Math.max(0.5, params.confidence)),
1621
+ reasons: params.reasons,
1622
+ effectSummary: params.effectSummary,
1623
+ }
1624
+ }
1625
+
1626
+ function dedupeOntologyBindings(bindings: ResolvedOntologyBinding[]): ResolvedOntologyBinding[] {
1627
+ const byKey = new Map<string, ResolvedOntologyBinding>()
1628
+ for (const binding of bindings) {
1629
+ const key = `${binding.conceptId}::${binding.targetType}::${binding.targetId}`
1630
+ const existing = byKey.get(key)
1631
+ if (!existing || binding.confidence > existing.confidence) byKey.set(key, binding)
1632
+ }
1633
+ return [...byKey.values()]
1634
+ .sort((a, b) => b.confidence - a.confidence || a.conceptName.localeCompare(b.conceptName))
1635
+ .slice(0, 4)
1636
+ }
1637
+
1638
+ function activeOntologyExtensions(): OntologyConcept[] {
1639
+ return loadOntologyExtensions().filter((concept) => concept.status === 'active')
1640
+ }
1641
+
1642
+ function matchSignalOntologyBindings(signal: PressureSignal, motifIds: string[], concepts: OntologyConcept[]): ResolvedOntologyBinding[] {
1643
+ const capability = normalizePressureValue(signal.capability)
1644
+ const region = normalizePressureValue(signal.region)
1645
+ const kind = normalizePressureValue(signal.kind)
1646
+ const motifTerms = uniqueStrings(motifIds.map((id) => ontologyMotifTerm(id)).filter(Boolean))
1647
+ const bindings: ResolvedOntologyBinding[] = []
1648
+
1649
+ for (const concept of concepts) {
1650
+ const terms = ontologyConceptTerms(concept)
1651
+ if (concept.conceptKind === 'capability') {
1652
+ if (capability && terms.includes(capability)) {
1653
+ bindings.push(buildOntologyBinding({
1654
+ concept,
1655
+ targetType: 'pressure-signal',
1656
+ targetId: signal.id,
1657
+ sourceKind: 'capability-match',
1658
+ confidence: 0.86,
1659
+ reasons: [`Signal capability ${signal.capability} matches approved capability concept ${concept.name}.`],
1660
+ effectSummary: 'Sharpens project or capability interpretation for this pressure signal.',
1661
+ }))
1662
+ continue
1663
+ }
1664
+ if (region && terms.includes(region)) {
1665
+ bindings.push(buildOntologyBinding({
1666
+ concept,
1667
+ targetType: 'pressure-signal',
1668
+ targetId: signal.id,
1669
+ sourceKind: 'pressure-region',
1670
+ confidence: 0.74,
1671
+ reasons: [`Signal region ${signal.region} aligns with approved capability concept ${concept.name}.`],
1672
+ effectSummary: 'Adds semantic coverage to a recurring pressure region.',
1673
+ }))
1674
+ }
1675
+ continue
1676
+ }
1677
+
1678
+ if (concept.conceptKind === 'pressure-type') {
1679
+ if ((concept.relatedMotifIds ?? []).some((id) => motifIds.includes(id))) {
1680
+ bindings.push(buildOntologyBinding({
1681
+ concept,
1682
+ targetType: 'pressure-signal',
1683
+ targetId: signal.id,
1684
+ sourceKind: 'motif-match',
1685
+ confidence: 0.9,
1686
+ reasons: [`Signal participates in approved motif family ${concept.name}.`],
1687
+ effectSummary: 'Marks this signal as part of an approved recurring pressure family.',
1688
+ }))
1689
+ continue
1690
+ }
1691
+ if (motifTerms.some((term) => terms.includes(term))) {
1692
+ bindings.push(buildOntologyBinding({
1693
+ concept,
1694
+ targetType: 'pressure-signal',
1695
+ targetId: signal.id,
1696
+ sourceKind: 'motif-match',
1697
+ confidence: 0.84,
1698
+ reasons: [`Signal motif region aligns with approved pressure family ${concept.name}.`],
1699
+ effectSummary: 'Carries an approved recurring pressure family into live signal interpretation.',
1700
+ }))
1701
+ continue
1702
+ }
1703
+ if (kind && terms.includes(kind)) {
1704
+ bindings.push(buildOntologyBinding({
1705
+ concept,
1706
+ targetType: 'pressure-signal',
1707
+ targetId: signal.id,
1708
+ sourceKind: 'pressure-region',
1709
+ confidence: 0.78,
1710
+ reasons: [`Signal kind ${signal.kind} matches approved pressure family ${concept.name}.`],
1711
+ effectSummary: 'Adds semantic identity to recurring pressure of the same kind.',
1712
+ }))
1713
+ }
1714
+ }
1715
+ }
1716
+
1717
+ return dedupeOntologyBindings(bindings)
1718
+ }
1719
+
1720
+ function matchMotifOntologyBindings(motif: Pick<ResolvedPressureMotif, 'id' | 'key' | 'kind' | 'capability'>, concepts: OntologyConcept[]): ResolvedOntologyBinding[] {
1721
+ const capability = normalizePressureValue(motif.capability)
1722
+ const key = normalizePressureValue(motif.key)
1723
+ const kind = normalizePressureValue(motif.kind)
1724
+ const bindings: ResolvedOntologyBinding[] = []
1725
+
1726
+ for (const concept of concepts) {
1727
+ const terms = ontologyConceptTerms(concept)
1728
+ if (concept.conceptKind === 'capability' && capability && terms.includes(capability)) {
1729
+ bindings.push(buildOntologyBinding({
1730
+ concept,
1731
+ targetType: 'pressure-motif',
1732
+ targetId: motif.id,
1733
+ sourceKind: 'capability-match',
1734
+ confidence: 0.84,
1735
+ reasons: [`Motif capability ${motif.capability} matches approved capability concept ${concept.name}.`],
1736
+ effectSummary: 'Makes recurring capability pressure visible as semantic adoption.',
1737
+ }))
1738
+ continue
1739
+ }
1740
+ if (concept.conceptKind === 'pressure-type') {
1741
+ if ((concept.relatedMotifIds ?? []).includes(motif.id)) {
1742
+ bindings.push(buildOntologyBinding({
1743
+ concept,
1744
+ targetType: 'pressure-motif',
1745
+ targetId: motif.id,
1746
+ sourceKind: 'motif-match',
1747
+ confidence: 0.92,
1748
+ reasons: [`Motif ${motif.key} directly matches approved pressure family ${concept.name}.`],
1749
+ effectSummary: 'Confirms this recurring motif is an approved semantic family.',
1750
+ }))
1751
+ continue
1752
+ }
1753
+ if ([key, kind].filter(Boolean).some((value) => terms.includes(value))) {
1754
+ bindings.push(buildOntologyBinding({
1755
+ concept,
1756
+ targetType: 'pressure-motif',
1757
+ targetId: motif.id,
1758
+ sourceKind: 'pressure-region',
1759
+ confidence: 0.86,
1760
+ reasons: [`Motif identity aligns with approved pressure family ${concept.name}.`],
1761
+ effectSummary: 'Carries approved semantic family identity into motif-level routing.',
1762
+ }))
1763
+ }
1764
+ }
1765
+ }
1766
+
1767
+ return dedupeOntologyBindings(bindings)
1768
+ }
1769
+
1770
+ function matchTransferOntologyBindings(event: TransferEvent, concepts: OntologyConcept[]): ResolvedOntologyBinding[] {
1771
+ const capabilityTerms = uniqueStrings((event.capabilityIds ?? []).map((value) => normalizePressureValue(value)).filter(Boolean))
1772
+ const motifTerms = uniqueStrings((event.motifIds ?? []).map((value) => ontologyMotifTerm(value)).filter(Boolean))
1773
+ const bindings: ResolvedOntologyBinding[] = []
1774
+
1775
+ for (const concept of concepts) {
1776
+ const terms = ontologyConceptTerms(concept)
1777
+ if (concept.conceptKind === 'capability' && capabilityTerms.some((value) => terms.includes(value))) {
1778
+ bindings.push(buildOntologyBinding({
1779
+ concept,
1780
+ targetType: 'transfer-event',
1781
+ targetId: event.id,
1782
+ sourceKind: 'capability-match',
1783
+ confidence: 0.82,
1784
+ reasons: [`Transfer capabilities align with approved concept ${concept.name}.`],
1785
+ effectSummary: 'Shows semantic adoption through reusable transfer evidence.',
1786
+ }))
1787
+ continue
1788
+ }
1789
+ if (concept.conceptKind === 'pressure-type') {
1790
+ if ((concept.relatedMotifIds ?? []).some((id) => (event.motifIds ?? []).includes(id))) {
1791
+ bindings.push(buildOntologyBinding({
1792
+ concept,
1793
+ targetType: 'transfer-event',
1794
+ targetId: event.id,
1795
+ sourceKind: 'transfer-motif',
1796
+ confidence: 0.88,
1797
+ reasons: [`Transfer realizes approved recurring family ${concept.name}.`],
1798
+ effectSummary: 'Connects realized transfer evidence to an approved recurring pressure family.',
1799
+ }))
1800
+ continue
1801
+ }
1802
+ if (motifTerms.some((value) => terms.includes(value))) {
1803
+ bindings.push(buildOntologyBinding({
1804
+ concept,
1805
+ targetType: 'transfer-event',
1806
+ targetId: event.id,
1807
+ sourceKind: 'transfer-motif',
1808
+ confidence: 0.8,
1809
+ reasons: [`Transfer motif linkage aligns with approved family ${concept.name}.`],
1810
+ effectSummary: 'Carries approved semantic family identity into realized transfer outcomes.',
1811
+ }))
1812
+ }
1813
+ }
1814
+ }
1815
+
1816
+ return dedupeOntologyBindings(bindings)
1817
+ }
1818
+
1819
+ function topologyMotifDescriptor(candidate: ResolvedTopologyReviewCandidate): string | null {
1820
+ if (candidate.changeType === 'split' && candidate.title.startsWith('Split overloaded skill:')) return 'overloaded-skill-split-pressure'
1821
+ if (candidate.changeType === 'rewire' && candidate.title.startsWith('Resolve conflicting pair:')) return 'conflict-rewire-pressure'
1822
+ if (candidate.changeType === 'promote' && candidate.title.startsWith('Promote recurring motif:')) return 'motif-promotion-pressure'
1823
+ if (candidate.changeType === 'merge' && candidate.title.startsWith('Merge overlap:')) return 'overlap-merge-pressure'
1824
+ if (candidate.changeType === 'consolidate') return 'consolidation-pressure'
1825
+ return null
1826
+ }
1827
+
1828
+ function matchTopologyOntologyBindings(candidate: ResolvedTopologyReviewCandidate, concepts: OntologyConcept[]): ResolvedOntologyBinding[] {
1829
+ const descriptor = normalizePressureValue(topologyMotifDescriptor(candidate) ?? candidate.changeType)
1830
+ const bindings: ResolvedOntologyBinding[] = []
1831
+
1832
+ for (const concept of concepts) {
1833
+ const terms = ontologyConceptTerms(concept)
1834
+ if (concept.conceptKind === 'topology-motif' && descriptor && terms.includes(descriptor)) {
1835
+ bindings.push(buildOntologyBinding({
1836
+ concept,
1837
+ targetType: 'topology-review',
1838
+ targetId: candidate.id,
1839
+ sourceKind: 'topology-descriptor',
1840
+ confidence: 0.86,
1841
+ reasons: [`Topology review descriptor ${descriptor} aligns with approved topology concept ${concept.name}.`],
1842
+ effectSummary: 'Adds semantic identity to recurring structural review patterns.',
1843
+ }))
1844
+ continue
1845
+ }
1846
+ if (concept.conceptKind === 'pressure-type' && (concept.relatedMotifIds ?? []).some((id) => (candidate.relatedMotifIds ?? []).includes(id))) {
1847
+ bindings.push(buildOntologyBinding({
1848
+ concept,
1849
+ targetType: 'topology-review',
1850
+ targetId: candidate.id,
1851
+ sourceKind: 'motif-match',
1852
+ confidence: 0.72,
1853
+ reasons: [`Topology candidate inherits recurring pressure motif semantics from approved family ${concept.name}.`],
1854
+ effectSummary: 'Links structural review back to an approved recurring pressure family.',
1855
+ }))
1856
+ }
1857
+ }
1858
+
1859
+ return dedupeOntologyBindings(bindings)
1860
+ }
1861
+
1862
+ function applyOntologyInfluenceToRoute(params: {
1863
+ recommendation: PressureRouteRecommendation
1864
+ signal: PressureSignal
1865
+ semanticBindings: ResolvedOntologyBinding[]
1866
+ governanceProfile: GovernanceProfile
1867
+ }): PressureRouteRecommendation {
1868
+ const { recommendation, signal, semanticBindings, governanceProfile } = params
1869
+ if (semanticBindings.length === 0) return recommendation
1870
+
1871
+ const conceptIds = uniqueStrings(semanticBindings.map((binding) => binding.conceptId))
1872
+ const conceptNames = uniqueStrings(semanticBindings.map((binding) => binding.conceptName))
1873
+ const hasCapability = semanticBindings.some((binding) => binding.conceptKind === 'capability')
1874
+ const hasPressureType = semanticBindings.some((binding) => binding.conceptKind === 'pressure-type')
1875
+ const hasTopologyMotif = semanticBindings.some((binding) => binding.conceptKind === 'topology-motif')
1876
+ const reasons = [...recommendation.reasons]
1877
+ let confidence = recommendation.confidence
1878
+ let semanticInfluence: OntologySemanticInfluence = 'explanatory'
1879
+
1880
+ if (recommendation.route === 'generalize' && hasPressureType && governanceProfile.riskTolerance >= 0.4 && governanceProfile.reviewThreshold <= 0.82) {
1881
+ confidence = Math.min(0.95, confidence + 0.04)
1882
+ semanticInfluence = 'weighted'
1883
+ reasons.push(`Approved semantic family ${conceptNames.join(', ')} reinforces network-level reuse inside the current governance envelope.`)
1884
+ } else if (recommendation.route === 'specialize' && hasCapability && signal.projectId && governanceProfile.reviewThreshold <= 0.86) {
1885
+ confidence = Math.min(0.95, confidence + 0.03)
1886
+ semanticInfluence = 'weighted'
1887
+ reasons.push(`Approved capability concept ${conceptNames.join(', ')} sharpens project-bounded specialization without widening scope.`)
1888
+ } else if (recommendation.route === 'evolve' && hasPressureType && signal.relatedFailureId) {
1889
+ confidence = Math.min(0.95, confidence + 0.02)
1890
+ semanticInfluence = 'weighted'
1891
+ reasons.push(`Approved semantic family ${conceptNames.join(', ')} sharpens this failure-linked repair lane.`)
1892
+ } else if (recommendation.route === 'manual-review' && (hasTopologyMotif || hasPressureType)) {
1893
+ reasons.push(`Approved semantic family ${conceptNames.join(', ')} confirms the pattern is real, but governance still prefers explicit operator review.`)
1894
+ } else {
1895
+ reasons.push(`Approved semantic family ${conceptNames.join(', ')} is visible here, but current governance/evidence still keeps the route bounded to ${recommendation.route}.`)
1896
+ }
1897
+
1898
+ return {
1899
+ ...recommendation,
1900
+ confidence,
1901
+ reasons,
1902
+ semanticInfluence,
1903
+ semanticConceptIds: conceptIds,
1904
+ }
1905
+ }
1906
+
1474
1907
  function buildRouteRecommendation(params: {
1475
1908
  signal: PressureSignal
1476
1909
  relatedSignals: PressureSignal[]
1477
1910
  linkedInterventions: PressureIntervention[]
1478
1911
  governanceMode: GovernanceModeName
1912
+ governanceProfile: GovernanceProfile
1913
+ semanticBindings: ResolvedOntologyBinding[]
1479
1914
  }): PressureRouteRecommendation {
1480
- const { signal, relatedSignals, linkedInterventions, governanceMode } = params
1915
+ const { signal, relatedSignals, linkedInterventions, governanceMode, governanceProfile, semanticBindings } = params
1481
1916
  const projectSpread = new Set(relatedSignals.map((entry) => entry.projectId ?? entry.projectPath).filter(Boolean)).size
1482
1917
  const recurrenceCount = relatedSignals.length
1483
1918
  const activeTypes = [...new Set(linkedInterventions.map((intervention) => intervention.interventionType))]
1484
1919
  const reasons: string[] = []
1920
+ let recommendation: PressureRouteRecommendation
1485
1921
 
1486
1922
  if (activeTypes.length >= 3 && !linkedInterventions.some((intervention) => intervention.status === 'completed' && intervention.impact === 'resolving')) {
1487
1923
  reasons.push('Multiple intervention lanes already touch this pressure region without clear closure.')
1488
1924
  reasons.push('Operator review is safer than blindly piling on another automated response.')
1489
- return {
1925
+ recommendation = {
1490
1926
  route: 'manual-review',
1491
1927
  scope: projectSpread >= 2 ? 'network' : signal.projectId ? 'project' : 'local',
1492
1928
  confidence: 0.58,
@@ -1494,27 +1930,21 @@ function buildRouteRecommendation(params: {
1494
1930
  triggeredBy: 'mixed-signal',
1495
1931
  reasons,
1496
1932
  }
1497
- }
1498
-
1499
- if (projectSpread >= 2 && recurrenceCount >= 2) {
1933
+ } else if (projectSpread >= 2 && recurrenceCount >= 2 && governanceMode !== 'project-critical') {
1500
1934
  reasons.push(`This pressure region repeats across ${projectSpread} projects.`)
1501
1935
  reasons.push('A network-level abstraction can absorb repeated local demand more efficiently than one-off fixes.')
1502
- if (governanceMode !== 'project-critical') {
1503
- return {
1504
- route: 'generalize',
1505
- scope: 'network',
1506
- confidence: Math.min(0.95, 0.7 + Math.min(0.15, (projectSpread - 1) * 0.08)),
1507
- governanceMode,
1508
- triggeredBy: 'recurring-cross-project-gap',
1509
- reasons,
1510
- }
1936
+ recommendation = {
1937
+ route: 'generalize',
1938
+ scope: 'network',
1939
+ confidence: Math.min(0.95, 0.7 + Math.min(0.15, (projectSpread - 1) * 0.08)),
1940
+ governanceMode,
1941
+ triggeredBy: 'recurring-cross-project-gap',
1942
+ reasons,
1511
1943
  }
1512
- }
1513
-
1514
- if (signal.projectId && signal.priority === 'high' && signal.suggestedAction === 'specialize') {
1944
+ } else if (signal.projectId && signal.priority === 'high' && signal.suggestedAction === 'specialize') {
1515
1945
  reasons.push('The pressure is high priority and bounded to a known project context.')
1516
1946
  reasons.push('Project-layer adaptation is the shortest truthful path to response.')
1517
- return {
1947
+ recommendation = {
1518
1948
  route: 'specialize',
1519
1949
  scope: 'project',
1520
1950
  confidence: 0.82,
@@ -1522,12 +1952,10 @@ function buildRouteRecommendation(params: {
1522
1952
  triggeredBy: 'high-priority-local-gap',
1523
1953
  reasons,
1524
1954
  }
1525
- }
1526
-
1527
- if (signal.relatedFailureId && signal.severity >= 0.8) {
1955
+ } else if (signal.relatedFailureId && signal.severity >= 0.8) {
1528
1956
  reasons.push('This pressure is tied to a concrete failure with strong corrective evidence.')
1529
1957
  reasons.push('An evolution proposal can test a direct skill-level response against replay and regression evidence.')
1530
- return {
1958
+ recommendation = {
1531
1959
  route: 'evolve',
1532
1960
  scope: signal.projectId ? 'project' : 'local',
1533
1961
  confidence: 0.78,
@@ -1535,12 +1963,10 @@ function buildRouteRecommendation(params: {
1535
1963
  triggeredBy: 'accepted-iteration-pattern',
1536
1964
  reasons,
1537
1965
  }
1538
- }
1539
-
1540
- if (governanceMode === 'exploration' || signal.suggestedAction === 'research' || !signal.projectId) {
1966
+ } else if (governanceMode === 'exploration' || signal.suggestedAction === 'research' || !signal.projectId) {
1541
1967
  reasons.push('The pressure still looks under-specified or capability-oriented.')
1542
1968
  reasons.push('Research is the best way to widen evidence before committing to structural change.')
1543
- return {
1969
+ recommendation = {
1544
1970
  route: 'research',
1545
1971
  scope: signal.projectId ? 'project' : 'local',
1546
1972
  confidence: 0.68,
@@ -1548,12 +1974,10 @@ function buildRouteRecommendation(params: {
1548
1974
  triggeredBy: 'insufficient-evidence',
1549
1975
  reasons,
1550
1976
  }
1551
- }
1552
-
1553
- if (signal.projectId) {
1977
+ } else if (signal.projectId) {
1554
1978
  reasons.push('The pressure is project-scoped and can likely be handled without network-wide promotion yet.')
1555
1979
  reasons.push('Specialization is the most direct bounded intervention available.')
1556
- return {
1980
+ recommendation = {
1557
1981
  route: 'specialize',
1558
1982
  scope: 'project',
1559
1983
  confidence: 0.7,
@@ -1561,18 +1985,25 @@ function buildRouteRecommendation(params: {
1561
1985
  triggeredBy: 'project-bounded-recurrence',
1562
1986
  reasons,
1563
1987
  }
1988
+ } else {
1989
+ reasons.push('No single route is dominant from the current evidence.')
1990
+ reasons.push('Research keeps the loop moving while preserving optionality.')
1991
+ recommendation = {
1992
+ route: 'research',
1993
+ scope: 'local',
1994
+ confidence: 0.6,
1995
+ governanceMode,
1996
+ triggeredBy: 'insufficient-evidence',
1997
+ reasons,
1998
+ }
1564
1999
  }
1565
2000
 
1566
- reasons.push('No single route is dominant from the current evidence.')
1567
- reasons.push('Research keeps the loop moving while preserving optionality.')
1568
- return {
1569
- route: 'research',
1570
- scope: 'local',
1571
- confidence: 0.6,
1572
- governanceMode,
1573
- triggeredBy: 'insufficient-evidence',
1574
- reasons,
1575
- }
2001
+ return applyOntologyInfluenceToRoute({
2002
+ recommendation,
2003
+ signal,
2004
+ semanticBindings,
2005
+ governanceProfile,
2006
+ })
1576
2007
  }
1577
2008
 
1578
2009
  function interventionEvidenceAccepted(
@@ -1605,6 +2036,7 @@ export function loadResolvedPressureSignals(): ResolvedPressureSignal[] {
1605
2036
  const history = loadHistory()
1606
2037
  const artifacts = loadEvolutionArtifacts()
1607
2038
  const governance = deriveGovernanceSummary(pressureSignals, interventions)
2039
+ const concepts = activeOntologyExtensions()
1608
2040
  const acceptedProposalIds = new Set(
1609
2041
  history.iterations.flatMap((iteration) => iteration.proposals)
1610
2042
  .filter((proposal) => proposal.outcome === 'accepted')
@@ -1642,11 +2074,14 @@ export function loadResolvedPressureSignals(): ResolvedPressureSignal[] {
1642
2074
  const relatedSignals = signalsByRegion.get(pressureRegionKey(signal)) ?? [signal]
1643
2075
  const projectSpread = new Set(relatedSignals.map((entry) => entry.projectId ?? entry.projectPath).filter(Boolean)).size
1644
2076
  const motifIds = relatedSignals.length >= 2 || projectSpread >= 2 ? [motifIdForSignal(signal)] : []
2077
+ const semanticBindings = matchSignalOntologyBindings(signal, motifIds, concepts)
1645
2078
  const routeRecommendation = buildRouteRecommendation({
1646
2079
  signal,
1647
2080
  relatedSignals,
1648
2081
  linkedInterventions,
1649
2082
  governanceMode: governance.activeMode,
2083
+ governanceProfile: governance.profile,
2084
+ semanticBindings,
1650
2085
  })
1651
2086
 
1652
2087
  const lifecycle: PressureLifecycleState = addressingInterventions.length > 0 || signal.status === 'addressed'
@@ -1660,13 +2095,15 @@ export function loadResolvedPressureSignals(): ResolvedPressureSignal[] {
1660
2095
  const lastActivityAt = [signal.detectedAt, latestInterventionAt].filter(Boolean).sort((a, b) => b.localeCompare(a))[0] ?? signal.detectedAt
1661
2096
  const interventionTypes = [...new Set(linkedInterventions.map((intervention) => intervention.interventionType))]
1662
2097
  const addressedAt = addressingInterventions.map((intervention) => intervention.completedAt ?? intervention.createdAt).sort((a, b) => b.localeCompare(a))[0]
2098
+ const semanticNames = uniqueStrings(semanticBindings.map((binding) => binding.conceptName))
2099
+ const semanticNote = semanticNames.length > 0 ? ` • semantic family ${semanticNames.join(', ')}` : ''
1663
2100
  const responseSummary = lifecycle === 'addressed'
1664
- ? `Addressed by ${interventionTypes.join(', ') || 'linked intervention'} evidence`
2101
+ ? `Addressed by ${interventionTypes.join(', ') || 'linked intervention'} evidence${semanticNote}`
1665
2102
  : lifecycle === 'in-progress'
1666
- ? `Response underway via ${interventionTypes.join(', ')}`
2103
+ ? `Response underway via ${interventionTypes.join(', ')}${semanticNote}`
1667
2104
  : lifecycle === 'stale'
1668
2105
  ? 'Superseded by newer pressure in the same project/capability region'
1669
- : `No linked response yet • recommend ${routeRecommendation.route}`
2106
+ : `No linked response yet • recommend ${routeRecommendation.route}${semanticNote}`
1670
2107
 
1671
2108
  return {
1672
2109
  ...signal,
@@ -1675,6 +2112,8 @@ export function loadResolvedPressureSignals(): ResolvedPressureSignal[] {
1675
2112
  interventionTypes,
1676
2113
  motifIds,
1677
2114
  routeRecommendation,
2115
+ semanticBindings,
2116
+ semanticConceptIds: uniqueStrings(semanticBindings.map((binding) => binding.conceptId)),
1678
2117
  lastActivityAt,
1679
2118
  addressedAt,
1680
2119
  responseSummary,
@@ -1693,6 +2132,7 @@ export function loadPressureMotifs(): ResolvedPressureMotif[] {
1693
2132
  const interventions = loadPressureInterventions()
1694
2133
  const transferEvents = loadTransferEvents()
1695
2134
  const governance = deriveGovernanceSummary()
2135
+ const concepts = activeOntologyExtensions()
1696
2136
  const groups = new Map<string, ResolvedPressureSignal[]>()
1697
2137
 
1698
2138
  for (const signal of resolvedSignals) {
@@ -1749,15 +2189,23 @@ export function loadPressureMotifs(): ResolvedPressureMotif[] {
1749
2189
  const linkedInterventionIds = linkedInterventions.map((intervention) => intervention.id)
1750
2190
  const linkedTransferEventIds = linkedTransferEvents.map((event) => event.id)
1751
2191
  const highPriorityCount = signals.filter((signal) => signal.priority === 'high').length
2192
+ const semanticBindings = matchMotifOntologyBindings({
2193
+ id: motifId,
2194
+ key: regionKey,
2195
+ kind: signals[0]?.kind ?? regionKey,
2196
+ capability,
2197
+ }, concepts)
2198
+ const semanticNames = uniqueStrings(semanticBindings.map((binding) => binding.conceptName))
2199
+ const semanticNote = semanticNames.length > 0 ? ` • semantic family ${semanticNames.join(', ')}` : ''
1752
2200
  const responseSummary = lifecycle === 'addressed'
1753
2201
  ? realizedTransfers.length > 0
1754
- ? 'Promoted into reusable transfer evidence'
1755
- : 'All linked pressure in this recurring region is addressed'
2202
+ ? `Promoted into reusable transfer evidence${semanticNote}`
2203
+ : `All linked pressure in this recurring region is addressed${semanticNote}`
1756
2204
  : lifecycle === 'in-progress'
1757
- ? `Promotion or response underway via ${interventionTypes.join(', ') || 'linked intervention'}`
2205
+ ? `Promotion or response underway via ${interventionTypes.join(', ') || 'linked intervention'}${semanticNote}`
1758
2206
  : lifecycle === 'stale'
1759
2207
  ? 'Recurring pressure has been superseded by newer equivalent signals'
1760
- : `Recurring demand is waiting for ${recommendation.route}`
2208
+ : `Recurring demand is waiting for ${recommendation.route}${semanticNote}`
1761
2209
 
1762
2210
  motifs.push({
1763
2211
  id: motifId,
@@ -1777,6 +2225,8 @@ export function loadPressureMotifs(): ResolvedPressureMotif[] {
1777
2225
  suggestedRoute: recommendation,
1778
2226
  lifecycle,
1779
2227
  interventionTypes,
2228
+ semanticBindings,
2229
+ semanticConceptIds: uniqueStrings(semanticBindings.map((binding) => binding.conceptId)),
1780
2230
  lastActivityAt,
1781
2231
  addressedAt,
1782
2232
  responseSummary,
@@ -1792,6 +2242,126 @@ export function loadPressureMotifs(): ResolvedPressureMotif[] {
1792
2242
  })
1793
2243
  }
1794
2244
 
2245
+ function collectOntologyAdoptionState(): { summary: OntologyAdoptionSummary, consumerMap: Map<string, OntologyConsumerSummary> } {
2246
+ const extensions = loadOntologyExtensions()
2247
+ const activeExtensions = extensions.filter((concept) => concept.status === 'active')
2248
+ const byConceptKind = ontologyKindCounts()
2249
+ const bindingsByTargetType = emptyOntologyBindingCounts()
2250
+ const consumerMap = new Map<string, OntologyConsumerSummary>()
2251
+ const conceptById = new Map(extensions.map((concept) => [concept.id, concept]))
2252
+
2253
+ function ensureConsumer(concept: OntologyConcept): OntologyConsumerSummary {
2254
+ const existing = consumerMap.get(concept.id)
2255
+ if (existing) return existing
2256
+ const created: OntologyConsumerSummary = {
2257
+ conceptId: concept.id,
2258
+ conceptName: concept.name,
2259
+ conceptKind: concept.conceptKind,
2260
+ status: concept.status,
2261
+ totalBindings: 0,
2262
+ bindingsByTargetType: emptyOntologyBindingCounts(),
2263
+ activeTargetIds: [],
2264
+ lastActivityAt: concept.lastObservedAt ?? concept.createdAt,
2265
+ }
2266
+ consumerMap.set(concept.id, created)
2267
+ return created
2268
+ }
2269
+
2270
+ function registerBinding(binding: ResolvedOntologyBinding) {
2271
+ const concept = conceptById.get(binding.conceptId)
2272
+ if (!concept || concept.status !== 'active') return
2273
+ const consumer = ensureConsumer(concept)
2274
+ consumer.totalBindings += 1
2275
+ consumer.bindingsByTargetType[binding.targetType] = (consumer.bindingsByTargetType[binding.targetType] ?? 0) + 1
2276
+ if (!consumer.activeTargetIds.includes(binding.targetId) && consumer.activeTargetIds.length < 12) {
2277
+ consumer.activeTargetIds.push(binding.targetId)
2278
+ }
2279
+ consumer.lastActivityAt = maxTimestamp(consumer.lastActivityAt, concept.lastObservedAt, concept.createdAt)
2280
+ bindingsByTargetType[binding.targetType] = (bindingsByTargetType[binding.targetType] ?? 0) + 1
2281
+ byConceptKind[concept.conceptKind] = (byConceptKind[concept.conceptKind] ?? 0) + 1
2282
+ }
2283
+
2284
+ const signals = loadResolvedPressureSignals()
2285
+ for (const signal of signals) {
2286
+ for (const binding of signal.semanticBindings ?? []) registerBinding(binding)
2287
+ if (signal.routeRecommendation?.semanticConceptIds?.length) {
2288
+ for (const conceptId of signal.routeRecommendation.semanticConceptIds) {
2289
+ const concept = conceptById.get(conceptId)
2290
+ if (!concept || concept.status !== 'active') continue
2291
+ registerBinding(buildOntologyBinding({
2292
+ concept,
2293
+ targetType: 'route-recommendation',
2294
+ targetId: `${signal.id}:route`,
2295
+ sourceKind: 'pressure-region',
2296
+ confidence: signal.routeRecommendation.semanticInfluence === 'weighted' ? 0.82 : 0.72,
2297
+ reasons: signal.routeRecommendation.reasons.slice(0, 2),
2298
+ effectSummary: `Route recommendation ${signal.routeRecommendation.route} is semantically informed by ${concept.name}.`,
2299
+ }))
2300
+ }
2301
+ }
2302
+ }
2303
+
2304
+ for (const motif of loadPressureMotifs()) {
2305
+ for (const binding of motif.semanticBindings ?? []) registerBinding(binding)
2306
+ }
2307
+
2308
+ for (const event of loadTransferEvents()) {
2309
+ for (const binding of event.semanticBindings ?? []) registerBinding(binding)
2310
+ }
2311
+
2312
+ for (const candidate of loadResolvedTopologyReviewCandidates()) {
2313
+ for (const binding of candidate.semanticBindings ?? []) registerBinding(binding)
2314
+ }
2315
+
2316
+ const activeConsumers = [...consumerMap.values()]
2317
+ .sort((a, b) => b.totalBindings - a.totalBindings || (b.lastActivityAt ?? '').localeCompare(a.lastActivityAt ?? ''))
2318
+
2319
+ const atRiskConcepts = activeConsumers
2320
+ .filter((consumer) => consumer.totalBindings > 0)
2321
+ .map((consumer) => ({
2322
+ ...consumer,
2323
+ warning: `${consumer.totalBindings} active consumer${consumer.totalBindings === 1 ? '' : 's'} would be affected by deprecating ${consumer.conceptName}.`,
2324
+ }))
2325
+
2326
+ return {
2327
+ summary: {
2328
+ activeConcepts: activeConsumers.length,
2329
+ unusedExtensions: activeExtensions.filter((concept) => !consumerMap.has(concept.id)).length,
2330
+ totalBindings: activeConsumers.reduce((sum, consumer) => sum + consumer.totalBindings, 0),
2331
+ routesInfluenced: signals.filter((signal) => signal.routeRecommendation?.semanticInfluence && signal.routeRecommendation.semanticInfluence !== 'none').length,
2332
+ conceptsWithConsumers: activeConsumers.length,
2333
+ conceptsAtDeprecationRisk: atRiskConcepts.length,
2334
+ bindingsByTargetType,
2335
+ usageByConceptKind: byConceptKind,
2336
+ topActiveConcepts: activeConsumers.slice(0, 8),
2337
+ atRiskConcepts: atRiskConcepts.slice(0, 8),
2338
+ },
2339
+ consumerMap: new Map(activeConsumers.map((consumer) => [consumer.conceptId, consumer])),
2340
+ }
2341
+ }
2342
+
2343
+ export function getOntologyAdoptionSummary(): OntologyAdoptionSummary {
2344
+ return collectOntologyAdoptionState().summary
2345
+ }
2346
+
2347
+ export function loadResolvedOntologyExtensions(): ResolvedOntologyExtension[] {
2348
+ const extensions = loadOntologyExtensions()
2349
+ const { consumerMap } = collectOntologyAdoptionState()
2350
+ return extensions
2351
+ .map((concept) => {
2352
+ const consumer = consumerMap.get(concept.id)
2353
+ return {
2354
+ ...concept,
2355
+ semanticBindings: [],
2356
+ adoptionCount: consumer?.totalBindings ?? 0,
2357
+ bindingsByTargetType: consumer?.bindingsByTargetType ?? emptyOntologyBindingCounts(),
2358
+ lastActivityAt: maxTimestamp(concept.lastObservedAt, consumer?.lastActivityAt, concept.createdAt),
2359
+ warning: consumer?.warning,
2360
+ } satisfies ResolvedOntologyExtension
2361
+ })
2362
+ .sort((a, b) => b.adoptionCount - a.adoptionCount || b.lastActivityAt.localeCompare(a.lastActivityAt) || a.name.localeCompare(b.name))
2363
+ }
2364
+
1795
2365
  export function loadCoEvolutionSummary(): CoEvolutionDashboardSummary {
1796
2366
  const graph = loadGraph()
1797
2367
  const pressureSignals = loadPressureSignals()
@@ -2193,6 +2763,15 @@ export function loadOntologySummary(): OntologyDashboardSummary {
2193
2763
  deprecated: ontologyReview.deprecated,
2194
2764
  changeEvents: ontologyReview.changeEvents,
2195
2765
  byConceptKind: ontologyReview.byConceptKind,
2766
+ adoption: {
2767
+ activeConcepts: ontologyReview.adoption.activeConcepts,
2768
+ unusedExtensions: ontologyReview.adoption.unusedExtensions,
2769
+ totalBindings: ontologyReview.adoption.totalBindings,
2770
+ routesInfluenced: ontologyReview.adoption.routesInfluenced,
2771
+ conceptsAtDeprecationRisk: ontologyReview.adoption.conceptsAtDeprecationRisk,
2772
+ bindingsByTargetType: ontologyReview.adoption.bindingsByTargetType,
2773
+ usageByConceptKind: ontologyReview.adoption.usageByConceptKind,
2774
+ },
2196
2775
  },
2197
2776
  governance: {
2198
2777
  activeMode: governance.activeMode,