ADR 0021: Generated Presentation Artifact Ownership¶
Status¶
Accepted
Date¶
2026-03-16
Context¶
ADR 0017 defines that localization belongs only to the presentation layer.
ADR 0018 defines the naming and taxonomy rules for message_key.
However, those ADRs do not fully define how persisted user-facing artifacts should be stored when the platform produces two different content classes:
- Deterministic presentation content that can be represented as
message_key + params - Non-deterministic AI-generated narrative text that does not have a stable message key
Without an explicit ownership model, the storage layer drifts into a hybrid design:
- locale becomes part of row identity
- rendered text becomes accidental source of truth
- deterministic and AI-generated artifacts follow different undocumented rules
- locale changes require duplicate rows or regeneration decisions that are not governed centrally
This gap affects persisted notifications, explanations, and briefs.
Decision¶
Persisted user-facing artifacts use a single canonical presentation envelope.
One Row Per Business Entity¶
Locale is not part of storage identity.
Examples:
ai_notificationsare unique bysource_event_type + source_event_idai_explanationsare unique byexplain_kind + subject_id
Content Kinds¶
Each persisted presentation artifact declares content_kind.
Allowed values:
descriptor_bundlegenerated_text
Descriptor Bundle¶
descriptor_bundle is used when the artifact can be rendered deterministically from shared catalogs.
It stores:
- version
- kind
- field descriptors
Example:
{
"version": 1,
"kind": "descriptor_bundle",
"title": {"key": "notification.signal.created.title", "params": {"symbol": "BTCUSDT"}},
"message": {"key": "notification.signal.created.message", "params": {"symbol": "BTCUSDT", "timeframe": 15}}
}
The localization engine renders these fields only on presentation boundaries.
Generated Text¶
generated_text is used when the artifact is freeform AI output and does not have a stable message key contract.
It stores:
- version
- kind
- rendered locale
- rendered text fields
Example:
{
"version": 1,
"kind": "generated_text",
"rendered_locale": "en",
"title": "ETHUSDT: anomaly detected",
"message": "IRIS flagged a volatility anomaly for ETHUSDT."
}
Generated text is a presentation snapshot, not a domain fact.
Legacy Text Columns¶
Legacy text columns may remain physically in the schema during migration, but they are not the source of truth.
Canonical content lives in the presentation envelope.
Locale Changes¶
For descriptor_bundle, locale switching re-renders from catalogs and does not create a second row.
For generated_text, locale switching does not create a second row automatically.
If a different locale snapshot is needed, it must be produced by explicit regeneration or replacement of the same artifact row.
Consequences¶
Positive:
- storage identity stays language-agnostic
- deterministic localized content remains fully centralized
- freeform AI narrative gets an explicit, governed ownership model
- locale changes stop multiplying rows
Tradeoffs:
- generated text remains locale-specific snapshot content
- migrations that collapse historic locale duplicates are lossy by design
- callers must understand
content_kindinstead of assuming every artifact is message-key-backed
Follow-up¶
- add CI checks for content envelope validity and
content_kindcoverage