Datus extensions to OSI (vendor spec)¶
- Status: draft, in use by Datus (dosi-engine + the Datus generation agent).
- Spec version:
datus-ext/1(the major series) - Current version: 1.1 — see §2.1 for what each
minor added and how a version bump is decided. Any engine can report the
version it implements:
dosi info,GET /v1/capabilities, ordosi_engine.DATUS_EXT_VERSION. - Scope: semantics that OSI core
0.2.0.dev0cannot yet express, carried inside the OSI-sanctionedcustom_extensionsfield so a document stays fully OSI-valid and every non-Datus consumer ignores them.
For a task-oriented walkthrough with before/after examples, see the Datus extensions guide. This page is the normative contract.
Separately from model semantics, dosi-engine's connection configuration also speaks Datus: profiles use the agent.yml
datasources:vocabulary and a fullagent.ymlcan be passed as--connectionsdirectly — see connectors.md.
This is a private vendor extension: it does not need to be accepted by the OSI
working group to be usable. Where an extension generalizes a gap that belongs in
the core spec (time granularity, cumulative windows), the long-term home is the
OSI RFC in rfc-time-semantics.md; this document is the
interim vehicle Datus ships against today.
1. Why this is safe against OSI¶
OSI's schema is strict (additionalProperties: false everywhere), so we cannot
add bare fields like relationship.join_type. But the spec provides a
first-class escape hatch on every major object:
custom_extensions is present on SemanticModel, Dataset, Field, Relationship,
and Metric (see crates/dosi-model/src/spec.rs). The upstream ossie validator
accepts it, so a document carrying Datus extensions passes the validate-osi
gate unchanged. A consumer that doesn't understand vendor_name: DATUS simply
skips it — the model still means exactly what pure OSI says it means. The Datus
extensions only refine engine behavior at points OSI leaves undefined; they
never change what a metric or dimension is.
2. The envelope¶
A Datus extension is one custom_extensions entry:
vendor_name: canonically the uppercase constant"DATUS"(matching the OSI spec's uppercase enum style, e.g.ANSI_SQL). Matching is case-insensitive; the historical lowercasedatuskeeps working.data: a JSON string (per the OSI specdatais a string, not an object) decoding to a single JSON object — the payload. At most oneDATUSentry per object. Two envelope keys are reserved:vandrequires.
Consumption rules (dosi-engine):
- Absent → the documented OSI default (below). Extensions are purely additive; a model with none behaves exactly as today.
- Unknown payload keys are ignored, not rejected — so a newer Datus agent can
emit keys an older engine doesn't read without breaking it (forward-compat).
The exception is
requires, below. - Unknown
vendor_name(anything butDATUS, compared case-insensitively) is ignored. - A malformed
DATUSpayload (not JSON, or a known key with the wrong type) is a structured error (invalid_datus_extension), not a silent default — a Datus-authored document is held to the Datus contract. Compilation still continues with the default, so one pass reports every problem in the model.
v — the declared version¶
v is MAJOR.MINOR. The string form is canonical ("v": "1.1"); a JSON
number is accepted for convenience ("v": 1.1, and the bare integer "v": 1
meaning 1.0).
A JSON number can only carry minors 0–9.
1.10and1.1are the samef64, so a number cannot express minor 10; a number with two or more fractional digits is rejected outright with a message telling you to use the string form. Once the current minor reaches 10, write"v"as a string always.
v is optional and omitting it is always safe — an unversioned payload
behaves exactly as it did before the version gate existed, and produces no
diagnostic. The version is declared per custom_extensions entry, not per
document: a model-level v does not become a default for its datasets, fields,
relationships, or metrics.
What stamping a version buys you, given the engine's own version:
| Situation | Engine's response | Code |
|---|---|---|
v absent or null |
silent — behaves as before the gate | — |
v malformed (bad string, wrong JSON type, number with ≥2 decimals) |
error | invalid_datus_extension |
v below 1.0, or a major newer than the engine's |
error | unsupported_datus_ext_version |
| minor newer than the engine's | warning naming the keys that were dropped; everything the engine knows still applies | datus_ext_version_ahead |
a key newer than the v you declared |
warning; the key is honored anyway | datus_ext_key_newer_than_declared |
| minor older than the engine's, keys all within it | silent | — |
A future major is the one blanket rejection: a major bump means at least one key changed meaning, and an engine that predates the change has no way to know which — reading the payload anyway would produce quietly wrong numbers. Minors are additive by construction, so they only ever warn.
requires — keys that must not be dropped¶
requires is an array of key names the producer declares must not be silently
ignored. A consumer that does not implement a listed key raises
datus_ext_key_required naming it, instead of dropping it.
This exists because a consumer cannot judge the danger of a key it has never
heard of. "Unknown keys are ignored" is the right default for a key like
join_type, where ignoring it falls back to a documented behavior — but the
wrong default for a key like semi_additive, where ignoring it means summing an
end-of-period balance and returning a number that is simply wrong. The producer
knows which is which; requires is how it says so.
Emit requires for every key whose ignore-impact is silent (see §2.1) and
for nothing else: letting an older engine degrade is better than refusing to
serve it. dosi info / GET /v1/capabilities report each key's impact.
2.1 Versioning policy¶
Every key this engine reads is registered with the version that introduced it
and what it costs a consumer to ignore it. The registry lives in
crates/dosi-compiler/src/ext.rs and is the source of truth: the version gate,
the basic-mode warning text, dosi info and GET /v1/capabilities all read it
at runtime, so none of them can drift from it. The change log in §7 is written
by hand against that registry.
Ignore-impact levels, ordered by increasing danger — note that failing loudly ranks safer than quietly returning different numbers:
| Impact | Meaning |
|---|---|
inert |
Presentation metadata the engine never consumed. Results are identical. |
degraded |
No wrong numbers — the request fails loudly instead (a structured error, or a query name that stops resolving). |
documented |
Numbers change, but predictably and in a documented direction (LEFT vs INNER, NULL vs 0). |
silent |
Numbers are wrong and the caller cannot tell. Must be listed under requires. |
No key shipped today is silent — which is exactly why basic mode is a
coherent thing to offer: ignoring the Datus extensions is currently always safe.
When to bump:
- MINOR — purely additive: a new key, or a new accepted value for an
existing key that no old payload could have used. Old payloads keep working
unchanged; a new payload on an old engine gets
datus_ext_version_aheadand loses only the new key. A new key whose impact issilentis still a minor bump, but the producer must list it underrequires. - MAJOR — only when an existing key changes meaning, is renamed, or is
withdrawn. Every payload declaring an older
vis then rejected per changed key, with an instruction for updating the YAML. - Neither — engine-internal work that changes no key's contract. A version with no key behind it would make the registry's invariants vacuous.
3. Extension catalog¶
D-JOIN — relationship join type¶
Where: a Relationship's custom_extensions.
Payload key: join_type, enum "left" | "inner".
Default (absent): "left".
Controls how a fan-out branch follows this relationship (many→one) when it joins the "one" side in to reach a dimension or an off-base measure:
"left"(default): keep rows on the many side that have no match — orphan fact rows survive and land in the NULL group of any dimension drawn from the missing side. Reconciliation / audit semantics."inner": drop unmatched many-side rows. Attribution semantics — a measure is only counted where the entity it attributes to actually exists.
This makes the INNER-vs-LEFT choice an explicit property of the relationship
instead of a hard-coded generator default (see docs/semantics.md S-JOIN).
relationships:
- name: sales_to_regions
from: sales
to: regions
from_columns: [region_id]
to_columns: [region_id]
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.0", "join_type": "inner"}'
D-FILL — metric null-fill for missing merge groups¶
Where: a Metric's custom_extensions.
Payload key: fill_nulls_with, a JSON number.
Default (absent): engine default — a bare COUNT metric fills 0
(docs/semantics.md S-FAN-6); every other metric keeps NULL.
An explicit fill_nulls_with takes precedence over the S-FAN-6 count default
and applies to any metric kind (SUM, ratio, expression), COALESCE-ing the
metric's output to the given number in both merge and single-branch plans.
After a multi-branch merge, a group present in only some branches projects
NULL for the metrics evaluated in the missing branch. fill_nulls_with: <n>
COALESCEs this metric's value to <n> (typically 0) so a group with no
contributing rows reports a concrete number instead of a blank.
Scope guard (unchanged from S-FAN-6): fill is applied to the metric's output,
never to a measure embedded in a ratio/expression — a count denominator must
not become a literal 0 divisor. For a ratio, fill the ratio, and the engine
still evaluates it null-safely.
metrics:
- name: sales_total
expression:
dialects:
- dialect: ANSI_SQL
expression: SUM(sales.amount)
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.0", "fill_nulls_with": 0}'
D-TIME — primary (aggregation) time dimension¶
Where: a Dataset's or a Metric's custom_extensions.
Payload key: time_dimension, a string name reference — on a dataset,
the name of one of its own is_time fields; on a metric, "field" or
"dataset.field".
Default (absent): a dataset with exactly one is_time field gets it as
implicit primary time (this inference reads no extension, so it applies in
basic mode too); zero or several is_time fields → no primary time.
Declares which time column is a table's/metric's business time axis — the column the engine uses when a query asks for a time range or grain without naming a column. Resolution per metric (S-TIME-5):
- the metric's own
time_dimension(highest precedence); - else the unique primary time among the metric's datasets (explicit dataset
time_dimension, else the single-is_timeinference).
A metric-level bare "field" resolves against the metric's own datasets first
(time-field names like etl_dt recur across datasets), widening to the whole
model only if none of them has the field.
Queries consume this through the reserved name metric_time
(--group-by metric_time:month, --time-dimension metric_time), and through
the time-range fallback: a range with no named dimension and no time item in
the group-by filters each metric's primary time — per aggregation branch, on
that branch's own column. A metric with no resolvable primary time yields
no_primary_time_dimension; metrics sharing a branch with different primary
times yield metric_time_conflict.
The reference must exist and be dimension.is_time: true, else
invalid_datus_extension.
datasets:
- name: orders
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.1", "time_dimension": "order_date"}'
fields:
- name: order_date
dimension: { is_time: true }
- name: ship_date
dimension: { is_time: true }
metrics:
- name: shipped_revenue
expression:
dialects: [{ dialect: ANSI_SQL, expression: SUM(orders.amount) }]
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.1", "time_dimension": "ship_date"}'
D-GRAIN — native time granularity¶
Where: a Field's custom_extensions (the field must be
dimension.is_time: true).
Payload key: time_granularity, enum "day" | "week" | "month" |
"quarter" | "year".
Default (absent): unknown — any query-time grain may be requested.
Declares the grain the column is stored at (a monthly snapshot's etl_dt,
say). Requesting a strictly finer grain (:day on a month column, via an
explicit group-by item or via metric_time) is a structured grain_too_fine
error instead of silently wrong data. Truncation to the native grain or
coarser is unchanged. Interim vehicle for the upstream RFC's
dimension.time.granularity.
fields:
- name: etl_dt
dimension: { is_time: true }
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.1", "time_granularity": "month"}'
D-DATASET — metric home dataset¶
Where: a Metric's custom_extensions.
Payload key: dataset, a string naming a dataset of the model.
Default (absent): attribution is derived from the metric SQL alone.
A COUNT(*) names no column, so in a multi-dataset model with no other
aggregate pinning a dataset, OSI leaves its attribution undefined and the
engine errors (count_star_needs_dataset). The hint resolves exactly that
tie. It never overrides SQL-derived attribution — a metric whose aggregates
name columns keeps their datasets regardless of the hint (extensions never
redefine what the SQL says). An unknown dataset name is
invalid_datus_extension.
metrics:
- name: chat_message_count
expression:
dialects: [{ dialect: ANSI_SQL, expression: "COUNT(*)" }]
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.1", "dataset": "chat_record"}'
D-WINDOW — derived window metrics (period-over-period / rolling / cumulative)¶
Where: a Metric's custom_extensions.
Payload key: window, a JSON object — full schema in
window-extension.md.
Default (absent): the metric is its plain base aggregate — no derivation.
Declares a window derivation over the metric's own expression (which must
infer as a single plain aggregate): the three sugars pop / rolling /
cumulative, or the general offset / frame form they desugar to. The
time axis is not declared here — D-WINDOW composes with D-TIME (metric
time_dimension > dataset primary; a missing axis fails at query time with
no_primary_time_dimension). Offsets lower to a calendar-correct shifted-key
self-join with automatic scan expansion + output trim; frames lower to
agg(v) OVER (… ROWS …), with reset adding a DATE_TRUNC partition key
and its own expansion + post-window trim. v1 query restrictions (one axis /
one family / one offset / one reset-with-start / single branch) are
structured not_implemented errors; a reset finer than the queried grain is
window_reset_too_fine. D-FILL never applies to derived outputs (NULL means
"no comparable prior period"). Raw window SQL in metric expressions stays
rejected (window_in_metric) — this extension is the only window path.
metrics:
- name: revenue_mom_growth
expression:
dialects: [{ dialect: ANSI_SQL, expression: "SUM(orders.amount)" }]
custom_extensions:
- vendor_name: DATUS
data: '{"v": 1, "window": {"type": "pop", "offset": "1 month"}}'
D-DERIVE — derived metrics (filter / compose)¶
Where: a Metric's custom_extensions.
Payload key: derive, a JSON object with a type discriminant.
Default (absent): the metric is self-contained, no structure metadata.
Spec: design/rfc-derived-metrics.md (M1 ships filter + compose;
reagg and window.base are reserved and reject not_implemented with a
hint naming their milestone).
Declares how a metric derives from other metrics of the same model —
structure OSI expressions cannot carry. The metric's expression stays a
legal, semantically equivalent OSI aggregate (basic mode computes it and
gets the same numbers); the engine enforces that equivalence at compile
time (derive_expression_mismatch), so treat the expression as
tool-maintained output, not something to edit by hand.
filter—{"type":"filter","base":<metric>,"where":<predicate>}: a dimensional-filter variant ofbase. The predicate folds into the aggregate argument (CASE WHEN pred THEN arg END;COUNTbases countTHEN 1), never aWHERE— non-matching time buckets keep existing (value NULL), which is what makes the variant safe under ROWS-framed windows. M1 limits the predicate to the base's own dataset (a cross-dataset predicate isnot_implementeduntil M4) andbaseto a plain non-derived single aggregate. The engine records the subset relation (subset_of) in metric metadata.compose—{"type":"compose","expr":<arithmetic>,"fill":<n>}: scalar arithmetic over metric names ("revenue - total_refunds"). Identifiers resolve strictly to metric names — a qualified or unknown name is an error with candidates, never a silent column read. Aggregate calls are rejected (composition never re-aggregates).fillreplaces a member's NULL after branch alignment, before the arithmetic (the cross-fact "missing bucket poisons the sum" fix). The engine detects linear combinations (Σ cᵢ·metricᵢ), records members + coefficients + the leaf-measure decomposition, and fixes the conformed dimension set (dimensions every member branch reaches, under the unique-join-path rule) at compile time —dosi listexposes it, and a query grouping outside it getsunconformed_dimensionnaming the member that misses the dimension. Members sharing no dimension warncompose_no_conformed_dimensionsat compile time. Attribution metadata is labeledexactonly for linear composes over SUM/COUNT-tier measures (a filteredCOUNT(DISTINCT)overlaps its complement — never exact).
Cycles among metric references are metric_reference_cycle with the full
path. Metadata surfaces on every host (dosi list, REST, MCP, Python):
derive_family, derive_base, subset_of, derive_members,
leaf_measures, attribution, conformed_dimensions.
metrics:
- name: net_revenue
expression:
dialects: [{ dialect: ANSI_SQL, expression: "COALESCE(SUM(orders.amount), 0) - COALESCE(SUM(refunds.ref_amount), 0)" }]
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.4", "derive": {"type": "compose", "expr": "revenue - total_refunds", "fill": 0}}'
D-MEASURE — explicit measure name¶
Where: a Metric's custom_extensions.
Payload key: measure, {"name": <identifier>}.
Default (absent): the sanitizer-generated stem (S-MEASURE-1).
Spec: design/rfc-minimal-declarations.md §2.
Names the metric's single synthesized measure explicitly. Measure names are
otherwise derived by sanitizing the aggregate argument ASCII-only, so two
aggregates differing only in non-ASCII content ('新品' / '老品' CASE
literals) collapse to one stem and fail measure_name_collision — the
model cannot compile at all. The explicit name is a label: dedup still
keys on the aggregate signature (an undeclared textual twin shares the
declared name), the name enters the same collision checks (two explicit
names on one signature, or one name on two signatures, still collide
loudly), and it applies only to single-aggregate metrics. Because measure
names surface in result columns and metadata, ignoring this key silently
renames them — emit it under requires so older engines refuse instead
(the first silent-impact key since requires shipped).
metrics:
- name: 新品收入
expression:
dialects: [{ dialect: ANSI_SQL, expression: "SUM(CASE WHEN orders.category = '新品' THEN orders.amount END)" }]
custom_extensions:
- vendor_name: DATUS
data: '{"v": "1.4", "requires": ["measure"], "measure": {"name": "orders_new_product_amount_sum"}}'
4. Precedence & defaults summary¶
| Point | OSI core | Datus extension | Engine default when absent |
|---|---|---|---|
| relationship join type | undefined (LEFT assumed) | D-JOIN join_type |
left |
| missing-group metric value | undefined | D-FILL fill_nulls_with |
count→0 (S-FAN-6), else NULL |
| primary time dimension | undefined (is_time only) |
D-TIME time_dimension (metric > dataset) |
the dataset's single is_time field, else none |
| native time grain | undefined | D-GRAIN time_granularity |
unknown — any grain allowed |
COUNT(*) home dataset |
undefined in multi-dataset models | D-DATASET dataset |
SQL-derived, else count_star_needs_dataset |
| window derivation | inexpressible (window SQL rejected) | D-WINDOW window |
plain base aggregate |
5. Validation & engine modes¶
- OSI gate (
scripts/validate_osi.py): unaffected —custom_extensionsis valid OSI, so all fixtures still PASS ossie. - Datus self-validation (engine in
--osi-datusmode, the default, when it reads aDATUSentry): the payload must be a JSON object; known keys must match their declared type/enum; otherwiseinvalid_datus_extension. Unknown keys pass (forward-compat) unless named inrequires. The envelope version gate (§2) runs here, once per payload — including on theSemanticModelcarrier, which defines no keys of its own but is still held to the envelope. - Basic mode (
--osi-basic): the engine treatsDATUSlike any unknown vendor — the extension is inert (spec-compliant passthrough) and the compile collects oneignored_vendor_extensionwarning per carrying element — SemanticModel, Dataset, Field, Relationship, and Metric alike — naming the default that applies instead (D-JOIN → LEFT, D-FILL → no fill beyond S-FAN-6, D-TIME → single-is_timeinference only, D-GRAIN → any grain, D-DATASET → SQL-derived attribution only, D-WINDOW → the plain base aggregate). The payload is not parsed, so a malformed payload is also just ignored-with-warning — and no version diagnostic can occur in basic mode at all. The same model file is valid in both modes; only the semantics differ. (The single-is_timeprimary-time inference is not an extension — it applies in basic mode too, sometric_timestill works wherever pure OSI determines a unique time axis.) - Implementation boundary: all extension reading — and the mode branch
itself — lives in one module,
crates/dosi-compiler/src/ext.rs. Future extensions land there with the same shape; the rest of the engine stays extension-agnostic.
6. Non-goals / boundaries¶
- Datus extensions never redefine an OSI object's identity or a metric's SQL — only engine behavior at OSI-undefined seams.
- They are not a place to smuggle vendor SQL. Metric expressions stay OSI
expression.dialects; the engine still infers metric semantics from that SQL. - Semantics that belong in OSI core (granularity, time spine, window/cumulative
metrics) are drafted for upstream in
../design/rfc-time-semantics.md; if/when adopted there, the corresponding Datus extension (D-GRAIN, D-WINDOW) is retired in favor of the core field.
7. Change log¶
Each released minor and the keys it added. The machine-readable form of this
table is capabilities().history — dosi info --format json or
GET /v1/capabilities.
| Version | Date | Keys added | Impact if ignored |
|---|---|---|---|
| 1.0 | 2026-07-11 | join_type (D-JOIN), fill_nulls_with (D-FILL) |
documented, documented |
| 1.1 | 2026-08-02 | time_dimension (D-TIME), time_granularity (D-GRAIN), dataset (D-DATASET) |
degraded, documented, degraded |
| 1.3 | 2026-08-09 | (none — in-family growth of window) |
(inherits window's documented) |
| 1.4 | 2026-08-12 | derive (D-DERIVE M1), measure (D-MEASURE) |
degraded, silent |
- 1.0 (2026-07-11): initial —
D-JOIN(relationshipjoin_type, implemented: dosi-compiler reads it intoRelationshipIr.join_kind, planner emits INNER/LEFT accordingly),D-FILL(metricfill_nulls_with, implemented 2026-07-12:MetricIr.fill_nulls_with→ planner COALESCEs the metric output; overrides the S-FAN-6 count default, any metric kind). - 1.1 (2026-08-02): time & attribution —
D-TIME(dataset/metrictime_dimension, implemented:DatasetIr.primary_time_dimension+MetricIr.time_dimension, the reservedmetric_timequery name, per-branch time-range fallback),D-GRAIN(fieldtime_granularity, implemented:FieldIr.time_granularity, finer-grain requests →grain_too_fine),D-DATASET(metricdataset, implemented:COUNT(*)home attribution). Basic-modeignored_vendor_extensionwarnings now cover all five carrier objects (previously only Relationship and Metric). - 1.1 (2026-08-04, no version bump): the envelope version gate itself —
vis now read and enforced (§2),requiresis introduced, each key carries a declared ignore-impact, and the whole registry is published throughdosi info,GET /v1/capabilities, anddosi_engine.DATUS_EXT. No key was added or changed, so the version stays at 1.1. Payloads without av— which is what the Datus agent emits today — are unaffected. - 1.2 (2026-08-06): window metrics —
D-WINDOW(metricwindow, implemented:MetricIr.window→ the planner'sWindowStageprojection root; pop/rolling/cumulative sugars + general offset/frame form, cumulative reset with scan expansion + post-window trim; ignore-impactdocumented— the metric falls back to its plain base aggregate; spec inwindow-extension.md). Promotes the 13 baisheng derived-time cases Deferred→Supported (55/2). Initially DuckDB-executed; the all-dialect enablement (per-dialect offset-shift lowerings, all-dialect snapshots, live-corpus verification,datus_windowcorpus scenario) landed as a follow-up — per-dialect status in../design/window-metrics-v2.md§1. In-family additions (same minor, fail-closed on engines predating them): mixed families / mixed shifts / mixed resets-with-start relaxed, andrequire_full_windowon finite frames (2026-08-06) — seewindow-extension.md#restrictions. - 1.3 (2026-08-09): window families & functions — grows the
windowkey in-family, no new registry row (capabilities().historystill ends at 1.2; the version signals the payload dialect). Additions: therankfamily (row_number / rank / dense_rank / ntile / percent_rank / cume_dist over the metric value), thevaluefamily (first_value / last_value / nth_value), offsetdirection: "forward"(LEAD — next-period reference), frameorder/partition/units/secondmodifiers (value-ordered and RANGE frames, partition modes, two-argument inputs), and registry levels W2 (stddev_pop/samp,var_pop/samp) + W3 (covar_pop/samp,corr). Every addition fails closed on a 1.2 engine: unknown top-level family keys fall into the exactly-one-family error, unknown in-family keys hitdeny_unknown_fields, unknown function names fail enum parse — and a declared"v": "1.3"trips the version gate besides. Spec:window-extension.md(family sections + function registry). - 1.4 (2026-08-12): derived metrics & measure naming —
D-DERIVEM1 (derivekey, implemented:filter+composefamilies, theresolve_metric_refspass with cycle detection, the expression equivalence gate, compile-time conformed dimensions, and derive metadata on every host surface;reagg/ cross-datasetfilterpredicates /window.baseare reserved and rejectnot_implementednaming their milestone — notewindow.basepreviously fell into the silently-ignored unknown-key bucket and now fails closed) andD-MEASURE(measurekey, implemented: explicit measure naming, signature-keyed dedup and collision checks unchanged — the fix for the non-ASCII stem collision, and the firstsilent-impact key: emit it underrequires). Specs:design/rfc-derived-metrics.md,design/rfc-minimal-declarations.md.
Not yet implemented (roadmap)¶
The queued RFCs are all additive, so each lands as one minor bump with its own registry rows. Order may change; every entry occupies a minor of its own.
| Minor | RFC | Keys | Carrier | Impact | requires? |
|---|---|---|---|---|---|
| 1.5 | rfc-semi-additive-metrics §4 |
semi_additive |
Metric | silent — an end-of-period balance gets summed across snapshots |
yes |
| 1.6 | rfc-minimal-declarations §3 D-DISTINCT-STATE |
distinct_state |
Field / Dataset | silent — a materialized bitmap column is aggregated as if it were an ordinary column |
yes |
| ≥1.5 | rfc-derived-metrics M2-M4 |
window.base, derive reagg + cross-dataset filter |
Metric | degraded/error — reserved shapes reject not_implemented today, never silently compute |
no |
| — | window-extension.md rank/share/streak families, W2+ functions |
future window shapes (in-family additions, fail-closed on older engines) |
Metric | degraded |
no |
The first silent key shipped at 1.4 (measure, D-MEASURE). If you
author models against a Datus agent that emits silent-impact keys, keep an
eye on the requires list it produces: that is what stops an older engine
from quietly returning wrong numbers, and it only works because requires
itself shipped at 1.1 — ahead of the first key that needs it.