Evidence Seal
Provenance as a property of the datum, not as chart styling. Every value that is not a direct current reading carries a seal: two orthogonal axes that never merge, a why one tap away, and screen-reader text that survives copy, quotation and export.
The design system owns the laws, not the words
These components enumerate no seal vocabulary. Terms arrive as declared data — { value, label, tone } resolved from ontology vocabulary rows at read time,
the same enum-choice mechanism every other declared vocabulary in the estate uses. Widening an
axis, re-wording a term, or switching a three-term evidence axis for a two-term one is a
declaration change: zero component code, no package release.
value is the English code and stays on the datum; label is the
localized text a reader and a screen reader receive; tone selects from the design
system's own presentation scale — positive · info · caution · neutral — which is
the part the design system legitimately owns.
The laws
Enforced by the components, not left to the caller. SealChip and KpiRegister are boundary elements: they throw rather than render a value whose provenance has gone missing. Every law below holds whatever vocabulary you declare.
- Two orthogonal axes — evidence and probability render as two adjacent chips and never collapse into one phrase, one score, or one label.
- Never label-alone — a seal never appears without its evidence chip.
- Evidence alone is valid; probability alone is not. A probability with no evidence throws.
- A value that needs a seal never renders without one — its window is later than now, or its provenance includes a model.
- The more authoritative value out-ranks — in type size, in position, and in screen-reader reading order. Enforced by evidence class, not by word.
- A why is always reachable from the seal, one tap away, by keyboard.
- The seal rides the datum — adjacent DOM text, so it survives re-render, copy, quotation, export and screen readers. A dashed line does none of that.
One component, three different vocabularies
The same SealChip, rendering three of the enumerations the corpus carries. Nothing
but the data differs between these rows — which is the whole point: reconciling them is an open
decision, and no component should quietly pre-empt it.
Staleness
When the validity window closes, the seal goes stale. Staleness is a separate orthogonal state, never a member of either vocabulary — it mutes whatever tone the term carries and leaves the term itself untouched. The word carries the meaning; the wash is redundant emphasis, so it survives greyscale and high contrast.
The why disclosure
Pass a why and the seal becomes a real button with aria-expanded,
opening the provenance one-liner. Click, Enter or Space toggle it; Escape closes it and returns
focus to the seal; the panel carries a visible close. The trigger's accessible name comes from
the caller's whyLabel pattern, since this package owns no product copy. Disclosure
state lives in component state, never in a DOM attribute.
KpiRegister
The two-register composition: the more authoritative value above, the less authoritative beneath. This is where "the reading out-ranks the forecast" becomes concrete in all three senses — size, position, and reading order. The ranking is enforced by evidence class, so it holds under any vocabulary; seating a more authoritative value in the lower register throws, as does handing over a secondary value with no seal.
CaminhoStateChip
The four verification states, rendered as words rather than icons or colour alone — a screen reader announces the full state, and the meaning survives greyscale.
Assigning a seal, end to end
Components never construct a seal, and the model never assigns its own. Assignment is
deterministic product code reading the datum's own provenance against an injected now, so the clock is testable and nothing reads a wall clock directly. assignSeal returns no words at all — only what the provenance makes the datum. resolveSeal then applies the declared vocabulary.
import { assignSeal, resolveSeal } from '@aiaiai-pt/widget-system/core';
import { SealChip } from '@aiaiai-pt/design-system';
// 1. Structural, wordless: what the datum's provenance makes it.
const assignment = assignSeal(
{ window_start, window_end, model_derived },
now,
);
// → { evidenceClass: 'future_window', stale: false, sealRequired: true }
// 2. The declared vocabulary, resolved at read time from the ontology.
const vocabulary = await loadSealVocabulary(tenant, locale);
// 3. Words applied.
const seal = resolveSeal(assignment, vocabulary, datum.probability_code);
<SealChip value={formatted} {...seal} why={provenanceLine} />Changing the vocabulary without a release
The terms are rows of a declared vocabulary entity on the ontology sheet — the platform's
established pattern for a vocabulary that carries metadata, the same shape occurrence_state uses to carry a code, a display name, a grouping and a colour. To
re-word a term, widen an axis, or collapse a three-term axis into two, edit the rows and
re-provision. No component changes, no package publishes, no host deploy.
# The vocabulary is an entity type, not an enum on a field.
- entity_type: seal_term
schema:
display_name: Termo de Selo
tenant_scoped: true
fields:
- { field_key: code, field_type: { type: string }, required: true }
- { field_key: label, field_type: { type: string }, required: true }
- { field_key: axis, field_type: { type: string }, required: true }
- { field_key: evidence_class, field_type: { type: string }, required: false }
- { field_key: tone, field_type: { type: string }, required: false }
- { field_key: order, field_type: { type: integer }, required: false } The rows are the vocabulary. These are cycle-6's narrower pair; swapping in cycle-4's three-term axis and four-term ladder is a change to these rows and nothing else:
code label axis evidence_class tone
──────────── ────────── ──────────── ──────────────── ────────
measured medido evidence direct_reading positive
inferred inferido evidence model_derived info
inferred inferido evidence future_window info ← two classes, one term
probable provável probability — neutral
uncertain incerto probability — neutral Because evidence_class is a column rather than something inferred from the word, a
deployment that wants a two-term evidence axis just points future_window and model_derived at the same term — the row above. The «previsto» wording question
stays where the operator can settle it, in the data, instead of frozen into anyone's component.
Tokens
The tokens are named for tones, never for seal words — which word wears which tone is declared beside the word.
TOKEN REFERENCE
SealChip, KpiRegister and CaminhoStateChip use component tokens from tokens/components.css:
--seal-positive-text / --seal-positive-bg--seal-info-text / --seal-info-bg--seal-caution-text / --seal-caution-bg--seal-stale-text / --seal-stale-bg--badge-neutral-bg / --badge-neutral-text (neutral tone)--color-surface-secondary + --elevation-border (why panel)--type-caption-* (why panel text)