helixevo 0.3.0 → 0.4.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.
@@ -78,14 +78,14 @@ export interface CanaryEntry {
78
78
  skillSlug: string; version: string; deployedAt: string; expiresAt: string
79
79
  }
80
80
 
81
- export type ArtifactProvenance = 'native-evolution' | 'derived-history'
81
+ export type ArtifactProvenance = 'native-evolution' | 'native-topology' | 'derived-history'
82
82
  export type ActivationProvenance = 'capture-derived' | 'project-analysis' | 'derived-failure'
83
83
  export type PressureProvenance = 'failure-native' | 'project-analysis-native' | 'failure-derived' | 'profile-gap-derived'
84
84
 
85
85
  export interface EvidenceArtifact {
86
86
  id: string
87
87
  createdAt: string
88
- artifactType: 'evolution' | 'replay' | 'regression' | 'canary' | 'project-analysis' | 'ontology-review'
88
+ artifactType: 'evolution' | 'replay' | 'regression' | 'canary' | 'project-analysis' | 'ontology-review' | 'topology-apply'
89
89
  provenance: ArtifactProvenance
90
90
  title: string
91
91
  summary: string
@@ -144,12 +144,262 @@ export interface PressureSignal {
144
144
  region?: string
145
145
  description?: string
146
146
  suggestedAction?: 'research' | 'specialize' | 'create'
147
+ confidence?: number
148
+ recurrenceCount?: number
147
149
  relatedFailureId?: string
148
150
  relatedActivationTraceId?: string
149
151
  skillIds?: string[]
150
152
  status?: 'open' | 'addressed'
151
153
  }
152
154
 
155
+ export type GovernanceModeName = 'conservative' | 'balanced' | 'exploration' | 'project-critical' | 'consolidation-focused' | 'transfer-focused'
156
+ export type GovernanceSelectionMode = 'auto' | 'manual'
157
+ export type GovernanceSource = 'derived' | 'operator-selected'
158
+ export type TransferType = 'specialist-to-generalist' | 'generalist-to-project' | 'cross-project' | 'domain-transfer'
159
+ export type PressureInterventionType = 'research' | 'specialize' | 'evolve' | 'generalize' | 'manual-review'
160
+ export type TopologyReviewSource = 'graph-optimize' | 'network-health' | 'pressure-motif' | 'transfer-pattern' | 'manual-route'
161
+ export type TopologyReviewStatus = 'open' | 'accepted' | 'rejected' | 'deferred'
162
+ export type TopologyReviewDecisionStatus = Exclude<TopologyReviewStatus, 'open'>
163
+ export type TopologyReviewPriority = 'high' | 'medium' | 'low'
164
+ export type PressureInterventionStatus = 'started' | 'completed' | 'failed' | 'dry-run'
165
+ export type PressureInterventionImpact = 'exploratory' | 'structural' | 'resolving' | 'none'
166
+ export type PressureLifecycleState = 'open' | 'in-progress' | 'addressed' | 'stale'
167
+ export type PressureRouteScope = 'local' | 'project' | 'network'
168
+ export type PressureRouteTrigger = 'high-priority-local-gap' | 'project-bounded-recurrence' | 'recurring-cross-project-gap' | 'overlapping-skills' | 'accepted-iteration-pattern' | 'insufficient-evidence' | 'mixed-signal'
169
+
170
+ export interface PressureRouteRecommendation {
171
+ route: PressureInterventionType
172
+ scope: PressureRouteScope
173
+ confidence: number
174
+ governanceMode: GovernanceModeName
175
+ triggeredBy: PressureRouteTrigger
176
+ reasons: string[]
177
+ }
178
+
179
+ export interface PressureIntervention {
180
+ id: string
181
+ interventionType: PressureInterventionType
182
+ status: PressureInterventionStatus
183
+ impact: PressureInterventionImpact
184
+ createdAt: string
185
+ completedAt?: string
186
+ sourceId: string
187
+ projectId?: string
188
+ projectPath?: string
189
+ scope?: PressureRouteScope
190
+ governanceMode?: GovernanceModeName
191
+ capabilities?: string[]
192
+ pressureSignalIds?: string[]
193
+ motifIds?: string[]
194
+ relatedFailureIds?: string[]
195
+ relatedActivationTraceIds?: string[]
196
+ relatedProposalIds?: string[]
197
+ relatedArtifactIds?: string[]
198
+ relatedTransferEventIds?: string[]
199
+ relatedSkillSlugs?: string[]
200
+ reasonSummary: string
201
+ notes?: string
202
+ }
203
+
204
+ export interface PressureMotif {
205
+ id: string
206
+ key: string
207
+ kind: string
208
+ capability?: string
209
+ description: string
210
+ createdAt: string
211
+ lastSeenAt: string
212
+ projectIds: string[]
213
+ pressureSignalIds: string[]
214
+ sourceSkillIds: string[]
215
+ recurrenceCount: number
216
+ highPriorityCount: number
217
+ linkedInterventionIds: string[]
218
+ linkedTransferEventIds: string[]
219
+ suggestedRoute: PressureRouteRecommendation
220
+ }
221
+
222
+ export interface TransferEvent {
223
+ id: string
224
+ timestamp: string
225
+ sourceProjectId?: string
226
+ targetProjectId?: string
227
+ sourceSkillIds: string[]
228
+ targetSkillIds?: string[]
229
+ capabilityIds?: string[]
230
+ motifIds?: string[]
231
+ transferType: TransferType
232
+ evidenceIds?: string[]
233
+ benefitEstimate?: number
234
+ status?: 'candidate' | 'realized' | 'rejected'
235
+ summary?: string
236
+ }
237
+
238
+ export interface GovernanceProfile {
239
+ mode: GovernanceModeName
240
+ reviewThreshold: number
241
+ riskTolerance: number
242
+ plasticityBias: number
243
+ explanation: string
244
+ }
245
+
246
+ export interface GovernanceState {
247
+ selectionMode: GovernanceSelectionMode
248
+ manualMode?: GovernanceModeName
249
+ updatedAt: string
250
+ note?: string
251
+ }
252
+
253
+ export interface ActiveGovernanceSummary {
254
+ activeMode: GovernanceModeName
255
+ derivedMode: GovernanceModeName
256
+ source: GovernanceSource
257
+ manualMode?: GovernanceModeName
258
+ rationale: string[]
259
+ profile: GovernanceProfile
260
+ }
261
+
262
+ export interface TopologyReviewCandidate {
263
+ id: string
264
+ reviewKey: string
265
+ createdAt: string
266
+ lastObservedAt: string
267
+ source: TopologyReviewSource
268
+ changeType: 'merge' | 'split' | 'promote' | 'demote' | 'rewire' | 'prune' | 'consolidate'
269
+ title: string
270
+ reasonSummary: string
271
+ description?: string
272
+ priority: TopologyReviewPriority
273
+ confidence: number
274
+ riskScore: number
275
+ governanceMode: GovernanceModeName
276
+ affectedNodeIds: string[]
277
+ affectedRelationIds?: string[]
278
+ projectIds?: string[]
279
+ relatedSignalIds?: string[]
280
+ relatedMotifIds?: string[]
281
+ relatedTransferEventIds?: string[]
282
+ relatedInterventionIds?: string[]
283
+ evidence: string[]
284
+ }
285
+
286
+ export interface TopologyReviewDecision {
287
+ id: string
288
+ candidateId: string
289
+ decision: TopologyReviewDecisionStatus
290
+ decidedAt: string
291
+ governanceMode: GovernanceModeName
292
+ rationale: string
293
+ decidedBy?: 'operator' | 'system'
294
+ }
295
+
296
+ export interface ResolvedTopologyReviewCandidate extends TopologyReviewCandidate {
297
+ status: TopologyReviewStatus
298
+ latestDecision?: TopologyReviewDecision
299
+ lastActivityAt: string
300
+ }
301
+
302
+ export type TopologyExecutionMode = 'prepare-only' | 'apply'
303
+ export type TopologyExecutionStatus = 'prepared' | 'applied' | 'rolled-back' | 'failed'
304
+
305
+ export interface TopologyEdgeRef {
306
+ from: string
307
+ to: string
308
+ type: SkillEdge['type']
309
+ }
310
+
311
+ export interface TopologyNodeOverride {
312
+ layer?: SkillNode['layer']
313
+ cognitiveRole?: CognitiveRole
314
+ stabilityState?: StabilityState
315
+ plasticityState?: PlasticityState
316
+ capabilities?: string[]
317
+ lineageId?: string
318
+ }
319
+
320
+ export interface TopologyOverrides {
321
+ updatedAt: string
322
+ nodeOverrides: Record<string, TopologyNodeOverride>
323
+ suppressedEdges: TopologyEdgeRef[]
324
+ addedEdges: SkillEdge[]
325
+ appliedPlanIds: string[]
326
+ }
327
+
328
+ export interface TopologySnapshotRef {
329
+ id: string
330
+ createdAt: string
331
+ label: string
332
+ candidateId?: string
333
+ planId?: string
334
+ executionRecordId?: string
335
+ snapshotDir: string
336
+ graphPath: string
337
+ overridesPath: string
338
+ skillFileSnapshots: { slug: string; path: string; snapshotPath: string }[]
339
+ }
340
+
341
+ export interface TopologyApplyPlan {
342
+ id: string
343
+ candidateId: string
344
+ acceptedDecisionId: string
345
+ createdAt: string
346
+ changeType: ResolvedTopologyReviewCandidate['changeType']
347
+ governanceMode: GovernanceModeName
348
+ safeToApply: boolean
349
+ executionMode: TopologyExecutionMode
350
+ targetSkillSlugs: string[]
351
+ plannedFileChanges: string[]
352
+ plannedGraphChanges: string[]
353
+ plannedNodeOverrides?: Record<string, TopologyNodeOverride>
354
+ plannedEdgeSuppressions?: TopologyEdgeRef[]
355
+ plannedAddedEdges?: SkillEdge[]
356
+ warnings: string[]
357
+ rollbackStrategy: string
358
+ beforeSnapshotId: string
359
+ reasonSummary: string
360
+ }
361
+
362
+ export interface TopologyExecutionRecord {
363
+ id: string
364
+ planId: string
365
+ candidateId: string
366
+ changeType: ResolvedTopologyReviewCandidate['changeType']
367
+ status: TopologyExecutionStatus
368
+ createdAt: string
369
+ executedAt?: string
370
+ rolledBackAt?: string
371
+ governanceMode: GovernanceModeName
372
+ beforeSnapshotId: string
373
+ afterSnapshotId?: string
374
+ artifactIds?: string[]
375
+ notes?: string[]
376
+ }
377
+
378
+ export interface ResolvedTopologyApplyPlan extends TopologyApplyPlan {
379
+ latestStatus: TopologyExecutionStatus | 'pending'
380
+ latestExecution?: TopologyExecutionRecord
381
+ lastActivityAt: string
382
+ }
383
+
384
+ export interface ResolvedPressureSignal extends PressureSignal {
385
+ lifecycle: PressureLifecycleState
386
+ linkedInterventions: PressureIntervention[]
387
+ interventionTypes: PressureInterventionType[]
388
+ motifIds?: string[]
389
+ routeRecommendation?: PressureRouteRecommendation
390
+ lastActivityAt: string
391
+ addressedAt?: string
392
+ responseSummary: string
393
+ }
394
+
395
+ export interface ResolvedPressureMotif extends PressureMotif {
396
+ lifecycle: PressureLifecycleState
397
+ interventionTypes: PressureInterventionType[]
398
+ lastActivityAt: string
399
+ addressedAt?: string
400
+ responseSummary: string
401
+ }
402
+
153
403
  export interface ProjectPressureSummary {
154
404
  projectId: string
155
405
  totalSignals: number
@@ -159,16 +409,92 @@ export interface ProjectPressureSummary {
159
409
  topCapabilities: string[]
160
410
  }
161
411
 
412
+ export interface ProjectResponseSummary extends ProjectPressureSummary {
413
+ openSignals: number
414
+ inProgressSignals: number
415
+ addressedSignals: number
416
+ staleSignals: number
417
+ activeInterventions: number
418
+ recentInterventionTypes: PressureInterventionType[]
419
+ }
420
+
162
421
  export interface CoEvolutionDashboardSummary {
163
422
  pressureSignals: {
164
423
  total: number
165
424
  native: number
166
425
  derived: number
167
426
  }
427
+ pressureInterventions: {
428
+ total: number
429
+ started: number
430
+ completed: number
431
+ failed: number
432
+ dryRun: number
433
+ }
434
+ pressureLifecycle: {
435
+ open: number
436
+ inProgress: number
437
+ addressed: number
438
+ stale: number
439
+ highPriorityOpen: number
440
+ }
441
+ pressureMotifs: {
442
+ total: number
443
+ open: number
444
+ inProgress: number
445
+ addressed: number
446
+ stale: number
447
+ promotionReady: number
448
+ }
449
+ governance: ActiveGovernanceSummary
450
+ topologyReviews: {
451
+ total: number
452
+ open: number
453
+ accepted: number
454
+ rejected: number
455
+ deferred: number
456
+ highPriorityOpen: number
457
+ generatedFromManualReview: number
458
+ byChangeType: Record<ResolvedTopologyReviewCandidate['changeType'], number>
459
+ }
460
+ topologyExecution: {
461
+ plans: number
462
+ pending: number
463
+ prepared: number
464
+ applied: number
465
+ rolledBack: number
466
+ failed: number
467
+ safeToApply: number
468
+ prepareOnly: number
469
+ snapshots: number
470
+ artifacts: number
471
+ }
168
472
  topProjects: ProjectPressureSummary[]
473
+ responseProjects: ProjectResponseSummary[]
169
474
  rolePressure: Record<CognitiveRole, number>
170
475
  crossProjectGapAreas: { area: string; projects: string[]; count: number }[]
171
476
  activeRecommendations: { action: 'research' | 'specialize' | 'create'; count: number }[]
477
+ governedRoutes: { route: PressureInterventionType; total: number }[]
478
+ interventionLanes: { type: PressureInterventionType; total: number; resolving: number; exploratory: number; none: number }[]
479
+ backlog: ResolvedPressureSignal[]
480
+ promoteQueue: ResolvedPressureMotif[]
481
+ recentAddressed: ResolvedPressureSignal[]
482
+ recentInterventions: PressureIntervention[]
483
+ recentTransfers: TransferEvent[]
484
+ topologyBacklog: ResolvedTopologyReviewCandidate[]
485
+ recentTopologyDecisions: ResolvedTopologyReviewCandidate[]
486
+ }
487
+
488
+ export interface TopologyDashboardSummary {
489
+ governance: ActiveGovernanceSummary
490
+ summary: CoEvolutionDashboardSummary['topologyReviews']
491
+ execution: CoEvolutionDashboardSummary['topologyExecution']
492
+ backlog: ResolvedTopologyReviewCandidate[]
493
+ recentDecisions: ResolvedTopologyReviewCandidate[]
494
+ acceptedReady: ResolvedTopologyReviewCandidate[]
495
+ preparedQueue: ResolvedTopologyApplyPlan[]
496
+ appliedHistory: ResolvedTopologyApplyPlan[]
497
+ candidates: ResolvedTopologyReviewCandidate[]
172
498
  }
173
499
 
174
500
  export interface OntologyDashboardSummary {
@@ -197,6 +523,66 @@ export interface OntologyDashboardSummary {
197
523
  native: number
198
524
  derived: number
199
525
  }
526
+ pressureInterventions: {
527
+ total: number
528
+ started: number
529
+ completed: number
530
+ failed: number
531
+ dryRun: number
532
+ }
533
+ pressureLifecycle: {
534
+ open: number
535
+ inProgress: number
536
+ addressed: number
537
+ stale: number
538
+ }
539
+ pressureMotifs: {
540
+ total: number
541
+ open: number
542
+ inProgress: number
543
+ addressed: number
544
+ stale: number
545
+ promotionReady: number
546
+ }
547
+ governedRoutes: Record<PressureInterventionType, number>
548
+ transferEvents: {
549
+ total: number
550
+ realized: number
551
+ candidate: number
552
+ rejected: number
553
+ }
554
+ governance: {
555
+ activeMode: GovernanceModeName
556
+ derivedMode: GovernanceModeName
557
+ source: GovernanceSource
558
+ manualMode?: GovernanceModeName
559
+ rationale: string[]
560
+ reviewThreshold: number
561
+ riskTolerance: number
562
+ plasticityBias: number
563
+ }
564
+ topologyReviews: {
565
+ total: number
566
+ open: number
567
+ accepted: number
568
+ rejected: number
569
+ deferred: number
570
+ highPriorityOpen: number
571
+ generatedFromManualReview: number
572
+ byChangeType: Record<ResolvedTopologyReviewCandidate['changeType'], number>
573
+ }
574
+ topologyExecution: {
575
+ plans: number
576
+ pending: number
577
+ prepared: number
578
+ applied: number
579
+ rolledBack: number
580
+ failed: number
581
+ safeToApply: number
582
+ prepareOnly: number
583
+ snapshots: number
584
+ artifacts: number
585
+ }
200
586
  annotatedFailures: {
201
587
  pressureSignals: number
202
588
  capabilityGaps: number
@@ -233,8 +619,38 @@ function inferPlasticityState(node: SkillNode): PlasticityState {
233
619
 
234
620
  // ─── Loaders ────────────────────────────────────────────────────
235
621
 
622
+ function edgeKey(edge: { from: string; to: string; type: string }): string {
623
+ return `${edge.from}::${edge.to}::${edge.type}`
624
+ }
625
+
626
+ export function loadTopologyOverrides(): TopologyOverrides {
627
+ const overrides = readJson<TopologyOverrides>('topology-overrides.json', { updatedAt: '', nodeOverrides: {}, suppressedEdges: [], addedEdges: [], appliedPlanIds: [] })
628
+ return {
629
+ updatedAt: overrides.updatedAt || '',
630
+ nodeOverrides: overrides.nodeOverrides ?? {},
631
+ suppressedEdges: overrides.suppressedEdges ?? [],
632
+ addedEdges: overrides.addedEdges ?? [],
633
+ appliedPlanIds: overrides.appliedPlanIds ?? [],
634
+ }
635
+ }
636
+
637
+ function applyTopologyOverridesToGraph(graph: SkillGraph, overrides: TopologyOverrides = loadTopologyOverrides()): SkillGraph {
638
+ const suppressed = new Set(overrides.suppressedEdges.map((edge) => edgeKey(edge)))
639
+ const edgeMap = new Map<string, SkillEdge>()
640
+ for (const edge of graph.edges) {
641
+ const key = edgeKey(edge)
642
+ if (!suppressed.has(key)) edgeMap.set(key, edge)
643
+ }
644
+ for (const edge of overrides.addedEdges) edgeMap.set(edgeKey(edge), edge)
645
+ return {
646
+ ...graph,
647
+ nodes: graph.nodes.map((node) => ({ ...node, ...(overrides.nodeOverrides[node.id] ?? {}) })),
648
+ edges: [...edgeMap.values()],
649
+ }
650
+ }
651
+
236
652
  export function loadGraph(): SkillGraph {
237
- return readJson<SkillGraph>('skill-graph.json', { updated: '', nodes: [], edges: [], clusters: [] })
653
+ return applyTopologyOverridesToGraph(readJson<SkillGraph>('skill-graph.json', { updated: '', nodes: [], edges: [], clusters: [] }))
238
654
  }
239
655
 
240
656
  export function loadFailures(): Failure[] {
@@ -405,19 +821,688 @@ export function loadPressureSignals(): PressureSignal[] {
405
821
  ].sort((a, b) => b.detectedAt.localeCompare(a.detectedAt))
406
822
  }
407
823
 
824
+ export function loadPressureInterventions(): PressureIntervention[] {
825
+ return readJsonl<PressureIntervention>('pressure-interventions.jsonl')
826
+ .slice()
827
+ .sort((a, b) => (b.completedAt ?? b.createdAt).localeCompare(a.completedAt ?? a.createdAt))
828
+ }
829
+
830
+ export function loadTransferEvents(): TransferEvent[] {
831
+ return readJsonl<TransferEvent>('transfer-events.jsonl')
832
+ .slice()
833
+ .sort((a, b) => b.timestamp.localeCompare(a.timestamp))
834
+ }
835
+
836
+ const DEFAULT_GOVERNANCE_STATE: GovernanceState = {
837
+ selectionMode: 'auto',
838
+ updatedAt: new Date(0).toISOString(),
839
+ }
840
+
841
+ const GOVERNANCE_PROFILES: Record<GovernanceModeName, GovernanceProfile> = {
842
+ conservative: { mode: 'conservative', reviewThreshold: 0.86, riskTolerance: 0.28, plasticityBias: 0.26, explanation: 'Prefer stronger evidence and slower structural change until risk falls.' },
843
+ balanced: { mode: 'balanced', reviewThreshold: 0.74, riskTolerance: 0.5, plasticityBias: 0.5, explanation: 'Keep local stabilization and structural adaptation in balance.' },
844
+ exploration: { mode: 'exploration', reviewThreshold: 0.62, riskTolerance: 0.7, plasticityBias: 0.72, explanation: 'Favor evidence gathering and wider structural exploration before locking in.' },
845
+ 'project-critical': { mode: 'project-critical', reviewThreshold: 0.82, riskTolerance: 0.35, plasticityBias: 0.34, explanation: 'Bias toward project-bounded stability and cautious structural change.' },
846
+ 'consolidation-focused': { mode: 'consolidation-focused', reviewThreshold: 0.78, riskTolerance: 0.42, plasticityBias: 0.44, explanation: 'Favor consolidating proven patterns into stable reusable structure.' },
847
+ 'transfer-focused': { mode: 'transfer-focused', reviewThreshold: 0.68, riskTolerance: 0.6, plasticityBias: 0.68, explanation: 'Bias toward cross-project promotion and reusable topology upgrades.' },
848
+ }
849
+
850
+ function topologyChangeCounts(): Record<ResolvedTopologyReviewCandidate['changeType'], number> {
851
+ return { merge: 0, split: 0, promote: 0, demote: 0, rewire: 0, prune: 0, consolidate: 0 }
852
+ }
853
+
854
+ function maxTimestamp(...values: Array<string | undefined>): string {
855
+ return values.filter(Boolean).sort((a, b) => b!.localeCompare(a!))[0] ?? new Date(0).toISOString()
856
+ }
857
+
858
+ function priorityRank(priority: TopologyReviewPriority): number {
859
+ return priority === 'high' ? 3 : priority === 'medium' ? 2 : 1
860
+ }
861
+
862
+ function topologyStatusRank(status: TopologyReviewStatus): number {
863
+ return status === 'open' ? 4 : status === 'deferred' ? 3 : status === 'accepted' ? 2 : 1
864
+ }
865
+
866
+ function governanceProfileForMode(mode: GovernanceModeName): GovernanceProfile {
867
+ return GOVERNANCE_PROFILES[mode]
868
+ }
869
+
870
+ export function loadGovernanceState(): GovernanceState {
871
+ const state = readJson<GovernanceState>('governance-state.json', DEFAULT_GOVERNANCE_STATE)
872
+ if (state.selectionMode === 'manual' && state.manualMode) return state
873
+ return { ...DEFAULT_GOVERNANCE_STATE, ...state, selectionMode: state.selectionMode === 'manual' && state.manualMode ? 'manual' : 'auto' }
874
+ }
875
+
876
+ export function loadStoredTopologyReviewCandidates(): TopologyReviewCandidate[] {
877
+ return readJson<TopologyReviewCandidate[]>('topology-review-candidates.json', [])
878
+ .slice()
879
+ .sort((a, b) => b.lastObservedAt.localeCompare(a.lastObservedAt) || a.title.localeCompare(b.title))
880
+ }
881
+
882
+ export function loadTopologyReviewDecisions(): TopologyReviewDecision[] {
883
+ return readJsonl<TopologyReviewDecision>('topology-review-decisions.jsonl')
884
+ .slice()
885
+ .sort((a, b) => b.decidedAt.localeCompare(a.decidedAt))
886
+ }
887
+
888
+ export function loadResolvedTopologyReviewCandidates(): ResolvedTopologyReviewCandidate[] {
889
+ const candidates = loadStoredTopologyReviewCandidates()
890
+ const latestDecisionByCandidate = new Map<string, TopologyReviewDecision>()
891
+ for (const decision of loadTopologyReviewDecisions()) {
892
+ const existing = latestDecisionByCandidate.get(decision.candidateId)
893
+ if (!existing || decision.decidedAt > existing.decidedAt) latestDecisionByCandidate.set(decision.candidateId, decision)
894
+ }
895
+ return candidates
896
+ .map((candidate) => {
897
+ const latestDecision = latestDecisionByCandidate.get(candidate.id)
898
+ const status: TopologyReviewStatus = latestDecision?.decision ?? 'open'
899
+ return {
900
+ ...candidate,
901
+ status,
902
+ latestDecision,
903
+ lastActivityAt: maxTimestamp(candidate.lastObservedAt, latestDecision?.decidedAt),
904
+ } satisfies ResolvedTopologyReviewCandidate
905
+ })
906
+ .sort((a, b) => topologyStatusRank(b.status) - topologyStatusRank(a.status)
907
+ || priorityRank(b.priority) - priorityRank(a.priority)
908
+ || b.confidence - a.confidence
909
+ || b.lastActivityAt.localeCompare(a.lastActivityAt)
910
+ || a.title.localeCompare(b.title))
911
+ }
912
+
913
+ export function loadTopologySnapshots(): TopologySnapshotRef[] {
914
+ return readJson<TopologySnapshotRef[]>('topology-snapshots.json', [])
915
+ .slice()
916
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
917
+ }
918
+
919
+ export function loadTopologyApplyPlans(): TopologyApplyPlan[] {
920
+ return readJson<TopologyApplyPlan[]>('topology-apply-plans.json', [])
921
+ .slice()
922
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
923
+ }
924
+
925
+ export function loadTopologyExecutionRecords(): TopologyExecutionRecord[] {
926
+ return readJsonl<TopologyExecutionRecord>('topology-executions.jsonl')
927
+ .slice()
928
+ .sort((a, b) => (b.executedAt ?? b.rolledBackAt ?? b.createdAt).localeCompare(a.executedAt ?? a.rolledBackAt ?? a.createdAt))
929
+ }
930
+
931
+ export function loadTopologyArtifacts(): EvidenceArtifact[] {
932
+ return readJsonl<EvidenceArtifact>('topology-artifacts.jsonl')
933
+ .slice()
934
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
935
+ }
936
+
937
+ export function loadResolvedTopologyApplyPlans(): ResolvedTopologyApplyPlan[] {
938
+ const plans = loadTopologyApplyPlans()
939
+ const latestByPlan = new Map<string, TopologyExecutionRecord>()
940
+ for (const record of loadTopologyExecutionRecords()) {
941
+ const existing = latestByPlan.get(record.planId)
942
+ const recordAt = record.executedAt ?? record.rolledBackAt ?? record.createdAt
943
+ const existingAt = existing ? (existing.executedAt ?? existing.rolledBackAt ?? existing.createdAt) : ''
944
+ if (!existing || recordAt > existingAt) latestByPlan.set(record.planId, record)
945
+ }
946
+ return plans
947
+ .map((plan) => {
948
+ const latestExecution = latestByPlan.get(plan.id)
949
+ return {
950
+ ...plan,
951
+ latestStatus: latestExecution?.status ?? 'pending',
952
+ latestExecution,
953
+ lastActivityAt: maxTimestamp(plan.createdAt, latestExecution?.executedAt, latestExecution?.rolledBackAt, latestExecution?.createdAt),
954
+ } satisfies ResolvedTopologyApplyPlan
955
+ })
956
+ .sort((a, b) => (b.lastActivityAt.localeCompare(a.lastActivityAt)) || (b.safeToApply === a.safeToApply ? 0 : b.safeToApply ? 1 : -1))
957
+ }
958
+
959
+ export function getTopologyExecutionSummary() {
960
+ const plans = loadResolvedTopologyApplyPlans()
961
+ const snapshots = loadTopologySnapshots()
962
+ const artifacts = loadTopologyArtifacts()
963
+ const plannedCandidateIds = new Set(plans.map((plan) => plan.candidateId))
964
+ const acceptedReady = loadResolvedTopologyReviewCandidates()
965
+ .filter((candidate) => candidate.status === 'accepted' && !plannedCandidateIds.has(candidate.id))
966
+ .slice(0, 8)
967
+ return {
968
+ plans: plans.length,
969
+ pending: plans.filter((plan) => plan.latestStatus === 'pending').length,
970
+ prepared: plans.filter((plan) => plan.latestStatus === 'prepared').length,
971
+ applied: plans.filter((plan) => plan.latestStatus === 'applied').length,
972
+ rolledBack: plans.filter((plan) => plan.latestStatus === 'rolled-back').length,
973
+ failed: plans.filter((plan) => plan.latestStatus === 'failed').length,
974
+ safeToApply: plans.filter((plan) => plan.safeToApply).length,
975
+ prepareOnly: plans.filter((plan) => plan.executionMode === 'prepare-only').length,
976
+ snapshots: snapshots.length,
977
+ artifacts: artifacts.length,
978
+ acceptedReady,
979
+ preparedQueue: plans.filter((plan) => plan.latestStatus === 'prepared').slice(0, 8),
980
+ appliedHistory: plans.filter((plan) => plan.latestStatus === 'applied' || plan.latestStatus === 'rolled-back').slice(0, 8),
981
+ }
982
+ }
983
+
984
+ export function getTopologyReviewSummary() {
985
+ const candidates = loadResolvedTopologyReviewCandidates()
986
+ const byChangeType = topologyChangeCounts()
987
+ for (const candidate of candidates) byChangeType[candidate.changeType] += 1
988
+ return {
989
+ total: candidates.length,
990
+ open: candidates.filter((candidate) => candidate.status === 'open').length,
991
+ accepted: candidates.filter((candidate) => candidate.status === 'accepted').length,
992
+ rejected: candidates.filter((candidate) => candidate.status === 'rejected').length,
993
+ deferred: candidates.filter((candidate) => candidate.status === 'deferred').length,
994
+ highPriorityOpen: candidates.filter((candidate) => candidate.status === 'open' && candidate.priority === 'high').length,
995
+ generatedFromManualReview: candidates.filter((candidate) => candidate.source === 'manual-route').length,
996
+ byChangeType,
997
+ backlog: candidates.filter((candidate) => candidate.status === 'open' || candidate.status === 'deferred').slice(0, 10),
998
+ recentDecisions: candidates.filter((candidate) => candidate.status !== 'open').slice(0, 8),
999
+ }
1000
+ }
1001
+
1002
+ export function loadTopologyDashboardSummary(): TopologyDashboardSummary {
1003
+ const governance = getActiveGovernanceSummary()
1004
+ const summary = getTopologyReviewSummary()
1005
+ const execution = getTopologyExecutionSummary()
1006
+ const candidates = loadResolvedTopologyReviewCandidates()
1007
+ return {
1008
+ governance,
1009
+ summary,
1010
+ execution,
1011
+ backlog: summary.backlog,
1012
+ recentDecisions: summary.recentDecisions,
1013
+ acceptedReady: execution.acceptedReady,
1014
+ preparedQueue: execution.preparedQueue,
1015
+ appliedHistory: execution.appliedHistory,
1016
+ candidates,
1017
+ }
1018
+ }
1019
+
1020
+ function normalizePressureValue(value: string | undefined): string {
1021
+ return (value ?? '')
1022
+ .trim()
1023
+ .toLowerCase()
1024
+ .replace(/[\s_/]+/g, '-')
1025
+ }
1026
+
1027
+ function pressureRegionKey(signal: Pick<PressureSignal, 'capability' | 'region' | 'kind'>): string {
1028
+ return normalizePressureValue(signal.capability ?? signal.region ?? signal.kind) || 'general-pressure'
1029
+ }
1030
+
1031
+ function motifIdForSignal(signal: Pick<PressureSignal, 'capability' | 'region' | 'kind'>): string {
1032
+ return `motif_${pressureRegionKey(signal)}`
1033
+ }
1034
+
1035
+ function pressureComparisonKey(signal: PressureSignal): string {
1036
+ const projectKey = normalizePressureValue(signal.projectId ?? signal.projectPath ?? 'global')
1037
+ return `${projectKey}::${pressureRegionKey(signal)}`
1038
+ }
1039
+
1040
+ function interventionComparisonKeys(intervention: PressureIntervention): string[] {
1041
+ const projectKey = normalizePressureValue(intervention.projectId ?? intervention.projectPath ?? 'global')
1042
+ const capabilityKeys = (intervention.capabilities?.length ?? 0) > 0
1043
+ ? intervention.capabilities ?? []
1044
+ : ['']
1045
+ return capabilityKeys.map((capability) => `${projectKey}::${normalizePressureValue(capability)}`)
1046
+ }
1047
+
1048
+ function signalMatchesInterventionFallback(signal: PressureSignal, intervention: PressureIntervention): boolean {
1049
+ if (intervention.pressureSignalIds?.includes(signal.id)) return true
1050
+ if (intervention.scope === 'network') return false
1051
+ if (signal.relatedFailureId && intervention.relatedFailureIds?.includes(signal.relatedFailureId)) return true
1052
+ if (signal.relatedActivationTraceId && intervention.relatedActivationTraceIds?.includes(signal.relatedActivationTraceId)) return true
1053
+
1054
+ const signalKey = pressureComparisonKey(signal)
1055
+ if (interventionComparisonKeys(intervention).includes(signalKey)) return true
1056
+
1057
+ if (signal.projectId && intervention.projectId && signal.projectId !== intervention.projectId) return false
1058
+ if (signal.projectPath && intervention.projectPath && signal.projectPath !== intervention.projectPath) return false
1059
+
1060
+ if (signal.capability && (intervention.capabilities?.length ?? 0) > 0) {
1061
+ return intervention.capabilities?.some((capability) => normalizePressureValue(capability) === normalizePressureValue(signal.capability)) ?? false
1062
+ }
1063
+
1064
+ return false
1065
+ }
1066
+
1067
+ function motifMatchesIntervention(motifId: string, signals: ResolvedPressureSignal[], intervention: PressureIntervention): boolean {
1068
+ if (intervention.motifIds?.includes(motifId)) return true
1069
+ if (signals.some((signal) => intervention.pressureSignalIds?.includes(signal.id))) return true
1070
+ if (intervention.scope === 'network') {
1071
+ const signalKeys = new Set(signals.map((signal) => pressureRegionKey(signal)))
1072
+ return (intervention.capabilities ?? []).some((capability) => signalKeys.has(normalizePressureValue(capability)))
1073
+ }
1074
+ return false
1075
+ }
1076
+
1077
+ function transferMatchesMotif(motifId: string, signals: ResolvedPressureSignal[], event: TransferEvent): boolean {
1078
+ if (event.motifIds?.includes(motifId)) return true
1079
+ const capabilities = new Set(signals.map((signal) => normalizePressureValue(signal.capability ?? signal.kind)).filter(Boolean))
1080
+ return (event.capabilityIds ?? []).some((capability) => capabilities.has(normalizePressureValue(capability)))
1081
+ }
1082
+
1083
+ function deriveGovernanceSummary(
1084
+ pressureSignals: PressureSignal[] = loadPressureSignals(),
1085
+ interventions: PressureIntervention[] = loadPressureInterventions(),
1086
+ ): ActiveGovernanceSummary {
1087
+ const highPrioritySignals = pressureSignals.filter((signal) => signal.priority === 'high' && signal.status !== 'addressed').length
1088
+ const recurringCrossProjectRegions = new Map<string, Set<string>>()
1089
+ for (const signal of pressureSignals) {
1090
+ const projectId = signal.projectId ?? signal.projectPath
1091
+ if (!projectId) continue
1092
+ const projects = recurringCrossProjectRegions.get(pressureRegionKey(signal)) ?? new Set<string>()
1093
+ projects.add(projectId)
1094
+ recurringCrossProjectRegions.set(pressureRegionKey(signal), projects)
1095
+ }
1096
+ const crossProjectMotifs = [...recurringCrossProjectRegions.values()].filter((projects) => projects.size >= 2).length
1097
+ const exploratoryRuns = interventions.filter((intervention) => intervention.status === 'dry-run' || intervention.impact === 'exploratory').length
1098
+ const failedRuns = interventions.filter((intervention) => intervention.status === 'failed').length
1099
+ const resolvingRuns = interventions.filter((intervention) => intervention.status === 'completed' && intervention.impact === 'resolving').length
1100
+
1101
+ let derivedMode: GovernanceModeName = 'balanced'
1102
+ let derivedRationale: string[] = [
1103
+ 'Current pressure is mixed across local response and broader transfer opportunities.',
1104
+ 'Governance should keep local adaptation and cross-project learning in balance.',
1105
+ ]
1106
+
1107
+ if (crossProjectMotifs >= 2) {
1108
+ derivedMode = 'transfer-focused'
1109
+ derivedRationale = [
1110
+ `${crossProjectMotifs} recurring pressure motifs now span multiple projects.`,
1111
+ 'Promoting reusable abstractions has higher leverage than only local adaptation right now.',
1112
+ ]
1113
+ } else if (highPrioritySignals >= 4) {
1114
+ derivedMode = 'project-critical'
1115
+ derivedRationale = [
1116
+ `${highPrioritySignals} high-priority pressure signals still need response.`,
1117
+ 'Governance should bias toward project-bounded stabilization before broader promotion work.',
1118
+ ]
1119
+ } else if (exploratoryRuns > resolvingRuns + 1) {
1120
+ derivedMode = 'exploration'
1121
+ derivedRationale = [
1122
+ `${exploratoryRuns} exploratory responses outweigh resolving outcomes.`,
1123
+ 'The network is still learning where the strongest durable response should be routed.',
1124
+ ]
1125
+ } else if (failedRuns >= 2 && resolvingRuns === 0) {
1126
+ derivedMode = 'conservative'
1127
+ derivedRationale = [
1128
+ `${failedRuns} recent response attempts failed without resolving evidence.`,
1129
+ 'Governance should slow promotion and prefer clearer evidence before acting broadly.',
1130
+ ]
1131
+ } else if (resolvingRuns >= 3 && highPrioritySignals <= 2) {
1132
+ derivedMode = 'consolidation-focused'
1133
+ derivedRationale = [
1134
+ `${resolvingRuns} resolving responses already landed recently.`,
1135
+ 'The network is in a good position to consolidate what it has learned.',
1136
+ ]
1137
+ }
1138
+
1139
+ const state = loadGovernanceState()
1140
+ if (state.selectionMode === 'manual' && state.manualMode) {
1141
+ return {
1142
+ activeMode: state.manualMode,
1143
+ derivedMode,
1144
+ source: 'operator-selected',
1145
+ manualMode: state.manualMode,
1146
+ rationale: [
1147
+ `Operator selected ${state.manualMode} governance on ${state.updatedAt}.`,
1148
+ `Derived mode would currently be ${derivedMode}.`,
1149
+ ...derivedRationale,
1150
+ ],
1151
+ profile: governanceProfileForMode(state.manualMode),
1152
+ }
1153
+ }
1154
+
1155
+ return {
1156
+ activeMode: derivedMode,
1157
+ derivedMode,
1158
+ source: 'derived',
1159
+ rationale: derivedRationale,
1160
+ profile: governanceProfileForMode(derivedMode),
1161
+ }
1162
+ }
1163
+
1164
+ export function getActiveGovernanceSummary(): ActiveGovernanceSummary {
1165
+ return deriveGovernanceSummary()
1166
+ }
1167
+
1168
+ function buildRouteRecommendation(params: {
1169
+ signal: PressureSignal
1170
+ relatedSignals: PressureSignal[]
1171
+ linkedInterventions: PressureIntervention[]
1172
+ governanceMode: GovernanceModeName
1173
+ }): PressureRouteRecommendation {
1174
+ const { signal, relatedSignals, linkedInterventions, governanceMode } = params
1175
+ const projectSpread = new Set(relatedSignals.map((entry) => entry.projectId ?? entry.projectPath).filter(Boolean)).size
1176
+ const recurrenceCount = relatedSignals.length
1177
+ const activeTypes = [...new Set(linkedInterventions.map((intervention) => intervention.interventionType))]
1178
+ const reasons: string[] = []
1179
+
1180
+ if (activeTypes.length >= 3 && !linkedInterventions.some((intervention) => intervention.status === 'completed' && intervention.impact === 'resolving')) {
1181
+ reasons.push('Multiple intervention lanes already touch this pressure region without clear closure.')
1182
+ reasons.push('Operator review is safer than blindly piling on another automated response.')
1183
+ return {
1184
+ route: 'manual-review',
1185
+ scope: projectSpread >= 2 ? 'network' : signal.projectId ? 'project' : 'local',
1186
+ confidence: 0.58,
1187
+ governanceMode,
1188
+ triggeredBy: 'mixed-signal',
1189
+ reasons,
1190
+ }
1191
+ }
1192
+
1193
+ if (projectSpread >= 2 && recurrenceCount >= 2) {
1194
+ reasons.push(`This pressure region repeats across ${projectSpread} projects.`)
1195
+ 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
+ }
1205
+ }
1206
+ }
1207
+
1208
+ if (signal.projectId && signal.priority === 'high' && signal.suggestedAction === 'specialize') {
1209
+ reasons.push('The pressure is high priority and bounded to a known project context.')
1210
+ reasons.push('Project-layer adaptation is the shortest truthful path to response.')
1211
+ return {
1212
+ route: 'specialize',
1213
+ scope: 'project',
1214
+ confidence: 0.82,
1215
+ governanceMode,
1216
+ triggeredBy: 'high-priority-local-gap',
1217
+ reasons,
1218
+ }
1219
+ }
1220
+
1221
+ if (signal.relatedFailureId && signal.severity >= 0.8) {
1222
+ reasons.push('This pressure is tied to a concrete failure with strong corrective evidence.')
1223
+ reasons.push('An evolution proposal can test a direct skill-level response against replay and regression evidence.')
1224
+ return {
1225
+ route: 'evolve',
1226
+ scope: signal.projectId ? 'project' : 'local',
1227
+ confidence: 0.78,
1228
+ governanceMode,
1229
+ triggeredBy: 'accepted-iteration-pattern',
1230
+ reasons,
1231
+ }
1232
+ }
1233
+
1234
+ if (governanceMode === 'exploration' || signal.suggestedAction === 'research' || !signal.projectId) {
1235
+ reasons.push('The pressure still looks under-specified or capability-oriented.')
1236
+ reasons.push('Research is the best way to widen evidence before committing to structural change.')
1237
+ return {
1238
+ route: 'research',
1239
+ scope: signal.projectId ? 'project' : 'local',
1240
+ confidence: 0.68,
1241
+ governanceMode,
1242
+ triggeredBy: 'insufficient-evidence',
1243
+ reasons,
1244
+ }
1245
+ }
1246
+
1247
+ if (signal.projectId) {
1248
+ reasons.push('The pressure is project-scoped and can likely be handled without network-wide promotion yet.')
1249
+ reasons.push('Specialization is the most direct bounded intervention available.')
1250
+ return {
1251
+ route: 'specialize',
1252
+ scope: 'project',
1253
+ confidence: 0.7,
1254
+ governanceMode,
1255
+ triggeredBy: 'project-bounded-recurrence',
1256
+ reasons,
1257
+ }
1258
+ }
1259
+
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
+ }
1270
+ }
1271
+
1272
+ function interventionEvidenceAccepted(
1273
+ intervention: PressureIntervention,
1274
+ acceptedProposalIds: Set<string>,
1275
+ acceptedArtifactIds: Set<string>,
1276
+ realizedTransferEventIds: Set<string>,
1277
+ ): boolean {
1278
+ if (intervention.status !== 'completed' || intervention.impact !== 'resolving') return false
1279
+ if (intervention.relatedTransferEventIds?.some((id) => realizedTransferEventIds.has(id))) return true
1280
+ if (intervention.relatedProposalIds?.some((id) => acceptedProposalIds.has(id))) {
1281
+ return !(intervention.interventionType === 'generalize' && intervention.scope === 'network')
1282
+ }
1283
+ if (intervention.relatedArtifactIds?.some((id) => acceptedArtifactIds.has(id))) {
1284
+ return !(intervention.interventionType === 'generalize' && intervention.scope === 'network')
1285
+ }
1286
+ if (intervention.interventionType !== 'generalize') return Boolean(intervention.relatedSkillSlugs?.length)
1287
+ return false
1288
+ }
1289
+
1290
+ function interventionKeepsPressureActive(impact: PressureInterventionImpact, status: PressureInterventionStatus): boolean {
1291
+ if (status === 'failed' || status === 'dry-run') return false
1292
+ return impact === 'exploratory' || impact === 'structural' || impact === 'resolving'
1293
+ }
1294
+
1295
+ export function loadResolvedPressureSignals(): ResolvedPressureSignal[] {
1296
+ const pressureSignals = loadPressureSignals()
1297
+ const interventions = loadPressureInterventions()
1298
+ const transferEvents = loadTransferEvents()
1299
+ const history = loadHistory()
1300
+ const artifacts = loadEvolutionArtifacts()
1301
+ const governance = deriveGovernanceSummary(pressureSignals, interventions)
1302
+ const acceptedProposalIds = new Set(
1303
+ history.iterations.flatMap((iteration) => iteration.proposals)
1304
+ .filter((proposal) => proposal.outcome === 'accepted')
1305
+ .map((proposal) => proposal.id),
1306
+ )
1307
+ const acceptedArtifactIds = new Set(
1308
+ artifacts.filter((artifact) => artifact.outcome === 'accepted').map((artifact) => artifact.id),
1309
+ )
1310
+ const realizedTransferEventIds = new Set(
1311
+ transferEvents.filter((event) => event.status === 'realized').map((event) => event.id),
1312
+ )
1313
+ const latestSignalByKey = new Map<string, string>()
1314
+ const signalsByRegion = new Map<string, PressureSignal[]>()
1315
+
1316
+ for (const signal of pressureSignals) {
1317
+ const comparisonKey = pressureComparisonKey(signal)
1318
+ const existing = latestSignalByKey.get(comparisonKey)
1319
+ if (!existing || signal.detectedAt > existing) {
1320
+ latestSignalByKey.set(comparisonKey, signal.detectedAt)
1321
+ }
1322
+
1323
+ const regionKey = pressureRegionKey(signal)
1324
+ const regionSignals = signalsByRegion.get(regionKey) ?? []
1325
+ regionSignals.push(signal)
1326
+ signalsByRegion.set(regionKey, regionSignals)
1327
+ }
1328
+
1329
+ return pressureSignals.map((signal) => {
1330
+ const linkedInterventions = interventions.filter((intervention) => signalMatchesInterventionFallback(signal, intervention))
1331
+ const addressingInterventions = linkedInterventions.filter((intervention) => interventionEvidenceAccepted(intervention, acceptedProposalIds, acceptedArtifactIds, realizedTransferEventIds))
1332
+ const activeInterventions = linkedInterventions.filter((intervention) => interventionKeepsPressureActive(intervention.impact, intervention.status))
1333
+ const latestInterventionAt = linkedInterventions.map((intervention) => intervention.completedAt ?? intervention.createdAt).sort((a, b) => b.localeCompare(a))[0]
1334
+ const latestSignalAt = latestSignalByKey.get(pressureComparisonKey(signal)) ?? signal.detectedAt
1335
+ const hasNewerEquivalentSignal = latestSignalAt > signal.detectedAt
1336
+ const relatedSignals = signalsByRegion.get(pressureRegionKey(signal)) ?? [signal]
1337
+ const projectSpread = new Set(relatedSignals.map((entry) => entry.projectId ?? entry.projectPath).filter(Boolean)).size
1338
+ const motifIds = relatedSignals.length >= 2 || projectSpread >= 2 ? [motifIdForSignal(signal)] : []
1339
+ const routeRecommendation = buildRouteRecommendation({
1340
+ signal,
1341
+ relatedSignals,
1342
+ linkedInterventions,
1343
+ governanceMode: governance.activeMode,
1344
+ })
1345
+
1346
+ const lifecycle: PressureLifecycleState = addressingInterventions.length > 0 || signal.status === 'addressed'
1347
+ ? 'addressed'
1348
+ : activeInterventions.length > 0
1349
+ ? 'in-progress'
1350
+ : hasNewerEquivalentSignal
1351
+ ? 'stale'
1352
+ : 'open'
1353
+
1354
+ const lastActivityAt = [signal.detectedAt, latestInterventionAt].filter(Boolean).sort((a, b) => b.localeCompare(a))[0] ?? signal.detectedAt
1355
+ const interventionTypes = [...new Set(linkedInterventions.map((intervention) => intervention.interventionType))]
1356
+ const addressedAt = addressingInterventions.map((intervention) => intervention.completedAt ?? intervention.createdAt).sort((a, b) => b.localeCompare(a))[0]
1357
+ const responseSummary = lifecycle === 'addressed'
1358
+ ? `Addressed by ${interventionTypes.join(', ') || 'linked intervention'} evidence`
1359
+ : lifecycle === 'in-progress'
1360
+ ? `Response underway via ${interventionTypes.join(', ')}`
1361
+ : lifecycle === 'stale'
1362
+ ? 'Superseded by newer pressure in the same project/capability region'
1363
+ : `No linked response yet • recommend ${routeRecommendation.route}`
1364
+
1365
+ return {
1366
+ ...signal,
1367
+ lifecycle,
1368
+ linkedInterventions,
1369
+ interventionTypes,
1370
+ motifIds,
1371
+ routeRecommendation,
1372
+ lastActivityAt,
1373
+ addressedAt,
1374
+ responseSummary,
1375
+ }
1376
+ })
1377
+ }
1378
+
408
1379
  function projectRoleForSkill(skillId: string, graph: SkillGraph): CognitiveRole {
409
1380
  const node = graph.nodes.find((entry) => entry.id === skillId)
410
1381
  if (!node) return 'hybrid'
411
1382
  return inferCognitiveRole(node)
412
1383
  }
413
1384
 
1385
+ export function loadPressureMotifs(): ResolvedPressureMotif[] {
1386
+ const resolvedSignals = loadResolvedPressureSignals()
1387
+ const interventions = loadPressureInterventions()
1388
+ const transferEvents = loadTransferEvents()
1389
+ const governance = deriveGovernanceSummary()
1390
+ const groups = new Map<string, ResolvedPressureSignal[]>()
1391
+
1392
+ for (const signal of resolvedSignals) {
1393
+ const regionKey = pressureRegionKey(signal)
1394
+ const entries = groups.get(regionKey) ?? []
1395
+ entries.push(signal)
1396
+ groups.set(regionKey, entries)
1397
+ }
1398
+
1399
+ const motifs: ResolvedPressureMotif[] = []
1400
+
1401
+ for (const [regionKey, signals] of groups.entries()) {
1402
+ const projectIds = [...new Set(signals.map((signal) => signal.projectId ?? signal.projectPath).filter(Boolean))] as string[]
1403
+ if (signals.length < 2 && projectIds.length < 2) continue
1404
+
1405
+ const motifId = `motif_${regionKey}`
1406
+ const linkedInterventions = interventions.filter((intervention) => motifMatchesIntervention(motifId, signals, intervention))
1407
+ const linkedTransferEvents = transferEvents.filter((event) => transferMatchesMotif(motifId, signals, event))
1408
+ const activeInterventions = linkedInterventions.filter((intervention) => interventionKeepsPressureActive(intervention.impact, intervention.status))
1409
+ const realizedTransfers = linkedTransferEvents.filter((event) => event.status === 'realized')
1410
+ const allAddressed = signals.every((signal) => signal.lifecycle === 'addressed')
1411
+ const allStale = signals.every((signal) => signal.lifecycle === 'stale')
1412
+ const lifecycle: PressureLifecycleState = realizedTransfers.length > 0 || allAddressed
1413
+ ? 'addressed'
1414
+ : activeInterventions.length > 0
1415
+ ? 'in-progress'
1416
+ : allStale
1417
+ ? 'stale'
1418
+ : 'open'
1419
+ const interventionTypes = [...new Set(linkedInterventions.map((intervention) => intervention.interventionType))]
1420
+ const recommendation = signals.find((signal) => signal.routeRecommendation?.route === 'generalize')?.routeRecommendation
1421
+ ?? signals[0]?.routeRecommendation
1422
+ ?? {
1423
+ route: 'research',
1424
+ scope: projectIds.length >= 2 ? 'network' : 'project',
1425
+ confidence: 0.5,
1426
+ governanceMode: governance.activeMode,
1427
+ triggeredBy: 'insufficient-evidence',
1428
+ reasons: ['Recurring pressure exists, but routing evidence remains coarse.'],
1429
+ }
1430
+ const lastActivityAt = [
1431
+ ...signals.map((signal) => signal.lastActivityAt),
1432
+ ...linkedInterventions.map((intervention) => intervention.completedAt ?? intervention.createdAt),
1433
+ ...linkedTransferEvents.map((event) => event.timestamp),
1434
+ ].sort((a, b) => b.localeCompare(a))[0] ?? signals[0]?.lastActivityAt ?? new Date(0).toISOString()
1435
+ const addressedAt = [
1436
+ ...realizedTransfers.map((event) => event.timestamp),
1437
+ ...signals.map((signal) => signal.addressedAt).filter(Boolean) as string[],
1438
+ ].sort((a, b) => b.localeCompare(a))[0]
1439
+ const capability = signals.find((signal) => Boolean(signal.capability))?.capability
1440
+ const description = signals.find((signal) => Boolean(signal.description))?.description
1441
+ ?? `Recurring pressure in ${capability ?? regionKey}`
1442
+ const sourceSkillIds = [...new Set(signals.flatMap((signal) => signal.skillIds ?? []))]
1443
+ const linkedInterventionIds = linkedInterventions.map((intervention) => intervention.id)
1444
+ const linkedTransferEventIds = linkedTransferEvents.map((event) => event.id)
1445
+ const highPriorityCount = signals.filter((signal) => signal.priority === 'high').length
1446
+ const responseSummary = lifecycle === 'addressed'
1447
+ ? realizedTransfers.length > 0
1448
+ ? 'Promoted into reusable transfer evidence'
1449
+ : 'All linked pressure in this recurring region is addressed'
1450
+ : lifecycle === 'in-progress'
1451
+ ? `Promotion or response underway via ${interventionTypes.join(', ') || 'linked intervention'}`
1452
+ : lifecycle === 'stale'
1453
+ ? 'Recurring pressure has been superseded by newer equivalent signals'
1454
+ : `Recurring demand is waiting for ${recommendation.route}`
1455
+
1456
+ motifs.push({
1457
+ id: motifId,
1458
+ key: regionKey,
1459
+ kind: signals[0]?.kind ?? regionKey,
1460
+ capability,
1461
+ description,
1462
+ createdAt: signals.map((signal) => signal.detectedAt).sort()[0] ?? lastActivityAt,
1463
+ lastSeenAt: signals.map((signal) => signal.detectedAt).sort((a, b) => b.localeCompare(a))[0] ?? lastActivityAt,
1464
+ projectIds,
1465
+ pressureSignalIds: signals.map((signal) => signal.id),
1466
+ sourceSkillIds,
1467
+ recurrenceCount: signals.length,
1468
+ highPriorityCount,
1469
+ linkedInterventionIds,
1470
+ linkedTransferEventIds,
1471
+ suggestedRoute: recommendation,
1472
+ lifecycle,
1473
+ interventionTypes,
1474
+ lastActivityAt,
1475
+ addressedAt,
1476
+ responseSummary,
1477
+ })
1478
+ }
1479
+
1480
+ return motifs.sort((a, b) => {
1481
+ const lifecycleWeight = (value: PressureLifecycleState) => value === 'open' ? 4 : value === 'in-progress' ? 3 : value === 'addressed' ? 2 : 1
1482
+ return lifecycleWeight(b.lifecycle) - lifecycleWeight(a.lifecycle)
1483
+ || b.highPriorityCount - a.highPriorityCount
1484
+ || b.recurrenceCount - a.recurrenceCount
1485
+ || b.lastActivityAt.localeCompare(a.lastActivityAt)
1486
+ })
1487
+ }
1488
+
414
1489
  export function loadCoEvolutionSummary(): CoEvolutionDashboardSummary {
415
1490
  const graph = loadGraph()
416
1491
  const pressureSignals = loadPressureSignals()
1492
+ const resolvedSignals = loadResolvedPressureSignals()
1493
+ const pressureInterventions = loadPressureInterventions()
1494
+ const pressureMotifs = loadPressureMotifs()
1495
+ const transferEvents = loadTransferEvents()
1496
+ const governance = getActiveGovernanceSummary()
1497
+ const topologyReviews = getTopologyReviewSummary()
1498
+ const topologyExecution = getTopologyExecutionSummary()
417
1499
  const rolePressure = emptyCounts(['generalist', 'specialist', 'hybrid'] as const)
418
1500
  const projectBuckets = new Map<string, ProjectPressureSummary>()
1501
+ const projectResponses = new Map<string, ProjectResponseSummary>()
419
1502
  const areaProjects = new Map<string, Set<string>>()
420
1503
  const recommendationCounts = new Map<'research' | 'specialize' | 'create', number>()
1504
+ const routeCounts = new Map<PressureInterventionType, number>()
1505
+ const interventionLaneMap = new Map<PressureInterventionType, { type: PressureInterventionType; total: number; resolving: number; exploratory: number; none: number }>()
421
1506
 
422
1507
  for (const signal of pressureSignals) {
423
1508
  for (const skillId of signal.skillIds ?? []) {
@@ -454,10 +1539,63 @@ export function loadCoEvolutionSummary(): CoEvolutionDashboardSummary {
454
1539
  }
455
1540
  }
456
1541
 
1542
+ for (const signal of resolvedSignals) {
1543
+ if (signal.routeRecommendation) {
1544
+ routeCounts.set(signal.routeRecommendation.route, (routeCounts.get(signal.routeRecommendation.route) ?? 0) + 1)
1545
+ }
1546
+ if (!signal.projectId) continue
1547
+ const existing = projectResponses.get(signal.projectId) ?? {
1548
+ ...(projectBuckets.get(signal.projectId) ?? {
1549
+ projectId: signal.projectId,
1550
+ totalSignals: 0,
1551
+ nativeSignals: 0,
1552
+ derivedSignals: 0,
1553
+ highPrioritySignals: 0,
1554
+ topCapabilities: [],
1555
+ }),
1556
+ openSignals: 0,
1557
+ inProgressSignals: 0,
1558
+ addressedSignals: 0,
1559
+ staleSignals: 0,
1560
+ activeInterventions: 0,
1561
+ recentInterventionTypes: [],
1562
+ }
1563
+ if (signal.lifecycle === 'open') existing.openSignals += 1
1564
+ if (signal.lifecycle === 'in-progress') existing.inProgressSignals += 1
1565
+ if (signal.lifecycle === 'addressed') existing.addressedSignals += 1
1566
+ if (signal.lifecycle === 'stale') existing.staleSignals += 1
1567
+ existing.activeInterventions += signal.linkedInterventions.filter((intervention) => interventionKeepsPressureActive(intervention.impact, intervention.status)).length
1568
+ for (const type of signal.interventionTypes) {
1569
+ if (!existing.recentInterventionTypes.includes(type) && existing.recentInterventionTypes.length < 3) {
1570
+ existing.recentInterventionTypes.push(type)
1571
+ }
1572
+ }
1573
+ projectResponses.set(signal.projectId, existing)
1574
+ }
1575
+
1576
+ for (const intervention of pressureInterventions) {
1577
+ const lane = interventionLaneMap.get(intervention.interventionType) ?? {
1578
+ type: intervention.interventionType,
1579
+ total: 0,
1580
+ resolving: 0,
1581
+ exploratory: 0,
1582
+ none: 0,
1583
+ }
1584
+ lane.total += 1
1585
+ if (intervention.impact === 'resolving') lane.resolving += 1
1586
+ else if (intervention.impact === 'exploratory' || intervention.impact === 'structural') lane.exploratory += 1
1587
+ else lane.none += 1
1588
+ interventionLaneMap.set(intervention.interventionType, lane)
1589
+ }
1590
+
457
1591
  const topProjects = [...projectBuckets.values()]
458
1592
  .sort((a, b) => b.totalSignals - a.totalSignals || b.highPrioritySignals - a.highPrioritySignals)
459
1593
  .slice(0, 6)
460
1594
 
1595
+ const responseProjects = [...projectResponses.values()]
1596
+ .sort((a, b) => (b.openSignals + b.inProgressSignals) - (a.openSignals + a.inProgressSignals) || b.highPrioritySignals - a.highPrioritySignals)
1597
+ .slice(0, 8)
1598
+
461
1599
  const crossProjectGapAreas = [...areaProjects.entries()]
462
1600
  .filter(([, projects]) => projects.size >= 2)
463
1601
  .map(([area, projects]) => ({ area, projects: [...projects], count: projects.size }))
@@ -468,16 +1606,98 @@ export function loadCoEvolutionSummary(): CoEvolutionDashboardSummary {
468
1606
  .map(([action, count]) => ({ action, count }))
469
1607
  .sort((a, b) => b.count - a.count)
470
1608
 
1609
+ const governedRoutes = [...routeCounts.entries()]
1610
+ .map(([route, total]) => ({ route, total }))
1611
+ .sort((a, b) => b.total - a.total || a.route.localeCompare(b.route))
1612
+
1613
+ const interventionLanes = [...interventionLaneMap.values()]
1614
+ .sort((a, b) => b.total - a.total || a.type.localeCompare(b.type))
1615
+
1616
+ const backlog = resolvedSignals
1617
+ .filter((signal) => signal.lifecycle === 'open' || signal.lifecycle === 'in-progress')
1618
+ .sort((a, b) => {
1619
+ const priorityScore = (value?: string) => value === 'high' ? 3 : value === 'medium' ? 2 : 1
1620
+ return priorityScore(b.priority) - priorityScore(a.priority) || b.severity - a.severity || b.detectedAt.localeCompare(a.detectedAt)
1621
+ })
1622
+ .slice(0, 12)
1623
+
1624
+ const promoteQueue = pressureMotifs
1625
+ .filter((motif) => motif.lifecycle !== 'addressed' && motif.suggestedRoute.route === 'generalize')
1626
+ .slice(0, 8)
1627
+
1628
+ const recentAddressed = resolvedSignals
1629
+ .filter((signal) => signal.lifecycle === 'addressed')
1630
+ .sort((a, b) => (b.addressedAt ?? b.lastActivityAt).localeCompare(a.addressedAt ?? a.lastActivityAt))
1631
+ .slice(0, 8)
1632
+
1633
+ const recentInterventions = pressureInterventions.slice(0, 8)
1634
+ const recentTransfers = transferEvents.slice(0, 8)
1635
+
471
1636
  return {
472
1637
  pressureSignals: {
473
1638
  total: pressureSignals.length,
474
1639
  native: pressureSignals.filter((signal) => signal.provenance === 'failure-native' || signal.provenance === 'project-analysis-native').length,
475
1640
  derived: pressureSignals.filter((signal) => signal.provenance === 'failure-derived' || signal.provenance === 'profile-gap-derived').length,
476
1641
  },
1642
+ pressureInterventions: {
1643
+ total: pressureInterventions.length,
1644
+ started: pressureInterventions.filter((intervention) => intervention.status === 'started').length,
1645
+ completed: pressureInterventions.filter((intervention) => intervention.status === 'completed').length,
1646
+ failed: pressureInterventions.filter((intervention) => intervention.status === 'failed').length,
1647
+ dryRun: pressureInterventions.filter((intervention) => intervention.status === 'dry-run').length,
1648
+ },
1649
+ pressureLifecycle: {
1650
+ open: resolvedSignals.filter((signal) => signal.lifecycle === 'open').length,
1651
+ inProgress: resolvedSignals.filter((signal) => signal.lifecycle === 'in-progress').length,
1652
+ addressed: resolvedSignals.filter((signal) => signal.lifecycle === 'addressed').length,
1653
+ stale: resolvedSignals.filter((signal) => signal.lifecycle === 'stale').length,
1654
+ highPriorityOpen: resolvedSignals.filter((signal) => signal.lifecycle !== 'addressed' && signal.priority === 'high').length,
1655
+ },
1656
+ pressureMotifs: {
1657
+ total: pressureMotifs.length,
1658
+ open: pressureMotifs.filter((motif) => motif.lifecycle === 'open').length,
1659
+ inProgress: pressureMotifs.filter((motif) => motif.lifecycle === 'in-progress').length,
1660
+ addressed: pressureMotifs.filter((motif) => motif.lifecycle === 'addressed').length,
1661
+ stale: pressureMotifs.filter((motif) => motif.lifecycle === 'stale').length,
1662
+ promotionReady: pressureMotifs.filter((motif) => motif.lifecycle !== 'addressed' && motif.suggestedRoute.route === 'generalize').length,
1663
+ },
1664
+ governance,
1665
+ topologyReviews: {
1666
+ total: topologyReviews.total,
1667
+ open: topologyReviews.open,
1668
+ accepted: topologyReviews.accepted,
1669
+ rejected: topologyReviews.rejected,
1670
+ deferred: topologyReviews.deferred,
1671
+ highPriorityOpen: topologyReviews.highPriorityOpen,
1672
+ generatedFromManualReview: topologyReviews.generatedFromManualReview,
1673
+ byChangeType: topologyReviews.byChangeType,
1674
+ },
1675
+ topologyExecution: {
1676
+ plans: topologyExecution.plans,
1677
+ pending: topologyExecution.pending,
1678
+ prepared: topologyExecution.prepared,
1679
+ applied: topologyExecution.applied,
1680
+ rolledBack: topologyExecution.rolledBack,
1681
+ failed: topologyExecution.failed,
1682
+ safeToApply: topologyExecution.safeToApply,
1683
+ prepareOnly: topologyExecution.prepareOnly,
1684
+ snapshots: topologyExecution.snapshots,
1685
+ artifacts: topologyExecution.artifacts,
1686
+ },
477
1687
  topProjects,
1688
+ responseProjects,
478
1689
  rolePressure,
479
1690
  crossProjectGapAreas,
480
1691
  activeRecommendations,
1692
+ governedRoutes,
1693
+ interventionLanes,
1694
+ backlog,
1695
+ promoteQueue,
1696
+ recentAddressed,
1697
+ recentInterventions,
1698
+ recentTransfers,
1699
+ topologyBacklog: topologyReviews.backlog,
1700
+ recentTopologyDecisions: topologyReviews.recentDecisions,
481
1701
  }
482
1702
  }
483
1703
 
@@ -556,9 +1776,23 @@ export function loadOntologySummary(): OntologyDashboardSummary {
556
1776
  const artifacts = loadEvolutionArtifacts()
557
1777
  const activationTraces = loadActivationTraces()
558
1778
  const pressureSignals = loadPressureSignals()
1779
+ const pressureInterventions = loadPressureInterventions()
1780
+ const resolvedPressureSignals = loadResolvedPressureSignals()
1781
+ const pressureMotifs = loadPressureMotifs()
1782
+ const transferEvents = loadTransferEvents()
1783
+ const governance = getActiveGovernanceSummary()
1784
+ const topologyReviews = getTopologyReviewSummary()
1785
+ const topologyExecution = getTopologyExecutionSummary()
559
1786
  const skillRoles = emptyCounts(['generalist', 'specialist', 'hybrid'] as const)
560
1787
  const stabilityStates = emptyCounts(['stable', 'adaptive', 'experimental'] as const)
561
1788
  const plasticityStates = emptyCounts(['consolidated', 'volatile', 'candidate'] as const)
1789
+ const governedRoutes: Record<PressureInterventionType, number> = {
1790
+ research: 0,
1791
+ specialize: 0,
1792
+ evolve: 0,
1793
+ generalize: 0,
1794
+ 'manual-review': 0,
1795
+ }
562
1796
 
563
1797
  for (const node of graph.nodes) {
564
1798
  skillRoles[inferCognitiveRole(node)] += 1
@@ -566,6 +1800,10 @@ export function loadOntologySummary(): OntologyDashboardSummary {
566
1800
  plasticityStates[inferPlasticityState(node)] += 1
567
1801
  }
568
1802
 
1803
+ for (const signal of resolvedPressureSignals) {
1804
+ if (signal.routeRecommendation) governedRoutes[signal.routeRecommendation.route] += 1
1805
+ }
1806
+
569
1807
  const proposals = history.iterations.flatMap((iteration) => iteration.proposals)
570
1808
  const observedMutationActions = [...new Set(proposals.map((proposal) => proposal.action))].sort()
571
1809
  const evidenceBackedProposals = proposals.filter((proposal) => {
@@ -607,6 +1845,66 @@ export function loadOntologySummary(): OntologyDashboardSummary {
607
1845
  native: pressureSignals.filter((signal) => signal.provenance === 'failure-native' || signal.provenance === 'project-analysis-native').length,
608
1846
  derived: pressureSignals.filter((signal) => signal.provenance === 'failure-derived' || signal.provenance === 'profile-gap-derived').length,
609
1847
  },
1848
+ pressureInterventions: {
1849
+ total: pressureInterventions.length,
1850
+ started: pressureInterventions.filter((intervention) => intervention.status === 'started').length,
1851
+ completed: pressureInterventions.filter((intervention) => intervention.status === 'completed').length,
1852
+ failed: pressureInterventions.filter((intervention) => intervention.status === 'failed').length,
1853
+ dryRun: pressureInterventions.filter((intervention) => intervention.status === 'dry-run').length,
1854
+ },
1855
+ pressureLifecycle: {
1856
+ open: resolvedPressureSignals.filter((signal) => signal.lifecycle === 'open').length,
1857
+ inProgress: resolvedPressureSignals.filter((signal) => signal.lifecycle === 'in-progress').length,
1858
+ addressed: resolvedPressureSignals.filter((signal) => signal.lifecycle === 'addressed').length,
1859
+ stale: resolvedPressureSignals.filter((signal) => signal.lifecycle === 'stale').length,
1860
+ },
1861
+ pressureMotifs: {
1862
+ total: pressureMotifs.length,
1863
+ open: pressureMotifs.filter((motif) => motif.lifecycle === 'open').length,
1864
+ inProgress: pressureMotifs.filter((motif) => motif.lifecycle === 'in-progress').length,
1865
+ addressed: pressureMotifs.filter((motif) => motif.lifecycle === 'addressed').length,
1866
+ stale: pressureMotifs.filter((motif) => motif.lifecycle === 'stale').length,
1867
+ promotionReady: pressureMotifs.filter((motif) => motif.lifecycle !== 'addressed' && motif.suggestedRoute.route === 'generalize').length,
1868
+ },
1869
+ governedRoutes,
1870
+ transferEvents: {
1871
+ total: transferEvents.length,
1872
+ realized: transferEvents.filter((event) => event.status === 'realized').length,
1873
+ candidate: transferEvents.filter((event) => event.status === 'candidate').length,
1874
+ rejected: transferEvents.filter((event) => event.status === 'rejected').length,
1875
+ },
1876
+ governance: {
1877
+ activeMode: governance.activeMode,
1878
+ derivedMode: governance.derivedMode,
1879
+ source: governance.source,
1880
+ manualMode: governance.manualMode,
1881
+ rationale: governance.rationale,
1882
+ reviewThreshold: governance.profile.reviewThreshold,
1883
+ riskTolerance: governance.profile.riskTolerance,
1884
+ plasticityBias: governance.profile.plasticityBias,
1885
+ },
1886
+ topologyReviews: {
1887
+ total: topologyReviews.total,
1888
+ open: topologyReviews.open,
1889
+ accepted: topologyReviews.accepted,
1890
+ rejected: topologyReviews.rejected,
1891
+ deferred: topologyReviews.deferred,
1892
+ highPriorityOpen: topologyReviews.highPriorityOpen,
1893
+ generatedFromManualReview: topologyReviews.generatedFromManualReview,
1894
+ byChangeType: topologyReviews.byChangeType,
1895
+ },
1896
+ topologyExecution: {
1897
+ plans: topologyExecution.plans,
1898
+ pending: topologyExecution.pending,
1899
+ prepared: topologyExecution.prepared,
1900
+ applied: topologyExecution.applied,
1901
+ rolledBack: topologyExecution.rolledBack,
1902
+ failed: topologyExecution.failed,
1903
+ safeToApply: topologyExecution.safeToApply,
1904
+ prepareOnly: topologyExecution.prepareOnly,
1905
+ snapshots: topologyExecution.snapshots,
1906
+ artifacts: topologyExecution.artifacts,
1907
+ },
610
1908
  annotatedFailures: {
611
1909
  pressureSignals: failures.filter((failure) => (failure.pressureSignals?.length ?? 0) > 0).length,
612
1910
  capabilityGaps: failures.filter((failure) => (failure.capabilityGaps?.length ?? 0) > 0).length,