AIAIAI Design System

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.

 +14 cm/h — measured  2,1 m — inferred  −18 cm — projected
 +14 cm/h — confirmado  2,1 m — inferido, incerto  −18 cm — modelado, quase certo
 +14 cm/h — medido  2,1 m — inferido, incerto

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.

 1,9 MW — projected, stale  2,1 m — inferido, obsoleto

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.

 −18 cm — projected  1,9 MW — modelado, obsoleto

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.

WATER LEVEL
+14 cm/h
WATER LEVEL
 +14 cm/h — measured
 −18 cm — projected

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.

To verify Verified Not verified Not done

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)