Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.axiomancer.io/llms.txt

Use this file to discover all available pages before exploring further.

Axiom Overwatch maintains temporal_edges — a relationship graph that records every observed change in a vessel’s identity over time. Each edge captures a transition between two snapshots of the same IMO (a name change, a flag change, an MMSI change) with a confidence score and an obfuscation flag. Downstream motif detectors (detect-temporal-motifs) traverse this graph to surface shell-hop chains, jurisdiction-shopping patterns, and other multi-step deception signals that a single-event API cannot see.

When to use it

Reach for temporal edges when you need the history of how a vessel’s identity has moved, not just the latest snapshot. Typical use cases:
  • Tracing a renamed-and-reflagged vessel across multiple identity hops
  • Distinguishing a routine rename (high name similarity) from a shell-hop (low similarity)
  • Surfacing MMSI changes, which are inherently suspicious because MMSI should be stable for the life of a hull
  • Feeding a motif detector that scans for repeated identity churn within a configurable window
If you only need the most recent identity change for a single vessel, query the Risk identity API instead — it reads the underlying vessel_identity_history directly without the edge transform.

How edges are built

For every IMO with new history since the last run, Overwatch fetches the full snapshot history, sorts it by observed_at, and walks consecutive pairs. Each detected change emits one edge.
1

Group history by IMO

All rows in vessel_identity_history for the affected IMOs are loaded and partitioned by imo_number. Spurious IMOs (0, 1) are filtered out at this stage.
2

Walk consecutive snapshots

Within each IMO group, snapshots are sorted by observed_at. The producer iterates pairwise (prev → curr) and tests three fields independently: name, flag, mmsi.
3

Emit one edge per changed field

A pair can produce zero, one, two, or three edges — a snapshot where the vessel renamed, reflagged, and changed MMSI on the same day produces all three. Each edge carries its own relation type, confidence, and obfuscation flag.
4

Upsert with deterministic IDs

Edge IDs encode the type, IMO, and millisecond timestamps of both endpoints (edge_{type}_{imo}_{from_ms}_{to_ms}). Re-running over the same window is idempotent — existing rows are skipped on conflict.

Relation types

RelationTriggerConfidenceObfuscation flag
renamed_toName changed and Jaccard similarity ≥ 0.3Equal to the similarity score (min 0.1)false
shell_hopName changed and Jaccard similarity < 0.3Equal to the similarity score (min 0.1)true
jurisdiction_changedFlag changed0.9false
mmsi_changedMMSI changed0.7true
The renamed_to / shell_hop split runs the same Jaccard character-set similarity used by TemporalGraphEngine in @axiom/core, so the live producer and the engine agree on whether a rename looks legitimate. A near-identical name (MV ATLASM/V ATLAS) scores high and stays as renamed_to; an unrelated name (MV ATLASOCEAN STAR VII) scores low and is promoted to shell_hop with obfuscation_flag = true. MMSI changes are always flagged as obfuscation because MMSI is meant to be stable for the operational life of the hull. Flag changes are not — vessels legitimately reflag for tax, registry, or charter reasons — so jurisdiction_changed ships with the obfuscation flag clear and lets downstream motif detectors decide based on cadence and combination.

Output shape

Each edge upserted into temporal_edges:
  • idedge_{type}_{imo}_{from_ms}_{to_ms}, deterministic and stable
  • source_entity_id, target_entity_idimo_{imo}_{ms} for the two endpoints
  • relation_type — one of the four values in the table above
  • valid_from — the observed_at of the new snapshot (when the change was first observed)
  • valid_tonull (open-ended; closed implicitly by the next edge from the same source)
  • confidence_decay — name-derived for renames, fixed for flag and MMSI changes
  • obfuscation_flagtrue for shell_hop and mmsi_changed
  • source_feed, source_system, source_uri, record_id — provenance tags identifying the producer and the underlying history row

Production pipeline

The producer runs as the populate-temporal-edges Edge Function rather than ad-hoc. It is the only writer of temporal_edges and is the upstream dependency for any motif-level analysis.

Incremental schedule

The function runs every 4 hours under pg_cron. Each run reads the last successful run’s metadata.last_run_at from ingestion_logs, queries vessel_identity_history for IMOs with new observations since that timestamp, and processes only those IMOs. On the first run (or after a wipe), the function falls back to a 180-day lookback so a fresh deployment populates promptly without scanning the entire history. To stay inside the 150-second Edge Function timeout, each run caps at 300 IMOs. History fetches are chunked at 75 IMOs per IN() call with a 5,000-row safety cap, which prevents PostgREST’s default page limit from silently dropping rows for IMOs late in the result set. Edge upserts are batched at 500 rows.

Direct invocation

You can call the Edge Function directly to force an incremental pass — for example, after correcting upstream identity data:
curl -X POST "$SUPABASE_URL/functions/v1/populate-temporal-edges" \
  -H "Authorization: Bearer $SUPABASE_SERVICE_ROLE_KEY" \
  -H "Content-Type: application/json"
The response includes counts for the run:
{
  "edges_created": 184,
  "imos_processed": 47
}
Each run also writes a row to ingestion_logs with source = 'populate-temporal-edges', the records-fetched and records-stored counts, and the last_run_at watermark used by the next incremental run.

Reference remediation runner

When the status page flags temporal_edges as stale — typically because the cron job missed a run or hit a transient error — invoke the reference remediation runner to re-trigger the producer and let the freshness gap clear on its own. The runner follows a strict read → confirm → act → exit contract: it reads the open freshness gap from cockpit_gaps, confirms remediation is actually warranted, posts to the Edge Function, and exits. It does not write to cockpit_gaps directly — cockpit_detect_gaps (every 15 minutes) prunes the gap on its next run once the underlying table is fresh again.
# Default — gated on an open cockpit_gaps freshness entry for temporal_edges
node scripts/remediate/populate-temporal-edges.mjs

# Plan-only — confirm a gap exists, print it, and exit without invoking
node scripts/remediate/populate-temporal-edges.mjs --dry-run

# Skip the gap gate — invoke the Edge Function unconditionally
node scripts/remediate/populate-temporal-edges.mjs --force
The runner requires SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in the environment (or --supabase-url / --service-key flags). Exit codes:
CodeMeaning
0No open gap (table fresh) or remediation succeeded
1Environment / configuration error
2Edge Function returned a non-2xx response
This pattern — a single source of truth (cockpit_gaps), runners that act but never mutate the gap table, and a detector that prunes on the next sweep — keeps cockpit_gaps durably consistent across multiple remediation runners without any cross-runner locking.

What the producer doesn’t do

  • No retroactive edits. Edges are immutable once written. If vessel_identity_history is corrected, run a targeted backfill — re-running the function will skip existing IDs because of ignoreDuplicates: true.
  • No cross-IMO inference. A renamed-and-reflagged vessel that surfaces under a new IMO is not stitched here. Cross-IMO entity resolution lives upstream in the identity-tracking pipeline.
  • No motif-level scoring. The producer only emits the building blocks. Multi-hop patterns (rapid name churn, repeated jurisdiction flips, name + MMSI co-changes) are scored by detect-temporal-motifs, which traverses the edges this function writes.
  • No alerting. The producer logs to ingestion_logs and exits. Alerts come from the consumers downstream of temporal_edges.
  • Risk API — surfaces identity-change events and the underlying vessel_identity_history
  • Investigations API — case files that pull identity history into a forensics view
  • Status page — health view that surfaces the freshness gap on temporal_edges