§04 · Inputs

Feed integration

A transport-agnostic consumer architecture, and the honest gap between “CoT parser” and “TAK client.”
Reads after
§03 MissionArtifact → decision
Reads before
§05 The LM perimeter
Key claim
The wire format does not change the compile-and-execute model. Lincoln is a consumer, not a TAK client; that is a deliberate scope choice.

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.

§1The integration question

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.

§2The layered architecture

EXTERNAL · OUT OF SCOPE FOR LINCOLN TAK Server CoT XML / TLS 8089 Sensor fusion vendor protocol ADS-B / Beast JSON / binary Link 16 gateway JREAP-C / TDL JSONL replay (demo) TRANSPORT ADAPTERS · TacticalTransport interface tak-tcp stub · throws on start sensor-adapter not built adsb-adapter not built link16-adapter not built cot-replay working · 24-event fixture @lincoln/tactical-bridge — SCHEMA BOUNDARY CotEventSchema · ISO-8601 · CoT type grammar · stale ≥ time malformed → drop with redacted log · validated → forward @lincoln/memory · spatial index, freshness last-write-wins by track time, p99 snapshot 0.221 ms …to @arbitration → @policy-engine → @audit (see §03)
Figure 1. Five lanes: external feeds, transport adapters, the schema boundary at the bridge, the spatial index, and the rest of the execute path. Adapters are interchangeable; the schema boundary is fixed; what passes through is a typed CotEvent regardless of where it came from.

§3The transport interface

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.

§4What the schema enforces

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.”

FieldRuleWhy
uidNon-empty stringTrack identity downstream
typeHyphen-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 / staleISO-8601 UTC with Date.parse round-tripCatches invented dates like 2025-13-99T...
stale ≥ timeCross-field invariantAlready-stale-on-arrival is the canonical adversarial case; we drop it
point.lat / lonBounded[-90, 90] / [-180, 180]; rejects garbled coordinates
point.ce / leNon-negativeCircular / linear error metrics, meters
detailObject if present, never array or primitiveTAK detail extensions are vendor-extensible — we preserve verbatim, but a stray array indicates a parser bug
received_atISO-8601 UTC, stamped by adapterOur wall-clock — basis for memory's freshness bucketing
source_transportOne of three discriminantsAudit 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.

§5Walking a TAK CoT XML event end-to-end

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>
  1. The tak-tcp adapter reads bytes off the TLS socket and reframes them on </event> boundaries.
  2. For each framed XML document, the adapter calls 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.
  3. The adapter stamps received_at (host wall-clock at receive) and source_transport: "tak-tcp" onto the parsed shape.
  4. The adapter calls the bridge's onEvent(raw) callback.
  5. The bridge runs 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).
  6. The bridge increments emitted and forwards the typed CotEvent to memory's recordCotEvent.
  7. Memory updates the spatial index by 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.

§6Built versus stubbed

Current status of each integration component.

ComponentStatusWhat 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.

§7Integration considerations

Standing Lincoln up against a live tactical feed depends primarily on the upstream feed's ICD rather than the transport. Four considerations:

These are ICD agreements with the feed owner, independent of wire format.