feat(ttp): E.3.15 UKC bridge — production phase-handoff edge fires

Add BaseRepository.list_ttp_decky_phases(identity_uuid) returning
per-decky tag observations as (decky_id, tactic, created_at_ts) rows
ordered by creation time. Rewrite from_identity_row() to project
tactic → UKCPhase via tactic_to_ukc_phase and populate the four
phase-handoff maps (first/last_phase_per_decky,
first/last_seen_per_decky) so combined_campaign_weight finally lights
up on real DB rows — not just synthetic fixtures.

ConnectedComponentsCampaignClusterer.tick() pulls each active
identity's per-decky phase observations before projecting features.
Repo failures are non-fatal: a partial repo falls back to the empty
phase-handoff signal (legacy behavior) so the worker stays up.

tests/clustering/test_ttp_phase_handoff.py pins the production-row
pair clearing CAMPAIGN_EDGE_THRESHOLD on a C2 → DISCOVERY hand-off —
the trip-wire that says the whole project paid off.

commands_by_phase_on_decky itself stays empty on the production path:
it is consumed only by the synthetic-fixture similarity surface, and
the phase-handoff edge does not use it. Synthetic fixtures still
populate it directly via from_synthetic_identity.
This commit is contained in:
2026-05-01 21:01:58 -04:00
parent 101127247e
commit 403d83faba
5 changed files with 259 additions and 9 deletions

View File

@@ -3056,7 +3056,21 @@ Order:
from `ttp_tag`. Validate that production phase-handoff edge
weights now fire (previously dormant — the phase-handoff
test's `xfail` flips to `xpass`, which is the moment we know
this whole project paid off).
this whole project paid off). ✅ done.
`tactic_to_ukc_phase` + `OBSERVABLE_PHASES` were already
shipped in earlier work — this step adds
`BaseRepository.list_ttp_decky_phases(identity_uuid)` and
rewrites `from_identity_row()` to populate the four
phase-handoff maps (`first_phase_per_decky`,
`last_phase_per_decky`, `first_seen_per_decky`,
`last_seen_per_decky`) from real `ttp_tag` rows.
`commands_by_phase_on_decky` itself stays empty on the
production path — the phase-handoff edge does not consume
it; the four phase-maps drive the F5 signal. Synthetic
fixtures continue to populate the commands map directly.
`tests/clustering/test_ttp_phase_handoff.py` pins the
production-row pair clearing `CAMPAIGN_EDGE_THRESHOLD` —
the trip-wire that says the whole project paid off.
16. **Frontend** — `IdentityDetail` "TTPs Observed" section,
`AttackerDetail` per-IP slice, Navigator export buttons,
rule-state controls (disable / clip / TTL) backed by the