Every BI tool generates insight text beside a chart. None puts a provenance-tracked claim on it. The data-truth bridge does exactly that: it ingests data-quality results — a failed dbt source-freshness check, a Great Expectations validation — and paints them as annotations that carry who found the problem, how,when, and against which data snapshot. The same chart, a new communicative act: report becomesalert, driven entirely by external metadata.
Why this matters
A dashboard that silently renders stale or out-of-bounds data is worse than no dashboard — it launders a pipeline failure into a confident-looking chart. The fix isn't a separate "data quality" tab nobody opens; it's putting the quality signal where the reader is already looking, with enough provenance that they can tell a hard rule from a statistical expectation from an AI guess. Because the annotations carry a lifecycle, a week-old freshness alert dims and dashes on its own instead of crying wolf forever. And because the bridge is a read-only overlay, it never reaches back into your warehouse — the data-quality system owns the checks; Semiotic owns only the visual and its provenance.
Report, then alert
Below is a daily transaction-volume series whose upstream pipeline stalled on day 10. On its own it's just a line that drops. Toggle the bridge on and the dbt freshness failure becomes a danger threshold at the last good load, the Great Expectations range check becomes a band marking where volumeshould have been, and each carries a provenanced label.
How an agent or screen-reader reads it — describeChart L4
This is an alerting chart; the drop to 178 at 1.8T warrants a closer look.
The provenanced threshold flips the communicative act from report to alert — same chart, new act, driven by external metadata. (No capability hint passed; the annotation alone does it.)
Freshness decay — how the alert ages
The annotations were stamped with a P2D TTL. Advance the clock and the shipped applyAnnotationLifecycle dims and dashes them as they pass fresh → aging → stale, so an old alert quiets itself instead of shouting indefinitely.
The provenance the chart now carries
Each generated annotation is an ordinary Semiotic annotation with two extra blocks: provenance (a system author, a rule basis, the dbt invocation id as dataVersion) and lifecycle (a proposed status, the TTL, a semantic anchor so it re-resolves on refresh). Here is the freshness alert the dbt artifact produced:
JSON
{ "type": "x-threshold", "value": 1781740800000, "label": "transactions stale since Jun 18", "color": "var(--semiotic-danger)", "provenance": { "author": "dbt freshness: transactions", "authorKind": "system", "source": "dbt", "basis": "rule", "createdAt": "2026-06-21T08:00:00Z", "dataVersion": "inv-7f3a", "stableId": "source.shop.raw.transactions" }, "lifecycle": { "status": "proposed", "anchor": "semantic", "ttlHint": "P2D" } }
What it refuses to place
A not_null dbt test and a uniqueness expectation assert a property of a column with no single chart coordinate. Rather than fabricate a position — which would put a plausible-but-wrong mark exactly where a non-expert reader would trust it — the bridge returns them inunplaced with a reason, for the host to render as a chart-level badge it has the context to place. Announcing the gap is the feature:
JSON
[ { "id": "test.shop.not_null_transactions_id.9c1", "reason": "custom check on \"?\" has no single chart coordinate; surface it as a chart-level status badge or a row-level mark in your UI" }, { "id": "ge:expect_column_values_to_be_unique:id", "reason": "custom check on \"id\" has no single chart coordinate; surface it as a chart-level status badge or a row-level mark in your UI" } ]
Wiring it up
TS
import { fromDbtArtifacts, fromGreatExpectations, applyAnnotationLifecycle, } from "semiotic/ai" // Read-only: parse the artifacts your CI already produces. const dbt = fromDbtArtifacts( { sources: sourcesJson, runResults: runResultsJson }, { ttlHint: "P2D" } ) const ge = fromGreatExpectations(validationJson, { ttlHint: "P2D" }) // Merge, age against the chart's "now", and render. const annotations = applyAnnotationLifecycle( [...dbt.annotations, ...ge.annotations], { now: Date.now() } ) <LineChart data={volume} xAccessor="t" yAccessor="value" xScaleType="time" annotations={annotations} /> // dbt.unplaced / ge.unplaced hold the checks with no chart coordinate.
Where this goes
The same shape recurs anywhere a chart sits downstream of a quality gate. A SOC dashboard annotates a metric the moment a detection rule fires. A financial report marks a figure that failed reconciliation, with the reconciliation run id baked in so a regulator can trace it. A model-monitoring view flags a drift threshold breach with a confidence below 1, distinct from the deterministic rules around it. In every case the value is the same: the chart stops merely showing the data and starts telling you whether to trust it.
Related