Declared-metadata annotation evidence

caption.declared.v1 is the one shipped annotation template. Its evidence comes from the metadata the generator declared when it composed each scene, never from measuring record.iq: the generator already holds every signal’s class, bandwidth, carrier, timing, and signal-to-noise ratio (SNR) exactly, so the annotation path reads that truth instead of re-estimating it from the waveform. Component count equals declared emitter count by construction: a scene with N declared emitters is annotated as exactly N components, up to the configured component_cap (default 32), beyond which the run either errors or truncates with a citable overflow marker. Nothing in the pipeline can promote noise into a component.

Scene-report annotations are training labels for models that ingest raw I/Q (the complex in-phase and quadrature samples), so the evidence and prose contracts below are label-quality contracts. Evidence stays exact and structured; prose stays inferential and hedged, because that is the epistemic voice a deployed waveform-only model must learn.

Evidence sources: sanitized evidence in, never a record out

build_evidence(record, *, config) is the single StoredRecord-to-evidence conversion, and every route reaches it. The record-addressed routes call it while assembling the prompt: signal_dataset, locally or on Dataproc, and the Signal Dataset snapshot route reaches it through an EvidenceSource, under a local Spark driver and on Dataproc alike. Every route therefore shares one builder, one system prompt, and one provider response contract; routes differ only in configuration.

EvidenceSource (see the annotation API reference) is the shard-streamed route’s trust boundary, and it has exactly one abstract method: __iter__. Implementations read record metadata however their storage backend requires and convert inside the iterator; only (sample_id, evidence) pairs cross the boundary. No LabeledScene, StoredRecord, or raw archive bytes appear in the surface or in any yielded value.

  • WebDatasetSource is the shipped implementation. It wraps the verified shard metadata iterator, which decodes each sample’s .json member and never decodes .iq.npy, and it runs identically under a local Spark driver and inside Dataproc Serverless executors.

  • LocalHdf5Source ships beside it as a metadata-only reference implementation, wrapping handle.read_metadata(sample_id). It is on no run path; the snapshot route reads metadata through the record-addressed loop instead.

The claim shape: payload and bookkeeping are structurally separable

Every claim in declared-evidence.v1 carries exactly one payload branch (quantity with a value and unit, or label with a canonical string) and one bookkeeping sub-object holding provenance, source field, derivation method, and any availability limitation. A training pipeline that wants payloads only drops the single bookkeeping sibling per claim; it never string-matches bookkeeping field names. Provenance is one of declared by generator (copied verbatim), derived from declared metadata (arithmetic on declared fields), or capture configuration (receiver settings). A derived claim always names its method; a declared one names it only when the field it copies is ambiguous, as bandwidth_hz is: one emitter declares a frequency-hopping span there and another a measured 99%-energy figure, so the claim says which.

A value that cannot be written as a number becomes a visible limitation (quantity: null plus a bookkeeping.limitation), never a filled-in number. Which limitation says why:

Limitation

Meaning

declared_value_unavailable

the generator declined to say, or said something unusable

unbounded_declared_value

it said infinity: a component with no noise, or no power

missing_acquisition_context

the receiver frame the claim needs was never declared

contradictory_acquisition_metadata

the record declares that frame two ways, and they disagree

non_instantaneous_bandwidth

a frequency relation over a hop span, which is not an occupancy

non_continuous_occupancy

a time relation over a presence envelope, not on-air time

This is why a stock narrowband-baseline annotation states no SNR: its shipped configuration leaves declared SNR infinite, so the claim reads unbounded_declared_value. Declare a finite per-emitter SNR in the generation config, as multi-emitter-baseline does at 20 dB, and the quantity appears.

Pairwise interactions are interval arithmetic over declared extents, never measurements, and they are withheld rather than approximated when the extents do not mean what the relation’s name asks. A frequency-hopping link declares the span it sweeps rather than what it occupies at any instant, and a beacon declares an extent covering the capture while transmitting for a thousandth of it; intersecting either would report emitters as permanently co-channel or permanently coincident. Where the generator declares a duty cycle, it is surfaced as its own claim so the envelope can be read correctly.

Field selection is configuration; internals are unreachable

Which fields are surfaced is user configuration; the builder’s extractor registry defines what can be read at all.

Field group

Extractor

Why

Modulation class, family, taxonomy

Yes

Inferable from the waveform

Bandwidth, carrier offset, timing

Yes

Inferable from the waveform

snr_db, sinr_db

Yes

Inferable from the waveform

Declared device or platform identity

Yes, where a dataset provides it

A dataset-level fact, marked declared

device_id, generator_name, seeds, configuration hashes

None

Unknowable from a waveform; training on them teaches a model to fabricate

No configuration can surface a field with no extractor. A canonical-label sanitizer additionally rejects identifier-shaped strings beneath every configured label.

declared_evidence:
  component_quantity_fields: [occupied_bandwidth_hz, frequency_offset_hz, snr_db, sinr_db]
  component_label_fields: [class_name, family]

Configure sinr_db alongside snr_db, not instead of it. Declared snr_db is per-emitter signal-to-noise and ignores co-channel emitters entirely. multi-emitter-baseline packs three to eight emitters into ±700 kHz and labels every one of them 20 dB, so once several share a channel the recoverable SINR falls well below the declared SNR and can go negative.

The 2026-08-08 declared-evidence run shows the same gap on a denser hand-assembled dataset, though only through the model’s own prose: on one scene it reports per-component SINRs from about -3 dB to +0.1 dB, and elsewhere on the page down to about -5 dB, against 20 dB declared SNR. That page records no SINR column of its own. Surfacing snr_db alone teaches a model to read 20 dB off a waveform whose recoverable SINR is near zero.

The component cap is configuration; truncation is visible

declared_evidence.component_cap bounds the component list (default 32). Under the default overflow: "error", a scene declaring more emitters than the cap fails the run loudly. Under overflow: "annotate_truncated", the first cap components in declaration order are included and the evidence carries a citable /overflow marker stating the declared total and included count; the scene-level component_count claim always states the true declared total, and the system prompt instructs the model to narrate the truncation. There is no silent truncation path.

Prose is authored, evidence is embedded

The provider receives the system prompt (a version-controlled markdown file, src/rfgen/annotation/declared/prompt.md) plus the canonical evidence JSON, and returns exactly {"caption": "..."}. The model emits no values, no citations, and no metadata.

An earlier shape returned a list of paragraphs each flagged hypothesis, on the theory that speculation could be marked and filtered later. Nothing ever read the flag, so the speculation reached the corpus indistinguishable from the rest of the prose. One field cannot be misused that way.

The persisted caption-declared.v1 row embeds that same evidence verbatim alongside the prose, so every value in a stored annotation came from declared metadata and none of it passed through the model. Evidence is embedded rather than referenced because it contains computed values (frequency offsets, time intervals, overlap fractions); regenerating it later would depend on builder code that may change, while embedding freezes what the annotation meant.

Naming signals: observables only

The prompt requires an inferential voice with values rounded to a few significant figures, and it requires every signal to be named by what a receiver can observe: frequency position first, adding bandwidth or modulation class only to separate co-located signals. Internal identifiers like component-0 are JSON keys in the evidence and must never appear in prose, for the same reason device_id and generator_name are excluded from evidence entirely: a model reading only a waveform cannot recover an index that encodes generator emitter order, so training on one teaches fabrication.

What can reject a generated label

Two things, and nothing else: the reply does not parse into the narrative model, or the resulting row exceeds its serialized byte budget. There are no paragraph-count, character, or citation limits, because a cap on prose discards correct training labels without protecting anything.

This makes no assertion about whether the prose reflects the evidence; no automated check can. Hosted-model output is non-deterministic, so judge narrative quality by comparing the embedded evidence against the text, with sampled human review or a larger model.

The measured-evidence template is gone

scene_report.evidence.v2 measured its evidence out of the waveform with a local STFT. It has been removed, and caption.declared.v1 is the only annotation template.

A thresholded spectrogram merges co-channel emitters into one component, fragments a hopping emitter into several, and under-reports SNR for anything wider than one FFT bin. Its detector could also report components that correspond to no declared emitter. Every one of those is a wrong training label, and the generator already knows what it placed, so measuring it back out could only lose information and invent errors.