4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
259 lines
7.7 KiB
Python
259 lines
7.7 KiB
Python
"""Pure upstream-correlation scoring algorithms.
|
|
|
|
Scores time-window, topology, and periodicity signals for upstream candidates.
|
|
All functions are deterministic; output feeds upstream-correlation reporting.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
|
|
from core.domain.correlation.confidence import (
|
|
EvidenceContribution,
|
|
WeightedConfidence,
|
|
build_weighted_confidence,
|
|
)
|
|
from core.domain.types.upstream import (
|
|
HintEvidenceScore,
|
|
PeriodicityScore,
|
|
TimeSeries,
|
|
TimeWindowCorrelation,
|
|
TopologyCorrelation,
|
|
TopologyNode,
|
|
UpstreamCandidate,
|
|
)
|
|
|
|
# Evidence weights for candidate correlation (sum to 1.0).
|
|
TIME_WINDOW_WEIGHT = 0.45
|
|
TOPOLOGY_WEIGHT = 0.30
|
|
PERIODICITY_WEIGHT = 0.10
|
|
FEATURE_WORKFLOW_WEIGHT = 0.15
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CandidateCorrelationScore:
|
|
candidate_name: str
|
|
time_window_score: float
|
|
topology_score: float
|
|
periodicity_score: float
|
|
feature_workflow_score: float
|
|
final_confidence: float
|
|
weighted_confidence: WeightedConfidence
|
|
rationale: str
|
|
|
|
|
|
def _parse_timestamp(value: str) -> datetime:
|
|
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
|
|
|
|
def _trend(values: tuple[float, ...]) -> list[int]:
|
|
trend: list[int] = []
|
|
for previous, current in zip(values, values[1:], strict=False):
|
|
if current > previous:
|
|
trend.append(1)
|
|
elif current < previous:
|
|
trend.append(-1)
|
|
else:
|
|
trend.append(0)
|
|
return trend
|
|
|
|
|
|
def score_time_window_correlation(
|
|
primary: TimeSeries,
|
|
candidate: TimeSeries,
|
|
) -> TimeWindowCorrelation:
|
|
primary_points = {
|
|
_parse_timestamp(timestamp): value
|
|
for timestamp, value in zip(primary.timestamps, primary.values, strict=False)
|
|
}
|
|
candidate_points = {
|
|
_parse_timestamp(timestamp): value
|
|
for timestamp, value in zip(candidate.timestamps, candidate.values, strict=False)
|
|
}
|
|
|
|
common_timestamps = tuple(sorted(set(primary_points) & set(candidate_points)))
|
|
if len(common_timestamps) < 2:
|
|
return TimeWindowCorrelation(
|
|
primary_signal=primary.name,
|
|
candidate_signal=candidate.name,
|
|
aligned_points=len(common_timestamps),
|
|
direction_matches=0,
|
|
score=0.0,
|
|
rationale="Not enough overlapping timestamps to score time-window correlation.",
|
|
)
|
|
|
|
primary_values = tuple(primary_points[timestamp] for timestamp in common_timestamps)
|
|
candidate_values = tuple(candidate_points[timestamp] for timestamp in common_timestamps)
|
|
|
|
primary_trend = _trend(primary_values)
|
|
candidate_trend = _trend(candidate_values)
|
|
|
|
comparable_steps = [
|
|
(primary_step, candidate_step)
|
|
for primary_step, candidate_step in zip(primary_trend, candidate_trend, strict=False)
|
|
if primary_step != 0 or candidate_step != 0
|
|
]
|
|
|
|
if not comparable_steps:
|
|
score = 0.0
|
|
direction_matches = 0
|
|
else:
|
|
direction_matches = sum(
|
|
1 for primary_step, candidate_step in comparable_steps if primary_step == candidate_step
|
|
)
|
|
score = round(direction_matches / len(comparable_steps), 4)
|
|
|
|
return TimeWindowCorrelation(
|
|
primary_signal=primary.name,
|
|
candidate_signal=candidate.name,
|
|
aligned_points=len(common_timestamps),
|
|
direction_matches=direction_matches,
|
|
score=score,
|
|
rationale=(
|
|
f"{candidate.name} matched {direction_matches}/{len(comparable_steps)} "
|
|
f"time-window trend steps against {primary.name}."
|
|
),
|
|
)
|
|
|
|
|
|
def score_topology_adjacency(
|
|
*,
|
|
source: TopologyNode,
|
|
target: TopologyNode,
|
|
) -> TopologyCorrelation:
|
|
if target.name in source.upstream_of:
|
|
return TopologyCorrelation(
|
|
source=source.name,
|
|
target=target.name,
|
|
adjacency_score=1.0,
|
|
rationale=f"{source.name} is topology-adjacent to {target.name}.",
|
|
)
|
|
|
|
return TopologyCorrelation(
|
|
source=source.name,
|
|
target=target.name,
|
|
adjacency_score=0.0,
|
|
rationale=f"{source.name} is not topology-adjacent to {target.name}.",
|
|
)
|
|
|
|
|
|
def score_periodic_spikes(
|
|
*,
|
|
signal_name: str,
|
|
values: tuple[float, ...],
|
|
spike_threshold: float,
|
|
) -> PeriodicityScore:
|
|
repeated_spikes = sum(1 for value in values if value >= spike_threshold)
|
|
|
|
if repeated_spikes <= 1:
|
|
score = 0.0
|
|
rationale = "No repeated spike pattern detected."
|
|
else:
|
|
score = 1.0
|
|
rationale = f"Detected repeated threshold crossings for {signal_name}."
|
|
|
|
return PeriodicityScore(
|
|
signal_name=signal_name,
|
|
repeated_spikes=repeated_spikes,
|
|
score=round(score, 4),
|
|
rationale=rationale,
|
|
)
|
|
|
|
|
|
def score_candidate_correlation(
|
|
*,
|
|
candidate_name: str,
|
|
time_window: TimeWindowCorrelation,
|
|
topology: TopologyCorrelation,
|
|
periodicity: PeriodicityScore | None = None,
|
|
operator_hint: HintEvidenceScore | None = None,
|
|
) -> CandidateCorrelationScore:
|
|
periodicity_score = periodicity.score if periodicity is not None else 0.0
|
|
feature_workflow_score = operator_hint.score if operator_hint is not None else 0.0
|
|
feature_workflow_rationale = (
|
|
operator_hint.rationale
|
|
if operator_hint is not None
|
|
else "No feature/workflow hint evidence."
|
|
)
|
|
|
|
weighted_confidence = build_weighted_confidence(
|
|
(
|
|
EvidenceContribution(
|
|
source="correlation",
|
|
score=time_window.score,
|
|
weight=TIME_WINDOW_WEIGHT,
|
|
rationale=time_window.rationale,
|
|
),
|
|
EvidenceContribution(
|
|
source="topology",
|
|
score=topology.adjacency_score,
|
|
weight=TOPOLOGY_WEIGHT,
|
|
rationale=topology.rationale,
|
|
),
|
|
EvidenceContribution(
|
|
source="periodicity",
|
|
score=periodicity_score,
|
|
weight=PERIODICITY_WEIGHT,
|
|
rationale=(
|
|
periodicity.rationale if periodicity is not None else "No periodicity evidence."
|
|
),
|
|
),
|
|
EvidenceContribution(
|
|
source="feature_workflow",
|
|
score=feature_workflow_score,
|
|
weight=FEATURE_WORKFLOW_WEIGHT,
|
|
rationale=feature_workflow_rationale,
|
|
),
|
|
)
|
|
)
|
|
|
|
return CandidateCorrelationScore(
|
|
candidate_name=candidate_name,
|
|
time_window_score=time_window.score,
|
|
topology_score=topology.adjacency_score,
|
|
periodicity_score=periodicity_score,
|
|
feature_workflow_score=feature_workflow_score,
|
|
final_confidence=weighted_confidence.score,
|
|
weighted_confidence=weighted_confidence,
|
|
rationale=(
|
|
f"confidence={weighted_confidence.label}; "
|
|
f"correlation={time_window.score}, "
|
|
f"topology={topology.adjacency_score}, "
|
|
f"periodicity={periodicity_score}, "
|
|
f"feature_workflow={feature_workflow_score}"
|
|
),
|
|
)
|
|
|
|
|
|
def rank_upstream_candidates(
|
|
candidates: list[UpstreamCandidate],
|
|
*,
|
|
top_n: int | None = None,
|
|
) -> list[UpstreamCandidate]:
|
|
ranked = sorted(
|
|
candidates,
|
|
key=lambda candidate: (-candidate.confidence, candidate.name),
|
|
)
|
|
|
|
if top_n is None:
|
|
return ranked
|
|
if top_n <= 0:
|
|
return []
|
|
|
|
return ranked[:top_n]
|
|
|
|
|
|
__all__ = [
|
|
"CandidateCorrelationScore",
|
|
"FEATURE_WORKFLOW_WEIGHT",
|
|
"PERIODICITY_WEIGHT",
|
|
"TIME_WINDOW_WEIGHT",
|
|
"TOPOLOGY_WEIGHT",
|
|
"rank_upstream_candidates",
|
|
"score_candidate_correlation",
|
|
"score_periodic_spikes",
|
|
"score_time_window_correlation",
|
|
"score_topology_adjacency",
|
|
]
|