flightopsDocumentation
Guides

Bounded evidence windows

12 min readdocs/EVIDENCE-QUERIES.md (source repository)

splashdown_query reads one topic over an exact MCAP log-time window. It works without ROS installed and supports indexed recordings larger than the existing 64 MiB whole-recording MCP limit. The result is an observation, not a robot-health, simulation, or acceptance verdict.

Use splashdown query:

sh
splashdown query incident.mcap --root /path/to/evidence \
  --topic /odom --start-ns 1700000000000000000 --end-ns 1700000002000000000 \
  --field twist.twist.linear.x --reduction mean --unit m/s --max-samples 20

It prints one JSON document and exits 0 for a complete observation or 3 for partial/unavailable evidence. Both window endpoints are integer nanoseconds on the recording's log_time clock. The start is inclusive and the end is exclusive. These are not relative seconds, ROS header timestamps, or an inferred run clock.

CLI argument / Python or MCP keyDefault and constraint
recordingRequired path. Relative paths resolve under the configured root.
--root / rootCLI defaults to the current directory. Required Python keyword. MCP uses the server's --root, not a per-call argument.
--topic / topicRequired absolute topic name, up to 512 characters.
--start-ns / start_nsRequired integer from 0 through 2**64 - 1, inclusive.
--end-ns / end_nsRequired integer from 1 through 2**64, inclusive; must exceed the start by at most 300 seconds.
--field / fieldUnset. Dotted public field names, at most 16 components and 256 characters.
--reduction / reductioncount without a field, mean with a field. count, min, max, mean, last are supported.
--unit / unitUnset. Up to 64 printable characters; requires a field.
--max-messages / max_messages10,000. Integer from 1 through 10,000.
--max-samples / max_samples0. Integer from 0 through 200.
--jsonAccepted for consistency with other splashdown commands; output is always JSON.

Argparse errors, such as missing required arguments or a misspelled reduction, exit 2 and print usage. Requests parsed successfully but refused by the query, such as an oversized interval, normally emit an incomplete JSON record and exit 3. No output file is created unless the caller redirects stdout or saves the result. Use a new filename for that redirection; normal shell > can replace a file.

splashdown query supervises a disposable worker process with a 25-second outer timeout. Normal output is the direct evidence-window record. If that process fails before producing a usable record, splashdown query emits an incomplete flightops.tool-result/0 query error envelope and exits 3. Both forms can be saved for the shared results reader.

The equivalent MCP call uses the existing Flightops stdio server:

json
{
  "name": "splashdown_query",
  "arguments": {
    "recording": "incident.mcap",
    "topic": "/odom",
    "start_ns": 1700000000000000000,
    "end_ns": 1700000002000000000,
    "field": "twist.twist.linear.x",
    "reduction": "mean",
    "unit": "m/s",
    "max_samples": 20
  }
}

recording, topic, start_ns, and end_ns are required. With no field, the query returns message count, first/last timestamp, rate over observed samples, and the largest gap between consecutive samples. It does not include silence between a window boundary and the first/last sample in that gap measurement.

The example is a tools/call params object; an MCP client supplies the JSON-RPC wrapper. The server returns the envelope in both structuredContent and a JSON text content block. Save the structuredContent object itself for flightops-results or flightops-tui, not the entire transport-level response. MCP marks blocked and unavailable results with isError: true even when partial observations are useful.

An optional dotted field path selects a finite number or boolean from embedded ROS 2 CDR/ros2msg messages. Supported reductions are count, min, max, mean, and last; the default for a field is mean. Booleans become 0/1. Expressions, array access, private attributes, cross-topic calculations, and nonnumeric fields are refused. last follows log time, with file position breaking timestamp ties.

Units are not guessed from a field name. unit is a caller declaration and is labeled caller-declared. Without it, the field/reduction unit stays null. Timing and count units are always explicit. The result also names recorded message types and encodings. publish_time_ns is preserved in optional samples, with no claim that its clock matches the recorder or a message header.

Reading the result

MCP preserves the existing flightops.tool-result/0 envelope. Its data is a flightops.evidence-window/0 record, the same document printed by splashdown query. Numerical reductions and statistics are computed by Python, not an agent.

OutcomeMeaning
observed, complete: trueAll matching messages in the supported window were examined.
empty_window, complete: trueThe channel exists, with zero matching messages. Count is zero; undefined rates and numeric reductions are null.
missing_topic, complete: falseNo recorded channel exists. Measurements stay null rather than becoming zero.
message_limit, truncated: trueThe message cap stopped the query. Statistics cover an observed prefix; the final reduction is null.
message_size_limit, complete: falseOne selected payload exceeds 1 MiB. Status is unavailable, measurements are null and truncated is false. This is not a measured count-prefix result.
Other incomplete outcomesAn index, encoding, integrity, resource, path, or input error prevented the requested observation. The reason is explicit.

Complete outcomes have status: pass. missing_topic and a measured message_limit prefix have status: blocked; other refusals have status: unavailable. truncated is true for a count-prefix limit or an output limit, and false for ordinary refusals. Always inspect complete and outcome together rather than assuming any returned count is final.

Top-level fieldMeaning
schemaflightops.evidence-window/0.
status, complete, truncated, outcomeObservation status, coverage and the machine-readable reason.
reasonExplanation on incomplete results; absent on complete observations.
requestValidated topic, exact bounds, interval, field, reduction and message/sample caps; null if the request was refused before validation.
sourceFile identity, recorded channel/schema metadata and inspected-byte accounting; null if unavailable.
measurementsCount, observed scope, first/last log times, rate, largest gap and reduction; null when no measurement can be established.
samplesBounded list of log_time_ns, publish_time_ns and value. Value is null for a count-only query.
samples_elidedNumber of measured messages omitted from samples, when measurements were produced.
clock, unitsClock conventions and explicit metric/field units.
limitsEffective fixed file/read/summary/chunk/expanded/message/time/output limits.

For n messages with distinct first and last timestamps, rate is (n - 1) * 1_000_000_000 / (last_log_time_ns - first_log_time_ns). One message has no rate or gap. Coincident timestamps have a null rate and zero gap when there are at least two messages. Empty count reduction is exactly zero; empty min, max, mean and last reductions are null. Mean uses Python's compensated math.fsum over scaled values. Numeric overflow is refused instead of returning NaN or infinity.

max_messages defaults to 10,000 and may only lower that cap. max_samples defaults to zero and accepts at most 200. Sample elision alone does not make a fully computed reduction incomplete. samples_elided reports omitted samples among observed messages; it does not estimate unseen messages after a limit. Partial samples come from a file-order prefix, sorted by timestamp for display.

source includes the resolved file path, size, modification time, summary SHA-256, and a SHA-256 over inspected chunk bytes. hashed_chunks names their offsets and lengths; the digest incorporates each offset as an unsigned little-endian 64-bit integer followed by that chunk's raw bytes, in file order. The summary hash covers the bytes from summary_start through the end of summary offsets, excluding the footer. File metadata is checked again after reading.

This identity covers a local file version and the inspected evidence. It does not authenticate its producer or hash/check unread data; whole_file_sha256 is explicitly null. A finalized MCAP footer and valid summary are required. Available summary and selected-chunk CRCs are verified. A larger proof or incident manifest can provide a separately established whole-file identity.

Resource bounds

ResourceHard query limit
Recording size32 GiB
Window duration300 seconds
Cumulative file reads64 MiB
Summary bytes / records4 MiB / 4,096
Channels / schemas1,024 each
Embedded schema256 KiB
Selected chunks64
Compressed or expanded chunk8 MiB, plus a bounded chunk header
Total expanded selected chunks32 MiB
Inspected chunk records100,000
Selected message payload1 MiB
Matching messages / returned samples10,000 / 200
Query JSON192 KiB
POSIX process address space512 MiB
Query time20 seconds; MCP also keeps its outer configured deadline

Limits are deliberate refusals, not invitations to raise every cap. Narrow the window or create a smaller recording when a query cannot fit. The 64 MiB input cap on splashdown_stats and splashdown_assert remains unchanged, as does the 256 KiB MCP envelope output cap.

The query reads the bounded summary through MCAP's indexed reader and seeks directly to matching chunks. It does not construct splashdown's whole-recording timestamp index. Chunk headers are checked against indexes before bounded decompression. Supported compression is uncompressed, Zstandard, and LZ4. Missing indexes and unfinished/truncated files are refused; there is no automatic full-file scan or recovery. Numeric decoding currently supports only embedded ros2msg schemas and CDR messages.

splashdown query and the MCP worker apply process limits on POSIX. MCP also runs each request in a disposable process and kills timed-out workers. The in-process query_window function has read/compute checks, but callers embedding it must supply the same disposable process boundary for untrusted decoders. Paths must resolve inside --root, including symlinks. Only regular files are accepted; FIFOs/devices and escapes outside the root are refused. The query never changes the recording.

Python API

python
from pathlib import Path
from splashdown.query import query_window

observation = query_window(
    "incident.mcap",
    root=Path("/path/to/evidence"),
    topic="/odom",
    start_ns=1700000000000000000,
    end_ns=1700000002000000000,
    field="twist.twist.linear.x",
    reduction="mean",
    unit="m/s",
    max_messages=10000,
    max_samples=20,
)
if observation["complete"]:
    print(observation["measurements"]["reduction"])
else:
    print(observation["outcome"], observation["reason"])

query_window returns a JSON-compatible dictionary for normal query refusals; it does not turn missing evidence into an exception-only interface. In a long-lived Python service, call it in a supervised child process. apply_process_limits() sets process-wide address-space and signal limits and is intended for that child, the standalone CLI or the MCP worker, not an already-running application process.

Saved-result integration

splashdown.query_result.validate_window(data) checks saved records before they are presented. It refuses contradictory status/completeness/truncation flags, invented clock mappings, nonfinite measurements, impossible count/timing values, missing provenance and inconsistent sample elision. When every sample is present, it also checks the reported reduction against those samples.

splashdown.query_result.presentation(data, source) returns the shared results model for HTML, TUI and plain output. It preserves zero measurements and null values, labels the result as an observation, and includes the original JSON. Neither function opens paths named by the evidence record. Validation checks the record's internal consistency; it does not authenticate its producer or reread the recording to recompute omitted measurements.

Both direct query JSON and a saved splashdown_query MCP envelope are supported:

bash
splashdown query incident.mcap --root /path/to/evidence \
  --topic /odom --start-ns 1700000000000000000 --end-ns 1700000002000000000 \
  --max-samples 20 > /tmp/odom-window.json
flightops-results /tmp/odom-window.json -o /tmp/odom-window.html
flightops-tui --plain /tmp/odom-window.json

For saved MCP queries, the shared loader validates the inner evidence-window record and requires outer status and complete to agree with it. A wrapper cannot turn a partial observation into a complete one. A timeout or crashed worker may instead return only an error envelope; that remains readable only as unavailable, complete: false, assurance: none, with a nonempty error and no unsupported inner schema. The UI labels both forms as observations, never as executed simulation results.

Python models and TUI JSON preserve integer values. HTML uses decimal strings for presentation integers outside JavaScript's safe range, so timestamps such as 1700000000000000000 and 1700000000000000010 remain distinct. The raw panel and download use the original UTF-8 JSON text, including exact numeric tokens, whitespace and line endings. Browser presentation strings are not a new wire schema. A JavaScript MCP client must also preserve large JSON integers before saving them; the viewer cannot recover precision lost earlier in the client.

Troubleshooting and verification

Outcome or symptomNext step
missing_topicCheck the exact recorded topic name. This is distinct from an existing topic with a complete zero-count interval.
empty_windowCheck clock basis and exact nanosecond bounds. The query does not translate relative run seconds.
message_limitSplit the time interval into smaller windows; prefix reductions are deliberately unavailable.
message_size_limitThe selected payload is too large for this query path. Use a supported offline workflow that can handle the payload; reducing sample output does not reduce payload size.
unindexed_recording, invalid_recording, unsupported_summaryUse a finalized supported MCAP. Recovery or reindexing is a separate operation and changes evidence that must receive its own identity.
chunk_limit, window_limit, read_limit, record_limitNarrow the interval. A narrower interval may still touch the same oversized chunk.
unsupported_encoding, missing_field, non_numeric_fieldTry a count-only query, or choose a supported numeric field from the embedded schema. Expressions and array indexing are unsupported.
invalid_pathPut the recording under the configured root and pass its correct relative path. Symlink escapes are refused.
source_changedFinish recording and query an unchanged file.
time_limit, memory_limit, worker timeoutReduce the query or use a reviewed smaller recording. An outer MCP timeout does not increase internal limits.
Saved record refused by the viewerKeep the original tool result intact. Inspect status/completeness, schema and finite-number errors; do not edit flags to force a green display.

Focused regression tests need a source checkout: they are in splashdown/tests/ and integrations/tests/, which are not in the install. From the repository root:

bash
.venv/bin/python -m pytest splashdown/tests/test_query.py -q
.venv/bin/python -m pytest \
  integrations/tests/test_queries.py integrations/tests/test_results_query.py -q

The tests generate real indexed MCAP, including a recording larger than 64 MiB, and exercise bounded reads, zero/absent/partial data, numeric/clock/unit semantics, compressed and malformed inputs, path refusal, MCP envelopes and exact HTML JSON. These are query and rendering checks, not closed-loop simulation acceptance.