224 lines
7.8 KiB
Python
224 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
|
"""dependency_tracker.py — Auto-propagation system for umbrella dependencies.
|
|
|
|
When a component changes, automatically updates dependent components.
|
|
Maps the dependency graph between umbrella systems and fires
|
|
propagation rules when changes are detected.
|
|
|
|
Usage:
|
|
dependency_tracker.py check <changed_file>
|
|
dependency_tracker.py propagate <changed_file>
|
|
dependency_tracker.py map
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from typing import Dict, List, Set
|
|
|
|
SCRIPTS_DIR = os.path.expanduser("~/.config/opencode/scripts")
|
|
DEPENDENCY_MAP_FILE = os.path.expanduser("~/.config/opencode/memory/dependency_map.json")
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# DEPENDENCY GRAPH
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
# Maps file → list of files that depend on it
|
|
DEPENDENCY_GRAPH = {
|
|
# MEMORY umbrella
|
|
"memory_store.py": [
|
|
"warm_router.py", "curator.py", "memory_consolidation.py",
|
|
"drift_core.py", "drift_cache.py", "tier_manager.py", "shard_manager.py",
|
|
"query_memory.py", "growth_simulator.py"
|
|
],
|
|
"memory_consolidation.py": [
|
|
"memory_store.py", "drift_compression.py", "curator.py"
|
|
],
|
|
"warm_router.py": [
|
|
"capability_broker.py", "memory_consolidation.py", "memory_store.py"
|
|
],
|
|
"graph_memory.py": [
|
|
"warm_router.py", "memory_consolidation.py", "shard_manager.py"
|
|
],
|
|
"shard_manager.py": [
|
|
"memory_store.py", "tier_manager.py"
|
|
],
|
|
"tier_manager.py": [
|
|
"memory_store.py", "drift_cache.py"
|
|
],
|
|
"query_memory.py": [
|
|
"memory_store.py", "growth_simulator.py"
|
|
],
|
|
|
|
# GOVERNANCE umbrella
|
|
"drift_core.py": [
|
|
"curator.py", "audit_ledger.py", "memory_store.py",
|
|
"drift_observability.py", "drift_cache.py"
|
|
],
|
|
"curator.py": [
|
|
"memory_store.py", "drift_core.py", "audit_ledger.py"
|
|
],
|
|
"drift_compression.py": [
|
|
"memory_consolidation.py", "curator.py"
|
|
],
|
|
"drift_observability.py": [
|
|
"drift_core.py", "audit_ledger.py"
|
|
],
|
|
"audit_ledger.py": [
|
|
"curator.py", "drift_core.py"
|
|
],
|
|
|
|
# IDENTITY umbrella
|
|
"persona_definition.py": [
|
|
"persona_check.py", "identity_anchors.py", "drift_observability.py"
|
|
],
|
|
"identity_anchors.py": [
|
|
"persona_definition.py", "drift_core.py"
|
|
],
|
|
"voice_hybrid.py": [
|
|
"delivery_adapter.py", "persona_definition.py"
|
|
],
|
|
|
|
# GROWTH umbrella
|
|
"growth_delta.py": [
|
|
"memory_store.py", "curator.py", "lessons.py"
|
|
],
|
|
"growth_simulator.py": [
|
|
"memory_store.py", "query_memory.py", "drift_core.py"
|
|
],
|
|
|
|
# PERCEPTION umbrella
|
|
"warm_router.py": [
|
|
"capability_broker.py", "memory_consolidation.py"
|
|
],
|
|
|
|
# INFRASTRUCTURE
|
|
"microkernel.py": [
|
|
"hook_engine.py", "event_bus.py"
|
|
],
|
|
}
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# PROPAGATION ACTIONS
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
def get_dependents(changed_file: str) -> List[str]:
|
|
"""Get files that depend on the changed file."""
|
|
basename = os.path.basename(changed_file)
|
|
return DEPENDENCY_GRAPH.get(basename, [])
|
|
|
|
|
|
def check_propagation(changed_file: str) -> dict:
|
|
"""Check what needs to be updated when a file changes."""
|
|
dependents = get_dependents(changed_file)
|
|
return {
|
|
"changed_file": os.path.basename(changed_file),
|
|
"dependents": dependents,
|
|
"count": len(dependents),
|
|
}
|
|
|
|
|
|
def propagate(changed_file: str, dry_run: bool = False) -> dict:
|
|
"""Propagate changes to dependent files.
|
|
|
|
Returns dict with propagation results.
|
|
"""
|
|
dependents = get_dependents(changed_file)
|
|
results = {
|
|
"changed_file": os.path.basename(changed_file),
|
|
"propagated": [],
|
|
"skipped": [],
|
|
"errors": [],
|
|
}
|
|
|
|
for dep in dependents:
|
|
dep_path = os.path.join(SCRIPTS_DIR, dep)
|
|
if not os.path.exists(dep_path):
|
|
results["skipped"].append({"file": dep, "reason": "not found"})
|
|
continue
|
|
|
|
if dry_run:
|
|
results["propagated"].append({"file": dep, "action": "would update"})
|
|
continue
|
|
|
|
# Check if the dependent file imports from the changed file
|
|
try:
|
|
with open(dep_path) as f:
|
|
content = f.read()
|
|
|
|
changed_basename = os.path.basename(changed_file).replace(".py", "")
|
|
if changed_basename in content:
|
|
results["propagated"].append({
|
|
"file": dep,
|
|
"action": "references changed file",
|
|
"detail": f"imports or references {changed_basename}"
|
|
})
|
|
else:
|
|
results["skipped"].append({"file": dep, "reason": "no direct reference"})
|
|
except Exception as e:
|
|
results["errors"].append({"file": dep, "error": str(e)})
|
|
|
|
return results
|
|
|
|
|
|
def get_full_map() -> dict:
|
|
"""Get the full dependency map."""
|
|
return DEPENDENCY_GRAPH
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
# CLI
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
def main():
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description="Dependency tracker for umbrella systems")
|
|
sub = parser.add_subparsers(dest="cmd")
|
|
|
|
p_check = sub.add_parser("check", help="Check what depends on a file")
|
|
p_check.add_argument("file")
|
|
|
|
p_prop = sub.add_parser("propagate", help="Propagate changes to dependents")
|
|
p_prop.add_argument("file")
|
|
p_prop.add_argument("--dry-run", action="store_true")
|
|
|
|
sub.add_parser("map", help="Show full dependency map")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if not args.cmd:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
if args.cmd == "check":
|
|
result = check_propagation(args.file)
|
|
print(f"File: {result['changed_file']}")
|
|
print(f"Dependents ({result['count']}):")
|
|
for dep in result['dependents']:
|
|
print(f" → {dep}")
|
|
|
|
elif args.cmd == "propagate":
|
|
result = propagate(args.file, dry_run=args.dry_run)
|
|
print(f"Propagation for: {result['changed_file']}")
|
|
print(f"Propagated ({len(result['propagated'])}):")
|
|
for p in result['propagated']:
|
|
print(f" ✓ {p['file']}: {p['action']}")
|
|
print(f"Skipped ({len(result['skipped'])}):")
|
|
for s in result['skipped']:
|
|
print(f" ⊘ {s['file']}: {s['reason']}")
|
|
if result['errors']:
|
|
print(f"Errors ({len(result['errors'])}):")
|
|
for e in result['errors']:
|
|
print(f" ✗ {e['file']}: {e['error']}")
|
|
|
|
elif args.cmd == "map":
|
|
dep_map = get_full_map()
|
|
print("Dependency Map:")
|
|
for file, deps in dep_map.items():
|
|
print(f" {file} → {', '.join(deps)}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|