Lincoln consumes track events through a feed-adapter pattern. Each adapter — TAK CoT, sensor fusion, ADS-B, JSONL replay — implements a small transport interface and lands events at a single typed schema boundary. The policy engine reads the resulting CotEvent stream regardless of origin. This document walks the adapter contract, the schema boundary, and a TAK CoT XML event end-to-end.
Tactical-data integration has two layers: the wire format (XML, JSON, binary) and the event semantics — whether the feed carries the fields the policy engine needs (PID-relevant classification, sensor-source provenance, kinematic certainty, freshness) or only a position and a label. The wire is mechanical; the semantic mapping is an Interface Control Document agreement with the upstream feed owner and is independent of wire format.
Lincoln has one schema boundary — CotEventSchema in @lincoln/tactical-bridge — and a transport interface (start(onEvent) / stop()) behind which an adapter speaks the source protocol. The bridge core validates events from any adapter against the schema and forwards only well-formed events to memory. New feeds become new adapters; the policy engine does not change.
CotEvent regardless of where it came from.
The adapter contract: start(onEvent) takes a callback the adapter invokes with each raw, unvalidated blob; stop() releases sockets, file handles, and timers. The adapter declares its kind as one of tak-tcp, cot-replay, peer-mesh, and that string is stamped onto every event so downstream consumers can tier-trust the source.
The bridge core wraps the adapter's onEvent in a validating wrapper. The adapter publishes raw blobs; memory receives validated CotEvents. In between, the bridge runs CotEventSchema.safeParse, drops on failure with a redacted log line and a counter increment, and forwards on success. The schema runs on every event regardless of adapter.
CotEventSchema is the floor every event has to clear regardless of origin. The rules are conservative on purpose — the policy engine is the system of record for “is this operationally relevant,” the schema is the system of record for “is this even well-formed.”
| Field | Rule | Why |
|---|---|---|
uid | Non-empty string | Track identity downstream |
type | Hyphen-separated alphanumeric segments (a-f-G-U-C, a-h-A, etc.) | MIL-STD-2525-aligned grammar; loose because TAK extensions invent codes freely |
time / start / stale | ISO-8601 UTC with Date.parse round-trip | Catches invented dates like 2025-13-99T... |
stale ≥ time | Cross-field invariant | Already-stale-on-arrival is the canonical adversarial case; we drop it |
point.lat / lon | Bounded | [-90, 90] / [-180, 180]; rejects garbled coordinates |
point.ce / le | Non-negative | Circular / linear error metrics, meters |
detail | Object if present, never array or primitive | TAK detail extensions are vendor-extensible — we preserve verbatim, but a stray array indicates a parser bug |
received_at | ISO-8601 UTC, stamped by adapter | Our wall-clock — basis for memory's freshness bucketing |
source_transport | One of three discriminants | Audit log can tier-trust events by origin |
Malformed events are an expected occurrence. The bridge logs a one-line redacted summary (the uid_hint only, never the full payload), increments a rejected counter, and continues reading. Memory receives only events that passed validation.
A canonical TAK Server position update on the CoT stream:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<event version="2.0"
uid="ANDROID-deadbeef"
type="a-f-G-U-C"
time="2026-05-02T19:00:00Z"
start="2026-05-02T19:00:00Z"
stale="2026-05-02T19:05:00Z"
how="m-g">
<point lat="32.7720" lon="-97.7720" hae="95.0" ce="9.9" le="9.9"/>
<detail>
<contact callsign="RAVEN-1-1"/>
<__group name="Cyan" role="Team Leader"/>
<track speed="22.0" course="178.0"/>
</detail>
</event>
tak-tcp adapter reads bytes off the TLS socket and reframes them on </event> boundaries.parseTakCotXml(xml) — which uses fast-xml-parser with attribute and tag value parsing off, coerces the five numeric point fields explicitly, and preserves <detail> children verbatim with the parser's @_ attribute prefix stripped.received_at (host wall-clock at receive) and source_transport: "tak-tcp" onto the parsed shape.onEvent(raw) callback.validateCotEvent(raw). The event passes (ISO timestamps round-trip, type matches the grammar, stale is later than time, lat/lon are in range, detail is an object).emitted and forwards the typed CotEvent to memory's recordCotEvent.uid and re-buckets the track for freshness.
Now suppose the same XML arrives malformed — say, stale earlier than time (already-stale on arrival). Steps 1–4 still happen. At step 5 the schema's superRefine fires: stale must be >= time. The bridge increments rejected, logs "dropping malformed CoT event" with the uid_hint and the Zod error string, and continues reading. Memory does not see the event. The policy engine does not see the event. The audit chain does not see the event. The bridge keeps draining the socket.
Current status of each integration component.
| Component | Status | What it would take to finish |
|---|---|---|
parseTakCotXml — XML to typed shape |
done | Built, 7 tests including round-trip into CotEventSchema |
CotEventSchema — validation |
done | Built, including the cross-field stale ≥ time refinement |
cot-replay — JSONL replay |
done | Built, drives the demo deterministically |
| Bridge core — validate & forward | done | Built, with metrics and consumer-throw protection |
TacticalTransport interface |
done | Stable contract, three named transport kinds |
tak-tcp — live TAK socket |
stub | TLS/mTLS socket, stream framer on </event>, reconnect/backoff. ~1–2 days. |
| UDP multicast (LAN/local TAK) | none | Datagram listener, group join. Roadmap. |
| Outbound CoT emission | none | Likely intentional — we are a consumer, not a TAK client. |
| Federation / cross-server provenance | none | Server-to-server protocol, out of scope for governance. |
| Adversarial-input fuzzing | partial | Specific named cases tested; random fuzzing on roadmap. |
| Sensor-fusion / ADS-B / Link-16 adapters | none | Each is a new adapter implementing TacticalTransport. Architecture supports it; we have not built one. |
Standing Lincoln up against a live tactical feed depends primarily on the upstream feed's ICD rather than the transport. Four considerations:
EO/IR mean a human reviewed it or a model classified it? Without this, the PROHIB-NO-PID hard-block has nothing to evaluate.
PROHIB-STALENESS hard-block can be defeated by a fusion stack that re-emits a stale track every second.
uid across re-acquisitions, or generate a fresh one every time the track is dropped and re-found? Memory does last-write-wins by time; the policy engine cares about staleness; both depend on uid meaning the same physical thing each time.
These are ICD agreements with the feed owner, independent of wire format.