Skip to content

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, or dosi_engine.DATUS_EXT_VERSION.
  • Scope: semantics that OSI core 0.2.0.dev0 cannot yet express, carried inside the OSI-sanctioned custom_extensions field 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 full agent.yml can be passed as --connections directly — 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:
  - vendor_name: <string>
    data: <JSON string>

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 lowercase datus keeps working.
  • data: a JSON string (per the OSI spec data is a string, not an object) decoding to a single JSON object — the payload. At most one DATUS entry per object. Two envelope keys are reserved: v and requires.

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 but DATUS, compared case-insensitively) is ignored.
  • A malformed DATUS payload (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.10 and 1.1 are the same f64, 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

data: '{"v": "1.4", "requires": ["semi_additive"], "semi_additive": {"reduce": "last"}}'

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_ahead and loses only the new key. A new key whose impact is silent is still a minor bump, but the producer must list it under requires.
  • MAJOR — only when an existing key changes meaning, is renamed, or is withdrawn. Every payload declaring an older v is 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):

  1. the metric's own time_dimension (highest precedence);
  2. else the unique primary time among the metric's datasets (explicit dataset time_dimension, else the single-is_time inference).

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 of base. The predicate folds into the aggregate argument (CASE WHEN pred THEN arg END; COUNT bases count THEN 1), never a WHERE — 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 is not_implemented until M4) and base to 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). fill replaces 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 list exposes it, and a query grouping outside it gets unconformed_dimension naming the member that misses the dimension. Members sharing no dimension warn compose_no_conformed_dimensions at compile time. Attribution metadata is labeled exact only for linear composes over SUM/COUNT-tier measures (a filtered COUNT(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_extensions is valid OSI, so all fixtures still PASS ossie.
  • Datus self-validation (engine in --osi-datus mode, the default, when it reads a DATUS entry): the payload must be a JSON object; known keys must match their declared type/enum; otherwise invalid_datus_extension. Unknown keys pass (forward-compat) unless named in requires. The envelope version gate (§2) runs here, once per payload — including on the SemanticModel carrier, which defines no keys of its own but is still held to the envelope.
  • Basic mode (--osi-basic): the engine treats DATUS like any unknown vendor — the extension is inert (spec-compliant passthrough) and the compile collects one ignored_vendor_extension warning 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_time inference 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_time primary-time inference is not an extension — it applies in basic mode too, so metric_time still 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().historydosi 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 (relationship join_type, implemented: dosi-compiler reads it into RelationshipIr.join_kind, planner emits INNER/LEFT accordingly), D-FILL (metric fill_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/metric time_dimension, implemented: DatasetIr.primary_time_dimension + MetricIr.time_dimension, the reserved metric_time query name, per-branch time-range fallback), D-GRAIN (field time_granularity, implemented: FieldIr.time_granularity, finer-grain requests → grain_too_fine), D-DATASET (metric dataset, implemented: COUNT(*) home attribution). Basic-mode ignored_vendor_extension warnings now cover all five carrier objects (previously only Relationship and Metric).
  • 1.1 (2026-08-04, no version bump): the envelope version gate itself — v is now read and enforced (§2), requires is introduced, each key carries a declared ignore-impact, and the whole registry is published through dosi info, GET /v1/capabilities, and dosi_engine.DATUS_EXT. No key was added or changed, so the version stays at 1.1. Payloads without a v — which is what the Datus agent emits today — are unaffected.
  • 1.2 (2026-08-06): window metrics — D-WINDOW (metric window, implemented: MetricIr.window → the planner's WindowStage projection root; pop/rolling/cumulative sugars + general offset/frame form, cumulative reset with scan expansion + post-window trim; ignore-impact documented — the metric falls back to its plain base aggregate; spec in window-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_window corpus 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, and require_full_window on finite frames (2026-08-06) — see window-extension.md#restrictions.
  • 1.3 (2026-08-09): window families & functions — grows the window key in-family, no new registry row (capabilities().history still ends at 1.2; the version signals the payload dialect). Additions: the rank family (row_number / rank / dense_rank / ntile / percent_rank / cume_dist over the metric value), the value family (first_value / last_value / nth_value), offset direction: "forward" (LEAD — next-period reference), frame order / partition / units / second modifiers (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 hit deny_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-DERIVE M1 (derive key, implemented: filter + compose families, the resolve_metric_refs pass with cycle detection, the expression equivalence gate, compile-time conformed dimensions, and derive metadata on every host surface; reagg / cross-dataset filter predicates / window.base are reserved and reject not_implemented naming their milestone — note window.base previously fell into the silently-ignored unknown-key bucket and now fails closed) and D-MEASURE (measure key, implemented: explicit measure naming, signature-keyed dedup and collision checks unchanged — the fix for the non-ASCII stem collision, and the first silent-impact key: emit it under requires). 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.