Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 8s
PR Checklist / pr-checklist (pull_request) Failing after 1m12s
Smoke Test / smoke (pull_request) Failing after 7s
Validate Config / YAML Lint (pull_request) Failing after 6s
Validate Config / JSON Validate (pull_request) Successful in 5s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 7s
Validate Config / Shell Script Lint (pull_request) Successful in 14s
Validate Config / Cron Syntax Check (pull_request) Successful in 4s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 4s
Validate Config / Playbook Schema Validation (pull_request) Successful in 7s
Architecture Lint / Lint Repository (pull_request) Failing after 6s
307 lines
10 KiB
Python
307 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""temporal_reasoner.py - GOFAI temporal reasoning engine for the Timmy Foundation fleet.
|
|
|
|
A symbolic temporal constraint network (TCN) for scheduling and ordering events.
|
|
Models Allen's interval algebra relations (before, after, meets, overlaps, etc.)
|
|
and propagates temporal constraints via path-consistency to detect conflicts.
|
|
No ML, no embeddings - just constraint propagation over a temporal graph.
|
|
|
|
Core concepts:
|
|
TimePoint: A named instant on a symbolic timeline.
|
|
Interval: A pair of time-points (start, end) with start < end.
|
|
Constraint: A relation between two time-points or intervals
|
|
(e.g. A.before(B), A.meets(B)).
|
|
|
|
Usage (Python API):
|
|
from temporal_reasoner import TemporalNetwork, Interval
|
|
tn = TemporalNetwork()
|
|
deploy = tn.add_interval('deploy', duration=(10, 30))
|
|
test = tn.add_interval('test', duration=(5, 15))
|
|
tn.add_constraint(deploy, 'before', test)
|
|
consistent = tn.propagate()
|
|
|
|
CLI:
|
|
python temporal_reasoner.py --demo
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import Dict, List, Optional, Set, Tuple
|
|
|
|
INF = float('inf')
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Data model
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TimePoint:
|
|
"""A named instant on the timeline."""
|
|
name: str
|
|
id: int = field(default=0)
|
|
|
|
def __str__(self) -> str:
|
|
return self.name
|
|
|
|
|
|
@dataclass
|
|
class Interval:
|
|
"""A named interval bounded by two time-points."""
|
|
name: str
|
|
start: int # index into the distance matrix
|
|
end: int # index into the distance matrix
|
|
|
|
def __str__(self) -> str:
|
|
return self.name
|
|
|
|
|
|
class Relation(Enum):
|
|
"""Allen's interval algebra relations (simplified subset)."""
|
|
BEFORE = 'before'
|
|
AFTER = 'after'
|
|
MEETS = 'meets'
|
|
MET_BY = 'met_by'
|
|
OVERLAPS = 'overlaps'
|
|
DURING = 'during'
|
|
EQUALS = 'equals'
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Simple Temporal Network (STN) via distance matrix
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TemporalNetwork:
|
|
"""Simple Temporal Network with Floyd-Warshall propagation.
|
|
|
|
Internally maintains a distance matrix D where D[i][j] is the
|
|
maximum allowed distance from time-point i to time-point j.
|
|
Negative cycles indicate inconsistency.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self._n = 0
|
|
self._names: List[str] = []
|
|
self._dist: List[List[float]] = []
|
|
self._intervals: Dict[str, Interval] = {}
|
|
self._origin_idx: int = -1
|
|
self._add_point('origin')
|
|
self._origin_idx = 0
|
|
|
|
# ------------------------------------------------------------------
|
|
# Point management
|
|
# ------------------------------------------------------------------
|
|
|
|
def _add_point(self, name: str) -> int:
|
|
"""Add a time-point and return its index."""
|
|
idx = self._n
|
|
self._n += 1
|
|
self._names.append(name)
|
|
# Extend distance matrix
|
|
for row in self._dist:
|
|
row.append(INF)
|
|
self._dist.append([INF] * self._n)
|
|
self._dist[idx][idx] = 0.0
|
|
return idx
|
|
|
|
# ------------------------------------------------------------------
|
|
# Interval management
|
|
# ------------------------------------------------------------------
|
|
|
|
def add_interval(
|
|
self,
|
|
name: str,
|
|
duration: Optional[Tuple[float, float]] = None,
|
|
) -> Interval:
|
|
"""Add a named interval with optional duration bounds [lo, hi].
|
|
|
|
Returns the Interval object with start/end indices.
|
|
"""
|
|
s = self._add_point(f"{name}.start")
|
|
e = self._add_point(f"{name}.end")
|
|
# start < end (at least 1 time unit)
|
|
self._dist[s][e] = min(self._dist[s][e], duration[1] if duration else INF)
|
|
self._dist[e][s] = min(self._dist[e][s], -(duration[0] if duration else 1))
|
|
interval = Interval(name=name, start=s, end=e)
|
|
self._intervals[name] = interval
|
|
return interval
|
|
|
|
# ------------------------------------------------------------------
|
|
# Constraint management
|
|
# ------------------------------------------------------------------
|
|
|
|
def add_distance_constraint(
|
|
self, i: int, j: int, lo: float, hi: float
|
|
) -> None:
|
|
"""Add constraint: lo <= t_j - t_i <= hi."""
|
|
self._dist[i][j] = min(self._dist[i][j], hi)
|
|
self._dist[j][i] = min(self._dist[j][i], -lo)
|
|
|
|
def add_constraint(
|
|
self, a: Interval, relation: str, b: Interval, gap: Tuple[float, float] = (0, INF)
|
|
) -> None:
|
|
"""Add an Allen-style relation between two intervals.
|
|
|
|
Supported relations: before, after, meets, met_by, equals.
|
|
"""
|
|
rel = relation.lower()
|
|
if rel == 'before':
|
|
# a.end + gap <= b.start
|
|
self.add_distance_constraint(a.end, b.start, gap[0], gap[1])
|
|
elif rel == 'after':
|
|
self.add_distance_constraint(b.end, a.start, gap[0], gap[1])
|
|
elif rel == 'meets':
|
|
# a.end == b.start
|
|
self.add_distance_constraint(a.end, b.start, 0, 0)
|
|
elif rel == 'met_by':
|
|
self.add_distance_constraint(b.end, a.start, 0, 0)
|
|
elif rel == 'equals':
|
|
self.add_distance_constraint(a.start, b.start, 0, 0)
|
|
self.add_distance_constraint(a.end, b.end, 0, 0)
|
|
else:
|
|
raise ValueError(f"Unsupported relation: {relation}")
|
|
|
|
# ------------------------------------------------------------------
|
|
# Propagation (Floyd-Warshall)
|
|
# ------------------------------------------------------------------
|
|
|
|
def propagate(self) -> bool:
|
|
"""Run Floyd-Warshall to propagate all constraints.
|
|
|
|
Returns True if the network is consistent (no negative cycles).
|
|
"""
|
|
n = self._n
|
|
d = self._dist
|
|
for k in range(n):
|
|
for i in range(n):
|
|
for j in range(n):
|
|
if d[i][k] + d[k][j] < d[i][j]:
|
|
d[i][j] = d[i][k] + d[k][j]
|
|
# Check for negative cycles
|
|
for i in range(n):
|
|
if d[i][i] < 0:
|
|
return False
|
|
return True
|
|
|
|
def is_consistent(self) -> bool:
|
|
"""Check consistency without mutating (copies matrix first)."""
|
|
import copy
|
|
saved = copy.deepcopy(self._dist)
|
|
result = self.propagate()
|
|
self._dist = saved
|
|
return result
|
|
|
|
# ------------------------------------------------------------------
|
|
# Query
|
|
# ------------------------------------------------------------------
|
|
|
|
def earliest(self, point_idx: int) -> float:
|
|
"""Earliest possible time for a point (relative to origin)."""
|
|
return -self._dist[point_idx][self._origin_idx]
|
|
|
|
def latest(self, point_idx: int) -> float:
|
|
"""Latest possible time for a point (relative to origin)."""
|
|
return self._dist[self._origin_idx][point_idx]
|
|
|
|
def interval_bounds(self, interval: Interval) -> Dict[str, Tuple[float, float]]:
|
|
"""Return earliest/latest start and end for an interval."""
|
|
return {
|
|
'start': (self.earliest(interval.start), self.latest(interval.start)),
|
|
'end': (self.earliest(interval.end), self.latest(interval.end)),
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# Display
|
|
# ------------------------------------------------------------------
|
|
|
|
def dump(self) -> None:
|
|
"""Print the current distance matrix and interval bounds."""
|
|
print(f"Temporal Network — {self._n} time-points, {len(self._intervals)} intervals")
|
|
print()
|
|
for name, interval in self._intervals.items():
|
|
bounds = self.interval_bounds(interval)
|
|
s_lo, s_hi = bounds['start']
|
|
e_lo, e_hi = bounds['end']
|
|
print(f" {name}:")
|
|
print(f" start: [{s_lo:.1f}, {s_hi:.1f}]")
|
|
print(f" end: [{e_lo:.1f}, {e_hi:.1f}]")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Demo: Timmy fleet deployment pipeline
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def run_demo() -> None:
|
|
"""Run a demo temporal reasoning scenario for the Timmy fleet."""
|
|
print("=" * 60)
|
|
print("Temporal Reasoner Demo - Fleet Deployment Pipeline")
|
|
print("=" * 60)
|
|
print()
|
|
|
|
tn = TemporalNetwork()
|
|
|
|
# Define pipeline stages with duration bounds [min, max]
|
|
build = tn.add_interval('build', duration=(5, 15))
|
|
test = tn.add_interval('test', duration=(10, 30))
|
|
review = tn.add_interval('review', duration=(2, 10))
|
|
deploy = tn.add_interval('deploy', duration=(1, 5))
|
|
monitor = tn.add_interval('monitor', duration=(20, 60))
|
|
|
|
# Temporal constraints
|
|
tn.add_constraint(build, 'meets', test) # test starts when build ends
|
|
tn.add_constraint(test, 'before', review, gap=(0, 5)) # review within 5 of test
|
|
tn.add_constraint(review, 'meets', deploy) # deploy immediately after review
|
|
tn.add_constraint(deploy, 'before', monitor, gap=(0, 2)) # monitor within 2 of deploy
|
|
|
|
# Global deadline: everything done within 120 time units
|
|
tn.add_distance_constraint(tn._origin_idx, monitor.end, 0, 120)
|
|
|
|
# Build must start within first 10 units
|
|
tn.add_distance_constraint(tn._origin_idx, build.start, 0, 10)
|
|
|
|
print("Constraints added. Propagating...")
|
|
consistent = tn.propagate()
|
|
print(f"Network consistent: {consistent}")
|
|
print()
|
|
|
|
if consistent:
|
|
tn.dump()
|
|
print()
|
|
|
|
# Now add a conflicting constraint to show inconsistency detection
|
|
print("--- Adding conflicting constraint: monitor.before(build) ---")
|
|
tn.add_constraint(monitor, 'before', build)
|
|
consistent2 = tn.propagate()
|
|
print(f"Network consistent after conflict: {consistent2}")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description="GOFAI temporal reasoning engine"
|
|
)
|
|
parser.add_argument(
|
|
"--demo",
|
|
action="store_true",
|
|
help="Run the fleet deployment pipeline demo",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
if args.demo or not any(vars(args).values()):
|
|
run_demo()
|
|
else:
|
|
parser.print_help()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |