helixevo 0.4.1 → 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.
@@ -16,6 +16,20 @@ function readJsonl<T>(filename: string): T[] {
16
16
  return readFileSync(path, 'utf-8').trim().split('\n').filter(Boolean).map(l => JSON.parse(l))
17
17
  }
18
18
 
19
+ function readOntologyJson<T>(filename: string, fallback: T): T {
20
+ const path = join(SG_DIR, 'ontology', filename)
21
+ if (!existsSync(path)) return fallback
22
+ return JSON.parse(readFileSync(path, 'utf-8'))
23
+ }
24
+
25
+ function readOntologyJsonl<T>(filename: string): T[] {
26
+ const path = join(SG_DIR, 'ontology', filename)
27
+ if (!existsSync(path)) return []
28
+ const raw = readFileSync(path, 'utf-8').trim()
29
+ if (!raw) return []
30
+ return raw.split('\n').filter(Boolean).map((l) => JSON.parse(l))
31
+ }
32
+
19
33
  // ─── Types ──────────────────────────────────────────────────────
20
34
 
21
35
  export type CognitiveRole = 'generalist' | 'specialist' | 'hybrid'
@@ -78,7 +92,7 @@ export interface CanaryEntry {
78
92
  skillSlug: string; version: string; deployedAt: string; expiresAt: string
79
93
  }
80
94
 
81
- export type ArtifactProvenance = 'native-evolution' | 'native-topology' | 'derived-history'
95
+ export type ArtifactProvenance = 'native-evolution' | 'native-topology' | 'native-ontology' | 'derived-history'
82
96
  export type ActivationProvenance = 'capture-derived' | 'project-analysis' | 'derived-failure'
83
97
  export type PressureProvenance = 'failure-native' | 'project-analysis-native' | 'failure-derived' | 'profile-gap-derived'
84
98
 
@@ -174,6 +188,8 @@ export interface PressureRouteRecommendation {
174
188
  governanceMode: GovernanceModeName
175
189
  triggeredBy: PressureRouteTrigger
176
190
  reasons: string[]
191
+ semanticInfluence?: OntologySemanticInfluence
192
+ semanticConceptIds?: string[]
177
193
  }
178
194
 
179
195
  export interface PressureIntervention {
@@ -296,6 +312,8 @@ export interface TopologyReviewDecision {
296
312
  export interface ResolvedTopologyReviewCandidate extends TopologyReviewCandidate {
297
313
  status: TopologyReviewStatus
298
314
  latestDecision?: TopologyReviewDecision
315
+ semanticBindings?: ResolvedOntologyBinding[]
316
+ semanticConceptIds?: string[]
299
317
  lastActivityAt: string
300
318
  }
301
319
 
@@ -387,6 +405,8 @@ export interface ResolvedPressureSignal extends PressureSignal {
387
405
  interventionTypes: PressureInterventionType[]
388
406
  motifIds?: string[]
389
407
  routeRecommendation?: PressureRouteRecommendation
408
+ semanticBindings?: ResolvedOntologyBinding[]
409
+ semanticConceptIds?: string[]
390
410
  lastActivityAt: string
391
411
  addressedAt?: string
392
412
  responseSummary: string
@@ -395,6 +415,8 @@ export interface ResolvedPressureSignal extends PressureSignal {
395
415
  export interface ResolvedPressureMotif extends PressureMotif {
396
416
  lifecycle: PressureLifecycleState
397
417
  interventionTypes: PressureInterventionType[]
418
+ semanticBindings?: ResolvedOntologyBinding[]
419
+ semanticConceptIds?: string[]
398
420
  lastActivityAt: string
399
421
  addressedAt?: string
400
422
  responseSummary: string
@@ -418,6 +440,11 @@ export interface ProjectResponseSummary extends ProjectPressureSummary {
418
440
  recentInterventionTypes: PressureInterventionType[]
419
441
  }
420
442
 
443
+ export interface ResolvedTransferEvent extends TransferEvent {
444
+ semanticBindings?: ResolvedOntologyBinding[]
445
+ semanticConceptIds?: string[]
446
+ }
447
+
421
448
  export interface CoEvolutionDashboardSummary {
422
449
  pressureSignals: {
423
450
  total: number
@@ -480,7 +507,7 @@ export interface CoEvolutionDashboardSummary {
480
507
  promoteQueue: ResolvedPressureMotif[]
481
508
  recentAddressed: ResolvedPressureSignal[]
482
509
  recentInterventions: PressureIntervention[]
483
- recentTransfers: TransferEvent[]
510
+ recentTransfers: ResolvedTransferEvent[]
484
511
  topologyBacklog: ResolvedTopologyReviewCandidate[]
485
512
  recentTopologyDecisions: ResolvedTopologyReviewCandidate[]
486
513
  }
@@ -497,9 +524,128 @@ export interface TopologyDashboardSummary {
497
524
  candidates: ResolvedTopologyReviewCandidate[]
498
525
  }
499
526
 
527
+ export type OntologyConceptKind = 'entity' | 'relation-subtype' | 'capability' | 'pressure-type' | 'topology-motif' | 'health-signal' | 'governance-pattern'
528
+ export type OntologyLayer = 'kernel' | 'extension' | 'frontier'
529
+ export type OntologyReviewDecisionStatus = 'promote' | 'reject' | 'defer'
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'
534
+
535
+ export interface OntologyKernelSnapshot {
536
+ version: string
537
+ updatedAt: string
538
+ pillars: string[]
539
+ layers: string[]
540
+ entityNames: string[]
541
+ relationFamilies: string[]
542
+ mutationOperations: string[]
543
+ invariantIds: string[]
544
+ }
545
+
546
+ export interface OntologyConcept {
547
+ id: string
548
+ name: string
549
+ conceptKind: OntologyConceptKind
550
+ layer: OntologyLayer
551
+ status: 'active' | 'provisional' | 'deprecated'
552
+ createdAt: string
553
+ parentConceptId?: string
554
+ description?: string
555
+ aliases?: string[]
556
+ promotionCriteria?: string[]
557
+ migrationMap?: Record<string, string>
558
+ reviewKey?: string
559
+ lastObservedAt?: string
560
+ sourceSummary?: string
561
+ projectIds?: string[]
562
+ evidenceIds?: string[]
563
+ observationCount?: number
564
+ confidence?: number
565
+ relatedSignalIds?: string[]
566
+ relatedMotifIds?: string[]
567
+ relatedReviewIds?: string[]
568
+ relatedTransferEventIds?: string[]
569
+ derivedFromKinds?: string[]
570
+ }
571
+
572
+ export interface OntologyReviewDecision {
573
+ id: string
574
+ conceptId: string
575
+ decision: OntologyReviewDecisionStatus
576
+ decidedAt: string
577
+ governanceMode: GovernanceModeName
578
+ rationale: string
579
+ decidedBy?: 'operator' | 'system'
580
+ }
581
+
582
+ export interface ResolvedOntologyConcept extends OntologyConcept {
583
+ reviewStatus: OntologyReviewStatus
584
+ latestReview?: OntologyReviewDecision
585
+ lastActivityAt: string
586
+ }
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
+
634
+ export interface OntologyChangeEvent {
635
+ id: string
636
+ timestamp: string
637
+ changeType: 'concept-hypothesized' | 'concept-promoted' | 'concept-rejected' | 'concept-split' | 'concept-merged' | 'concept-deprecated' | 'relation-refined' | 'mapping-migrated' | 'invariant-added' | 'invariant-relaxed'
638
+ conceptIds: string[]
639
+ reason: string
640
+ evidenceIds?: string[]
641
+ proposalId?: string
642
+ approvedBy?: string
643
+ ontologyVersion?: string
644
+ }
645
+
500
646
  export interface OntologyDashboardSummary {
501
647
  specVersion: string
502
- source: 'graph' | 'compat-derived'
648
+ source: 'graph' | 'compat-derived' | 'hybrid-native-derived'
503
649
  skillRoles: Record<CognitiveRole, number>
504
650
  stabilityStates: Record<StabilityState, number>
505
651
  plasticityStates: Record<PlasticityState, number>
@@ -551,6 +697,27 @@ export interface OntologyDashboardSummary {
551
697
  candidate: number
552
698
  rejected: number
553
699
  }
700
+ ontologyLoop: {
701
+ kernelConcepts: number
702
+ extensions: number
703
+ frontier: number
704
+ reviewOpen: number
705
+ promoted: number
706
+ rejected: number
707
+ deferred: number
708
+ deprecated: number
709
+ changeEvents: number
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
+ }
720
+ }
554
721
  governance: {
555
722
  activeMode: GovernanceModeName
556
723
  derivedMode: GovernanceModeName
@@ -590,12 +757,30 @@ export interface OntologyDashboardSummary {
590
757
  }
591
758
  }
592
759
 
760
+ export interface OntologyControlDashboardSummary {
761
+ governance: ActiveGovernanceSummary
762
+ kernel: OntologyKernelSnapshot
763
+ summary: OntologyDashboardSummary['ontologyLoop']
764
+ adoption: OntologyAdoptionSummary
765
+ frontier: ResolvedOntologyConcept[]
766
+ extensions: ResolvedOntologyExtension[]
767
+ recentChanges: OntologyChangeEvent[]
768
+ }
769
+
593
770
  const DASHBOARD_ONTOLOGY_SPEC_VERSION = '0.1.0'
594
771
 
595
772
  function emptyCounts<T extends string>(values: readonly T[]): Record<T, number> {
596
773
  return Object.fromEntries(values.map((value) => [value, 0])) as Record<T, number>
597
774
  }
598
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
+
599
784
  function inferCognitiveRole(node: SkillNode): CognitiveRole {
600
785
  if (node.cognitiveRole) return node.cognitiveRole
601
786
  if (node.layer === 'project' || Boolean(node.project)) return 'specialist'
@@ -679,6 +864,211 @@ export function loadCanaries(): { entries: CanaryEntry[] } {
679
864
  return readJson<{ entries: CanaryEntry[] }>('canary-registry.json', { entries: [] })
680
865
  }
681
866
 
867
+ export function loadOntologyKernelSnapshot(): OntologyKernelSnapshot {
868
+ return readOntologyJson<OntologyKernelSnapshot>('kernel.json', {
869
+ version: DASHBOARD_ONTOLOGY_SPEC_VERSION,
870
+ updatedAt: '',
871
+ pillars: [
872
+ 'Operational World',
873
+ 'Perception / Pressure',
874
+ 'Working Memory / Activation',
875
+ 'Long-Term Memory',
876
+ 'Plasticity / Topology',
877
+ 'Evidence / Immune System',
878
+ 'Executive / Governance',
879
+ 'Metacognition / Health',
880
+ 'Time / Lineage',
881
+ ],
882
+ layers: ['kernel', 'extension', 'frontier'],
883
+ entityNames: [
884
+ 'ProjectRecord',
885
+ 'SessionRecord',
886
+ 'TaskRecord',
887
+ 'FailureRecord',
888
+ 'PressureSignal',
889
+ 'Capability',
890
+ 'SkillNode',
891
+ 'SkillRelation',
892
+ 'ActivationEvent',
893
+ 'MutationProposal',
894
+ 'TopologyChange',
895
+ 'EvidenceArtifact',
896
+ 'EvaluationResult',
897
+ 'TransferEvent',
898
+ 'GovernanceMode',
899
+ 'HealthMetric',
900
+ 'LineageRecord',
901
+ 'OntologyConcept',
902
+ 'OntologyChangeEvent',
903
+ ],
904
+ relationFamilies: [
905
+ 'belongs_to',
906
+ 'derived_from',
907
+ 'replaced_by',
908
+ 'parent_of',
909
+ 'child_of',
910
+ 'specializes',
911
+ 'generalizes_from',
912
+ 'depends_on',
913
+ 'enhances',
914
+ 'conflicts_with',
915
+ 'co_evolves_with',
916
+ 'covers',
917
+ 'activated_for',
918
+ 'suppressed_by',
919
+ 'contributed_to',
920
+ 'competed_with',
921
+ 'triggered_by',
922
+ 'validated_by',
923
+ 'contradicted_by',
924
+ 'survived_in',
925
+ 'requires_review',
926
+ 'proposed_due_to',
927
+ 'benefits',
928
+ 'transferred_to',
929
+ 'refined_by',
930
+ 'promoted_from',
931
+ 'executed_under',
932
+ 'deferred_by',
933
+ 'prioritized_by',
934
+ 'locked_by',
935
+ 'measures',
936
+ 'signals_weakness_in',
937
+ 'suggests_repair_for',
938
+ ],
939
+ mutationOperations: [
940
+ 'create_skill',
941
+ 'edit_skill',
942
+ 'specialize_skill',
943
+ 'generalize_skill',
944
+ 'promote_skill',
945
+ 'demote_skill',
946
+ 'retire_skill',
947
+ 'merge_skills',
948
+ 'split_skill',
949
+ 'rewire_relation',
950
+ 'prune_branch',
951
+ 'consolidate_cluster',
952
+ ],
953
+ invariantIds: [
954
+ 'accepted-change-requires-evidence',
955
+ 'specialist-requires-local-context',
956
+ 'promotion-requires-repeated-evidence',
957
+ 'relation-requires-family-and-confidence',
958
+ 'topology-change-preserves-lineage',
959
+ 'deprecated-state-remains-recoverable',
960
+ 'ontology-promotion-requires-migration',
961
+ 'frontier-concepts-stay-provisional',
962
+ 'transfer-requires-source-and-target',
963
+ 'high-risk-topology-change-requires-validation',
964
+ ],
965
+ })
966
+ }
967
+
968
+ export function loadOntologyExtensions(): OntologyConcept[] {
969
+ return readOntologyJson<OntologyConcept[]>('extensions.json', [])
970
+ .slice()
971
+ .sort((a, b) => (b.lastObservedAt ?? b.createdAt).localeCompare(a.lastObservedAt ?? a.createdAt) || a.name.localeCompare(b.name))
972
+ }
973
+
974
+ export function loadOntologyFrontier(): OntologyConcept[] {
975
+ return readOntologyJson<OntologyConcept[]>('frontier.json', [])
976
+ .slice()
977
+ .sort((a, b) => (b.lastObservedAt ?? b.createdAt).localeCompare(a.lastObservedAt ?? a.createdAt) || a.name.localeCompare(b.name))
978
+ }
979
+
980
+ export function loadOntologyReviewDecisions(): OntologyReviewDecision[] {
981
+ return readOntologyJsonl<OntologyReviewDecision>('reviews.jsonl')
982
+ .slice()
983
+ .sort((a, b) => b.decidedAt.localeCompare(a.decidedAt))
984
+ }
985
+
986
+ export function loadOntologyChangeEvents(): OntologyChangeEvent[] {
987
+ return readOntologyJsonl<OntologyChangeEvent>('change-log.jsonl')
988
+ .slice()
989
+ .sort((a, b) => b.timestamp.localeCompare(a.timestamp))
990
+ }
991
+
992
+ function ontologyStatusRank(status: OntologyReviewStatus): number {
993
+ return status === 'open' ? 4 : status === 'deferred' ? 3 : status === 'rejected' ? 2 : 1
994
+ }
995
+
996
+ export function loadResolvedOntologyFrontierConcepts(): ResolvedOntologyConcept[] {
997
+ const concepts = loadOntologyFrontier()
998
+ const latestByConcept = new Map<string, OntologyReviewDecision>()
999
+ for (const decision of loadOntologyReviewDecisions()) {
1000
+ const existing = latestByConcept.get(decision.conceptId)
1001
+ if (!existing || decision.decidedAt > existing.decidedAt) latestByConcept.set(decision.conceptId, decision)
1002
+ }
1003
+ return concepts
1004
+ .map((concept) => {
1005
+ const latestReview = latestByConcept.get(concept.id)
1006
+ const reviewStatus: OntologyReviewStatus = latestReview?.decision === 'promote'
1007
+ ? 'promoted'
1008
+ : latestReview?.decision === 'reject'
1009
+ ? 'rejected'
1010
+ : latestReview?.decision === 'defer'
1011
+ ? 'deferred'
1012
+ : 'open'
1013
+ return {
1014
+ ...concept,
1015
+ reviewStatus,
1016
+ latestReview,
1017
+ lastActivityAt: maxTimestamp(concept.lastObservedAt, latestReview?.decidedAt, concept.createdAt),
1018
+ }
1019
+ })
1020
+ .sort((a, b) => ontologyStatusRank(b.reviewStatus) - ontologyStatusRank(a.reviewStatus)
1021
+ || (b.confidence ?? 0) - (a.confidence ?? 0)
1022
+ || (b.observationCount ?? 0) - (a.observationCount ?? 0)
1023
+ || b.lastActivityAt.localeCompare(a.lastActivityAt)
1024
+ || a.name.localeCompare(b.name))
1025
+ }
1026
+
1027
+ function ontologyKindCounts(): Partial<Record<OntologyConceptKind, number>> {
1028
+ return {}
1029
+ }
1030
+
1031
+ export function getOntologyReviewSummary() {
1032
+ const kernel = loadOntologyKernelSnapshot()
1033
+ const frontier = loadResolvedOntologyFrontierConcepts()
1034
+ const extensions = loadOntologyExtensions()
1035
+ const resolvedExtensions = loadResolvedOntologyExtensions()
1036
+ const changes = loadOntologyChangeEvents()
1037
+ const adoption = getOntologyAdoptionSummary()
1038
+ const byConceptKind = ontologyKindCounts()
1039
+ for (const concept of [...frontier, ...extensions]) {
1040
+ byConceptKind[concept.conceptKind] = (byConceptKind[concept.conceptKind] ?? 0) + 1
1041
+ }
1042
+ return {
1043
+ kernelConcepts: kernel.entityNames.length,
1044
+ extensions: extensions.length,
1045
+ frontier: frontier.length,
1046
+ reviewOpen: frontier.filter((concept) => concept.reviewStatus === 'open').length,
1047
+ promoted: extensions.filter((concept) => concept.status === 'active').length,
1048
+ rejected: frontier.filter((concept) => concept.reviewStatus === 'rejected').length,
1049
+ deferred: frontier.filter((concept) => concept.reviewStatus === 'deferred').length,
1050
+ deprecated: extensions.filter((concept) => concept.status === 'deprecated').length,
1051
+ changeEvents: changes.length,
1052
+ byConceptKind,
1053
+ adoption,
1054
+ backlog: frontier.filter((concept) => concept.reviewStatus === 'open' || concept.reviewStatus === 'deferred').slice(0, 10),
1055
+ recentChanges: changes.slice(0, 10),
1056
+ recentExtensions: resolvedExtensions.slice(0, 8),
1057
+ }
1058
+ }
1059
+
1060
+ export function loadOntologyControlSummary(): OntologyControlDashboardSummary {
1061
+ return {
1062
+ governance: getActiveGovernanceSummary(),
1063
+ kernel: loadOntologyKernelSnapshot(),
1064
+ summary: getOntologyReviewSummary(),
1065
+ adoption: getOntologyAdoptionSummary(),
1066
+ frontier: loadResolvedOntologyFrontierConcepts(),
1067
+ extensions: loadResolvedOntologyExtensions(),
1068
+ recentChanges: loadOntologyChangeEvents().slice(0, 10),
1069
+ }
1070
+ }
1071
+
682
1072
  export function loadSkillContent(slug: string): string {
683
1073
  const path = join(SG_DIR, 'general', slug, 'SKILL.md')
684
1074
  if (!existsSync(path)) return ''
@@ -827,9 +1217,18 @@ export function loadPressureInterventions(): PressureIntervention[] {
827
1217
  .sort((a, b) => (b.completedAt ?? b.createdAt).localeCompare(a.completedAt ?? a.createdAt))
828
1218
  }
829
1219
 
830
- export function loadTransferEvents(): TransferEvent[] {
1220
+ export function loadTransferEvents(): ResolvedTransferEvent[] {
1221
+ const concepts = activeOntologyExtensions()
831
1222
  return readJsonl<TransferEvent>('transfer-events.jsonl')
832
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
+ })
833
1232
  .sort((a, b) => b.timestamp.localeCompare(a.timestamp))
834
1233
  }
835
1234
 
@@ -887,6 +1286,7 @@ export function loadTopologyReviewDecisions(): TopologyReviewDecision[] {
887
1286
 
888
1287
  export function loadResolvedTopologyReviewCandidates(): ResolvedTopologyReviewCandidate[] {
889
1288
  const candidates = loadStoredTopologyReviewCandidates()
1289
+ const concepts = activeOntologyExtensions()
890
1290
  const latestDecisionByCandidate = new Map<string, TopologyReviewDecision>()
891
1291
  for (const decision of loadTopologyReviewDecisions()) {
892
1292
  const existing = latestDecisionByCandidate.get(decision.candidateId)
@@ -896,10 +1296,18 @@ export function loadResolvedTopologyReviewCandidates(): ResolvedTopologyReviewCa
896
1296
  .map((candidate) => {
897
1297
  const latestDecision = latestDecisionByCandidate.get(candidate.id)
898
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)
899
1305
  return {
900
1306
  ...candidate,
901
1307
  status,
902
1308
  latestDecision,
1309
+ semanticBindings,
1310
+ semanticConceptIds: uniqueStrings(semanticBindings.map((binding) => binding.conceptId)),
903
1311
  lastActivityAt: maxTimestamp(candidate.lastObservedAt, latestDecision?.decidedAt),
904
1312
  } satisfies ResolvedTopologyReviewCandidate
905
1313
  })
@@ -1165,22 +1573,356 @@ export function getActiveGovernanceSummary(): ActiveGovernanceSummary {
1165
1573
  return deriveGovernanceSummary()
1166
1574
  }
1167
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
+
1168
1907
  function buildRouteRecommendation(params: {
1169
1908
  signal: PressureSignal
1170
1909
  relatedSignals: PressureSignal[]
1171
1910
  linkedInterventions: PressureIntervention[]
1172
1911
  governanceMode: GovernanceModeName
1912
+ governanceProfile: GovernanceProfile
1913
+ semanticBindings: ResolvedOntologyBinding[]
1173
1914
  }): PressureRouteRecommendation {
1174
- const { signal, relatedSignals, linkedInterventions, governanceMode } = params
1915
+ const { signal, relatedSignals, linkedInterventions, governanceMode, governanceProfile, semanticBindings } = params
1175
1916
  const projectSpread = new Set(relatedSignals.map((entry) => entry.projectId ?? entry.projectPath).filter(Boolean)).size
1176
1917
  const recurrenceCount = relatedSignals.length
1177
1918
  const activeTypes = [...new Set(linkedInterventions.map((intervention) => intervention.interventionType))]
1178
1919
  const reasons: string[] = []
1920
+ let recommendation: PressureRouteRecommendation
1179
1921
 
1180
1922
  if (activeTypes.length >= 3 && !linkedInterventions.some((intervention) => intervention.status === 'completed' && intervention.impact === 'resolving')) {
1181
1923
  reasons.push('Multiple intervention lanes already touch this pressure region without clear closure.')
1182
1924
  reasons.push('Operator review is safer than blindly piling on another automated response.')
1183
- return {
1925
+ recommendation = {
1184
1926
  route: 'manual-review',
1185
1927
  scope: projectSpread >= 2 ? 'network' : signal.projectId ? 'project' : 'local',
1186
1928
  confidence: 0.58,
@@ -1188,27 +1930,21 @@ function buildRouteRecommendation(params: {
1188
1930
  triggeredBy: 'mixed-signal',
1189
1931
  reasons,
1190
1932
  }
1191
- }
1192
-
1193
- if (projectSpread >= 2 && recurrenceCount >= 2) {
1933
+ } else if (projectSpread >= 2 && recurrenceCount >= 2 && governanceMode !== 'project-critical') {
1194
1934
  reasons.push(`This pressure region repeats across ${projectSpread} projects.`)
1195
1935
  reasons.push('A network-level abstraction can absorb repeated local demand more efficiently than one-off fixes.')
1196
- if (governanceMode !== 'project-critical') {
1197
- return {
1198
- route: 'generalize',
1199
- scope: 'network',
1200
- confidence: Math.min(0.95, 0.7 + Math.min(0.15, (projectSpread - 1) * 0.08)),
1201
- governanceMode,
1202
- triggeredBy: 'recurring-cross-project-gap',
1203
- reasons,
1204
- }
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,
1205
1943
  }
1206
- }
1207
-
1208
- if (signal.projectId && signal.priority === 'high' && signal.suggestedAction === 'specialize') {
1944
+ } else if (signal.projectId && signal.priority === 'high' && signal.suggestedAction === 'specialize') {
1209
1945
  reasons.push('The pressure is high priority and bounded to a known project context.')
1210
1946
  reasons.push('Project-layer adaptation is the shortest truthful path to response.')
1211
- return {
1947
+ recommendation = {
1212
1948
  route: 'specialize',
1213
1949
  scope: 'project',
1214
1950
  confidence: 0.82,
@@ -1216,12 +1952,10 @@ function buildRouteRecommendation(params: {
1216
1952
  triggeredBy: 'high-priority-local-gap',
1217
1953
  reasons,
1218
1954
  }
1219
- }
1220
-
1221
- if (signal.relatedFailureId && signal.severity >= 0.8) {
1955
+ } else if (signal.relatedFailureId && signal.severity >= 0.8) {
1222
1956
  reasons.push('This pressure is tied to a concrete failure with strong corrective evidence.')
1223
1957
  reasons.push('An evolution proposal can test a direct skill-level response against replay and regression evidence.')
1224
- return {
1958
+ recommendation = {
1225
1959
  route: 'evolve',
1226
1960
  scope: signal.projectId ? 'project' : 'local',
1227
1961
  confidence: 0.78,
@@ -1229,12 +1963,10 @@ function buildRouteRecommendation(params: {
1229
1963
  triggeredBy: 'accepted-iteration-pattern',
1230
1964
  reasons,
1231
1965
  }
1232
- }
1233
-
1234
- if (governanceMode === 'exploration' || signal.suggestedAction === 'research' || !signal.projectId) {
1966
+ } else if (governanceMode === 'exploration' || signal.suggestedAction === 'research' || !signal.projectId) {
1235
1967
  reasons.push('The pressure still looks under-specified or capability-oriented.')
1236
1968
  reasons.push('Research is the best way to widen evidence before committing to structural change.')
1237
- return {
1969
+ recommendation = {
1238
1970
  route: 'research',
1239
1971
  scope: signal.projectId ? 'project' : 'local',
1240
1972
  confidence: 0.68,
@@ -1242,12 +1974,10 @@ function buildRouteRecommendation(params: {
1242
1974
  triggeredBy: 'insufficient-evidence',
1243
1975
  reasons,
1244
1976
  }
1245
- }
1246
-
1247
- if (signal.projectId) {
1977
+ } else if (signal.projectId) {
1248
1978
  reasons.push('The pressure is project-scoped and can likely be handled without network-wide promotion yet.')
1249
1979
  reasons.push('Specialization is the most direct bounded intervention available.')
1250
- return {
1980
+ recommendation = {
1251
1981
  route: 'specialize',
1252
1982
  scope: 'project',
1253
1983
  confidence: 0.7,
@@ -1255,18 +1985,25 @@ function buildRouteRecommendation(params: {
1255
1985
  triggeredBy: 'project-bounded-recurrence',
1256
1986
  reasons,
1257
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
+ }
1258
1999
  }
1259
2000
 
1260
- reasons.push('No single route is dominant from the current evidence.')
1261
- reasons.push('Research keeps the loop moving while preserving optionality.')
1262
- return {
1263
- route: 'research',
1264
- scope: 'local',
1265
- confidence: 0.6,
1266
- governanceMode,
1267
- triggeredBy: 'insufficient-evidence',
1268
- reasons,
1269
- }
2001
+ return applyOntologyInfluenceToRoute({
2002
+ recommendation,
2003
+ signal,
2004
+ semanticBindings,
2005
+ governanceProfile,
2006
+ })
1270
2007
  }
1271
2008
 
1272
2009
  function interventionEvidenceAccepted(
@@ -1299,6 +2036,7 @@ export function loadResolvedPressureSignals(): ResolvedPressureSignal[] {
1299
2036
  const history = loadHistory()
1300
2037
  const artifacts = loadEvolutionArtifacts()
1301
2038
  const governance = deriveGovernanceSummary(pressureSignals, interventions)
2039
+ const concepts = activeOntologyExtensions()
1302
2040
  const acceptedProposalIds = new Set(
1303
2041
  history.iterations.flatMap((iteration) => iteration.proposals)
1304
2042
  .filter((proposal) => proposal.outcome === 'accepted')
@@ -1336,11 +2074,14 @@ export function loadResolvedPressureSignals(): ResolvedPressureSignal[] {
1336
2074
  const relatedSignals = signalsByRegion.get(pressureRegionKey(signal)) ?? [signal]
1337
2075
  const projectSpread = new Set(relatedSignals.map((entry) => entry.projectId ?? entry.projectPath).filter(Boolean)).size
1338
2076
  const motifIds = relatedSignals.length >= 2 || projectSpread >= 2 ? [motifIdForSignal(signal)] : []
2077
+ const semanticBindings = matchSignalOntologyBindings(signal, motifIds, concepts)
1339
2078
  const routeRecommendation = buildRouteRecommendation({
1340
2079
  signal,
1341
2080
  relatedSignals,
1342
2081
  linkedInterventions,
1343
2082
  governanceMode: governance.activeMode,
2083
+ governanceProfile: governance.profile,
2084
+ semanticBindings,
1344
2085
  })
1345
2086
 
1346
2087
  const lifecycle: PressureLifecycleState = addressingInterventions.length > 0 || signal.status === 'addressed'
@@ -1354,13 +2095,15 @@ export function loadResolvedPressureSignals(): ResolvedPressureSignal[] {
1354
2095
  const lastActivityAt = [signal.detectedAt, latestInterventionAt].filter(Boolean).sort((a, b) => b.localeCompare(a))[0] ?? signal.detectedAt
1355
2096
  const interventionTypes = [...new Set(linkedInterventions.map((intervention) => intervention.interventionType))]
1356
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(', ')}` : ''
1357
2100
  const responseSummary = lifecycle === 'addressed'
1358
- ? `Addressed by ${interventionTypes.join(', ') || 'linked intervention'} evidence`
2101
+ ? `Addressed by ${interventionTypes.join(', ') || 'linked intervention'} evidence${semanticNote}`
1359
2102
  : lifecycle === 'in-progress'
1360
- ? `Response underway via ${interventionTypes.join(', ')}`
2103
+ ? `Response underway via ${interventionTypes.join(', ')}${semanticNote}`
1361
2104
  : lifecycle === 'stale'
1362
2105
  ? 'Superseded by newer pressure in the same project/capability region'
1363
- : `No linked response yet • recommend ${routeRecommendation.route}`
2106
+ : `No linked response yet • recommend ${routeRecommendation.route}${semanticNote}`
1364
2107
 
1365
2108
  return {
1366
2109
  ...signal,
@@ -1369,6 +2112,8 @@ export function loadResolvedPressureSignals(): ResolvedPressureSignal[] {
1369
2112
  interventionTypes,
1370
2113
  motifIds,
1371
2114
  routeRecommendation,
2115
+ semanticBindings,
2116
+ semanticConceptIds: uniqueStrings(semanticBindings.map((binding) => binding.conceptId)),
1372
2117
  lastActivityAt,
1373
2118
  addressedAt,
1374
2119
  responseSummary,
@@ -1387,6 +2132,7 @@ export function loadPressureMotifs(): ResolvedPressureMotif[] {
1387
2132
  const interventions = loadPressureInterventions()
1388
2133
  const transferEvents = loadTransferEvents()
1389
2134
  const governance = deriveGovernanceSummary()
2135
+ const concepts = activeOntologyExtensions()
1390
2136
  const groups = new Map<string, ResolvedPressureSignal[]>()
1391
2137
 
1392
2138
  for (const signal of resolvedSignals) {
@@ -1443,15 +2189,23 @@ export function loadPressureMotifs(): ResolvedPressureMotif[] {
1443
2189
  const linkedInterventionIds = linkedInterventions.map((intervention) => intervention.id)
1444
2190
  const linkedTransferEventIds = linkedTransferEvents.map((event) => event.id)
1445
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(', ')}` : ''
1446
2200
  const responseSummary = lifecycle === 'addressed'
1447
2201
  ? realizedTransfers.length > 0
1448
- ? 'Promoted into reusable transfer evidence'
1449
- : '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}`
1450
2204
  : lifecycle === 'in-progress'
1451
- ? `Promotion or response underway via ${interventionTypes.join(', ') || 'linked intervention'}`
2205
+ ? `Promotion or response underway via ${interventionTypes.join(', ') || 'linked intervention'}${semanticNote}`
1452
2206
  : lifecycle === 'stale'
1453
2207
  ? 'Recurring pressure has been superseded by newer equivalent signals'
1454
- : `Recurring demand is waiting for ${recommendation.route}`
2208
+ : `Recurring demand is waiting for ${recommendation.route}${semanticNote}`
1455
2209
 
1456
2210
  motifs.push({
1457
2211
  id: motifId,
@@ -1471,6 +2225,8 @@ export function loadPressureMotifs(): ResolvedPressureMotif[] {
1471
2225
  suggestedRoute: recommendation,
1472
2226
  lifecycle,
1473
2227
  interventionTypes,
2228
+ semanticBindings,
2229
+ semanticConceptIds: uniqueStrings(semanticBindings.map((binding) => binding.conceptId)),
1474
2230
  lastActivityAt,
1475
2231
  addressedAt,
1476
2232
  responseSummary,
@@ -1486,6 +2242,126 @@ export function loadPressureMotifs(): ResolvedPressureMotif[] {
1486
2242
  })
1487
2243
  }
1488
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
+
1489
2365
  export function loadCoEvolutionSummary(): CoEvolutionDashboardSummary {
1490
2366
  const graph = loadGraph()
1491
2367
  const pressureSignals = loadPressureSignals()
@@ -1783,6 +2659,7 @@ export function loadOntologySummary(): OntologyDashboardSummary {
1783
2659
  const governance = getActiveGovernanceSummary()
1784
2660
  const topologyReviews = getTopologyReviewSummary()
1785
2661
  const topologyExecution = getTopologyExecutionSummary()
2662
+ const ontologyReview = getOntologyReviewSummary()
1786
2663
  const skillRoles = emptyCounts(['generalist', 'specialist', 'hybrid'] as const)
1787
2664
  const stabilityStates = emptyCounts(['stable', 'adaptive', 'experimental'] as const)
1788
2665
  const plasticityStates = emptyCounts(['consolidated', 'volatile', 'candidate'] as const)
@@ -1815,7 +2692,9 @@ export function loadOntologySummary(): OntologyDashboardSummary {
1815
2692
 
1816
2693
  return {
1817
2694
  specVersion: graph.ontologyVersion ?? DASHBOARD_ONTOLOGY_SPEC_VERSION,
1818
- source: graph.ontologyVersion ? 'graph' : 'compat-derived',
2695
+ source: ontologyReview.frontier > 0 || ontologyReview.extensions > 0 || ontologyReview.changeEvents > 0
2696
+ ? 'hybrid-native-derived'
2697
+ : graph.ontologyVersion ? 'graph' : 'compat-derived',
1819
2698
  skillRoles,
1820
2699
  stabilityStates,
1821
2700
  plasticityStates,
@@ -1832,7 +2711,7 @@ export function loadOntologySummary(): OntologyDashboardSummary {
1832
2711
  evidenceBackedProposals,
1833
2712
  artifacts: {
1834
2713
  total: artifacts.length,
1835
- native: artifacts.filter((artifact) => artifact.provenance === 'native-evolution').length,
2714
+ native: artifacts.filter((artifact) => artifact.provenance === 'native-evolution' || artifact.provenance === 'native-ontology').length,
1836
2715
  derived: artifacts.filter((artifact) => artifact.provenance === 'derived-history').length,
1837
2716
  },
1838
2717
  activationTraces: {
@@ -1873,6 +2752,27 @@ export function loadOntologySummary(): OntologyDashboardSummary {
1873
2752
  candidate: transferEvents.filter((event) => event.status === 'candidate').length,
1874
2753
  rejected: transferEvents.filter((event) => event.status === 'rejected').length,
1875
2754
  },
2755
+ ontologyLoop: {
2756
+ kernelConcepts: ontologyReview.kernelConcepts,
2757
+ extensions: ontologyReview.extensions,
2758
+ frontier: ontologyReview.frontier,
2759
+ reviewOpen: ontologyReview.reviewOpen,
2760
+ promoted: ontologyReview.promoted,
2761
+ rejected: ontologyReview.rejected,
2762
+ deferred: ontologyReview.deferred,
2763
+ deprecated: ontologyReview.deprecated,
2764
+ changeEvents: ontologyReview.changeEvents,
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
+ },
2775
+ },
1876
2776
  governance: {
1877
2777
  activeMode: governance.activeMode,
1878
2778
  derivedMode: governance.derivedMode,