跳转至

Datus 对 OSI 的扩展(厂商规范)

  • 状态:草案,Datus 已在用(Dosi 与 Datus 的模型生成 Agent)。
  • 规范版本datus-ext/1(主版本系列)
  • 当前版本1.1 —— 每个小版本加了什么、以及版本号如何决定, 见 §2.1。任何引擎都能报告自己实现的版本: dosi infoGET /v1/capabilities,或 dosi_engine.DATUS_EXT_VERSION
  • 范围:OSI 核心 0.2.0.dev0 还无法表达的那些语义, 统统装在 OSI 官方认可的 custom_extensions 字段内部, 这样文档仍然是完全合法的 OSI,而每个非 Datus 的消费方都会忽略它们。

想要带前后对比示例的任务式讲解,见 Datus 扩展指南。这一页是规范性契约。

除了模型语义之外,Dosi 的连接配置也讲 Datus: 配置沿用 agent.yml 的 datasources: 词汇表,一份完整的 agent.yml 可以直接作为 --connections 传入 —— 见 connectors.md

这是一个私有的厂商扩展,不必经 OSI 工作组接纳即可使用。 当某个扩展所泛化的空缺本该属于核心规范时(时间粒度、累计窗口), 它的长期归宿是 rfc-time-semantics.md 里的 OSI RFC;本文档是 Datus 今天据以发布的过渡载体。

1. 为什么这对 OSI 是安全的

OSI 的 schema 很严格(到处都是 additionalProperties: false), 所以我们不能直接加 relationship.join_type 这样的裸字段。 但规范在每个主要对象上都提供了一等公民级别的逃生舱:

custom_extensions:
  - vendor_name: <string>
    data: <JSON string>

custom_extensions 存在于 SemanticModel、Dataset、Field、Relationship 和 Metric 之上(见 crates/dosi-model/src/spec.rs)。上游的 ossie 校验器 接受它,所以一份携带 Datus 扩展的文档原样通过 validate-osi 这道关。 不认识 vendor_name: DATUS 的消费方直接跳过, 模型的含义仍精确等于纯 OSI 所说的含义。Datus 扩展只在 OSI 未作定义之处 细化引擎行为;它们绝不改变一个指标或维度是什么

2. 信封

一个 Datus 扩展就是一条 custom_extensions 条目:

  • vendor_name:规范写法是大写常量 "DATUS"(与 OSI 规范的大写枚举风格一致, 比如 ANSI_SQL)。匹配时不区分大小写;历史上的小写 datus 继续可用。
  • data:一个 JSON 字符串(按 OSI 规范,data 是字符串而不是对象), 解码后是单个 JSON 对象 —— 即载荷。每个对象上最多一条 DATUS 条目。 有两个信封键是保留的:vrequires

消费规则(Dosi):

  • 不存在 → 采用下文写明的 OSI 默认行为。扩展纯粹是增量的, 一个没有任何扩展的模型,行为与今天完全一致。
  • 未知的载荷键会被忽略,而不是被拒绝:这样更新的 Datus Agent 可以输出老引擎不读的键,而不至于把它弄坏(前向兼容)。 例外是下面的 requires
  • 未知的 vendor_name(除 DATUS 之外的一切,比较时不区分大小写)会被忽略。
  • 格式不对的 DATUS 载荷(不是 JSON,或某个已知键的类型不对)是一个 结构化错误invalid_datus_extension),而不是悄悄取默认值: 一份由 Datus 编写的文档要按 Datus 的契约来要求。编译仍会带着默认值继续, 这样一趟就能报出模型里的所有问题。

v —— 声明的版本

v 的形式是 MAJOR.MINOR字符串形式是规范写法("v": "1.1"); 为方便起见也接受 JSON 数字("v": 1.1,以及裸整数 "v": 1 表示 1.0)。

JSON 数字只能表达 0–9 的小版本号。 1.101.1 是同一个 f64, 所以数字无法表达小版本 10;带两位及以上小数的数字会被直接拒绝, 并提示你改用字符串形式。一旦当前小版本到达 10,请一律把 "v" 写成字符串。

v 是可选的,省略它永远是安全的 —— 一个未声明版本的载荷, 行为与版本闸门存在之前完全一致,也不会产生任何诊断信息。 版本是custom_extensions 条目声明的,而不是按文档: 模型级的 v 不会成为它下面数据集、字段、关系或指标的默认值。

给定引擎自身的版本,标注版本能给你带来什么:

情形 引擎的反应 错误码
v 不存在或为 null 静默 —— 行为与闸门出现之前一致
v 格式不对(坏字符串、JSON 类型不对、带 ≥2 位小数的数字) 错误 invalid_datus_extension
v 低于 1.0,或主版本比引擎的新 错误 unsupported_datus_ext_version
小版本比引擎的新 告警并点名被丢弃的键;引擎认识的一切仍然生效 datus_ext_version_ahead
某个键比你声明的 v 更新 告警;但该键仍会被采纳 datus_ext_key_newer_than_declared
小版本比引擎的旧,且所用键都在其范围内 静默

未来的主版本是唯一一种一刀切的拒绝。主版本号提升意味着至少有一个键改变了含义, 而早于这次改变的引擎无从知道是哪一个,硬着头皮读那个载荷会产生悄无声息的错误数字。 小版本按构造就是增量的,所以它们最多只是告警。

requires —— 不允许被丢弃的键

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

requires 是一个键名数组,生产方借此声明这些键不得被悄悄忽略。 没有实现其中某个键的消费方会抛出 datus_ext_key_required 并点名该键, 而不是丢弃它。

它之所以存在,是因为消费方无法判断自己从没听说过的键有多危险。 对 join_type 这样的键,"未知键被忽略"是正确的默认: 忽略它只是退回到一个有文档的行为。但对 semi_additive 这样的键就是错误的默认: 忽略它意味着把期末余额加总起来,返回一个干脆就是错的数字。 生产方知道哪个是哪个,requires 就是它表达这一点的方式。

对每一个忽略影响为 silent 的键(见 §2.1)都要输出 requires, 其余一概不要。让老引擎降级运行,好过直接拒绝为它服务。 dosi info / GET /v1/capabilities 会报告每个键的影响级别。

2.1 版本策略

本引擎读取的每个键都登记了引入它的版本,以及消费方忽略它的代价。 这份登记表位于 crates/dosi-compiler/src/ext.rs,是唯一事实来源: 版本闸门、基础模式的告警文案、dosi infoGET /v1/capabilities 都在运行时读取它,所以它们谁也不可能与之漂移。§7 的变更记录是照着这份登记表手写的。

忽略影响级别,按危险程度递增排列 —— 注意大声失败排在比悄悄返回不同数字更安全的位置:

影响 含义
inert 引擎从不消费的展示类元数据。结果完全相同。
degraded 不会出错误数字 —— 请求会大声失败(一个结构化错误,或某个查询名不再能解析)。
documented 数字会变,但变得可预期、方向有文档可依(LEFT 对 INNER,NULL 对 0)。
silent 数字是错的,而调用方无从察觉。必须列进 requires

今天发布的键里没有任何一个是 silent, 这恰恰是基础模式作为一个选项能自洽的原因:目前忽略 Datus 扩展总是安全的。

什么时候提升版本号:

  • MINOR —— 纯增量:一个新键,或者某个已有键的一个新可接受取值 (老载荷不可能用过它)。老载荷原样继续工作;新载荷跑在老引擎上会得到 datus_ext_version_ahead,只损失那个新键。 一个影响为 silent 的新键仍然只是小版本提升, 但生产方必须把它列进 requires
  • MAJOR —— 只有当某个已有键改变了含义、被重命名或被撤销时。 此后每个声明了更旧 v 的载荷都会按改变了的键被逐一拒绝, 并给出更新 YAML 的指引。
  • 两者都不 —— 不改变任何键契约的引擎内部工作。 一个背后没有任何键的版本会让登记表的不变式变得空洞。

3. 扩展目录

D-JOIN —— 关系的连接类型

位置:一个 Relationshipcustom_extensions载荷键join_type,枚举 "left" | "inner"默认(不存在时)"left"

控制一个扇出分支在沿这个关系(多→一)把"一"端 JOIN 进来、 以到达某个维度或某个不在基上的度量时,行为如何:

  • "left"(默认):保留多端没有匹配的行 —— 孤儿事实行会存活下来,落进任何取自缺失那一侧的维度的 NULL 分组里。 对账/审计语义。
  • "inner":丢弃未匹配的多端行。归因语义 —— 一个度量只在它所归因的实体确实存在时才被统计。

这让 INNER 还是 LEFT 成为关系的一个显式属性, 而不是生成器里写死的默认值(见 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 —— 合并时缺失分组的指标空值填充

位置:一个 Metriccustom_extensions载荷键fill_nulls_with,一个 JSON 数字默认(不存在时):引擎默认 —— 一个纯 COUNT 指标填 0docs/semantics.md S-FAN-6);其余每个指标保持 NULL

显式的 fill_nulls_with 优先于 S-FAN-6 的计数默认值, 并且对任何种类的指标都适用(SUM、比率、表达式), 在合并计划和单分支计划里都会把该指标的输出 COALESCE 成给定的数字。

在一次多分支合并之后,只出现在部分分支里的分组, 对那些在缺失分支中计算的指标会投影出 NULLfill_nulls_with: <n> 会把这个指标的值 COALESCE<n>(通常是 0), 这样一个没有任何贡献行的分组就会报出一个具体数字而不是空白。

作用范围的保护(与 S-FAN-6 一致):填充作用于指标的输出, 绝不作用于嵌在比率/表达式里的某个度量 —— 一个计数作为分母时不能变成字面量 0 除数。对比率来说,请填充那个比率, 引擎仍会以空值安全的方式计算它。

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 —— 主(聚合)时间维度

位置:一个 Dataset 或一个 Metriccustom_extensions载荷键time_dimension,一个字符串名称引用 —— 在数据集上, 是它自己某个 is_time 字段的名字;在指标上,是 "field""dataset.field"默认(不存在时):一个恰好只有一个 is_time 字段的数据集, 会把它作为隐含的主时间(这条推断不读取任何扩展,所以在基础模式下同样适用); 零个或多个 is_time 字段 → 没有主时间。

声明哪一列是一张表/一个指标的业务时间轴 —— 也就是当查询要求某个时间范围或粒度却没有点名列时,引擎会使用的那一列。 每个指标的解析顺序(S-TIME-5):

  1. 该指标自己的 time_dimension(优先级最高);
  2. 否则取该指标所涉数据集中唯一的那个主时间(数据集显式的 time_dimension, 否则是单 is_time 推断)。

指标级的裸 "field" 先在该指标自己的数据集里解析 (像 etl_dt 这样的时间字段名会在多个数据集里重复出现), 只有当它们都没有这个字段时,才放宽到整个模型。

查询通过保留名 metric_time 来消费它 (--group-by metric_time:month--time-dimension metric_time), 也通过时间范围的回退来消费:一个没有点名维度、group-by 里也没有时间项的范围 会去过滤每个指标的主时间 —— 按聚合分支分别进行,用该分支自己的那一列。 一个无法解析出主时间的指标会产生 no_primary_time_dimension; 共处一个分支却有不同主时间的指标会产生 metric_time_conflict

被引用的字段必须存在且为 dimension.is_time: true,否则 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 —— 原生时间粒度

位置:一个 Fieldcustom_extensions(该字段必须是 dimension.is_time: true)。 载荷键time_granularity,枚举 "day" | "week" | "month" | "quarter" | "year"默认(不存在时):未知 —— 可以请求任意的查询时粒度。

声明该列存储时所用的粒度(比如一份按月快照的 etl_dt)。 请求一个严格更细的粒度(在一个 month 列上请求 :day, 无论是通过显式 group-by 项还是通过 metric_time)会得到一个结构化的 grain_too_fine 错误,而不是悄无声息的错误数据。 截断到原生粒度或更粗的粒度则不受影响。 这是上游 RFC 里 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 —— 指标的归属数据集

位置:一个 Metriccustom_extensions载荷键dataset,一个字符串,点名模型里的某个数据集。 默认(不存在时):归属只从指标 SQL 本身推导。

COUNT(*) 没有点名任何列,所以在一个多数据集模型里、 又没有别的聚合把某个数据集钉住时,OSI 让它的归属处于未定义状态, 引擎会报错(count_star_needs_dataset)。这个提示恰好解决那种歧义。 它绝不会覆盖从 SQL 推导出的归属 —— 一个其聚合点名了列的指标, 无论有没有这个提示都保留它们各自的数据集(扩展绝不重新定义 SQL 所说的东西)。 一个不存在的数据集名会是 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 —— 派生的窗口指标(同环比/滚动/累计)

位置:一个 Metriccustom_extensions载荷键window,一个 JSON 对象 —— 完整 schema 见 window-extension.md默认(不存在时):该指标就是它那个朴素的基础聚合 —— 不做任何派生。

在指标自己的 expression(必须能推断为单个朴素聚合)之上声明一种窗口派生: pop / rolling / cumulative 三种语法糖, 或者它们展开成的通用 offset / frame 形态。时间轴在这里声明 —— D-WINDOW 与 D-TIME 组合使用(指标的 time_dimension > 数据集的主时间; 缺失时间轴会在查询时以 no_primary_time_dimension 失败)。 偏移会下降成一个日历正确的移位键自连接,带自动扫描扩展 + 输出裁剪; 窗框会下降成 agg(v) OVER (… ROWS …),其中 reset 会加上一个 DATE_TRUNC 分区键,并有自己的扩展 + 窗口后裁剪。v1 的查询限制 (一个时间轴/一个族/一个偏移/一个带起始的重置/单分支)都是结构化的 not_implemented 错误;比所查询粒度更细的重置是 window_reset_too_fine。 D-FILL 绝不作用于派生输出(NULL 的含义是"没有可比的上一期")。 指标表达式里的裸窗口 SQL 仍然被拒绝(window_in_metric)—— 这个扩展是唯一的窗口通路。

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 —— 派生指标(filter / compose)

位置:一个 Metriccustom_extensions载荷键derive,一个带 type 判别式的 JSON 对象。 默认(不存在时):该指标是自包含的,没有任何结构元数据。 规范design/rfc-derived-metrics.md(M1 发布 filter + composereaggwindow.base 是保留形态,会以 not_implemented 拒绝, 并在提示里点名它们所属的里程碑)。

声明一个指标如何从同一模型的其它指标派生 —— 这是 OSI 表达式承载不了的结构信息。 该指标的 expression 仍然是一个合法且语义等价的 OSI 聚合(基础模式照样计算它, 得到相同的数字);引擎会在编译期强制这份等价性(derive_expression_mismatch), 所以请把这个表达式当作工具维护的产物,而不是可以手改的东西。

  • filter —— {"type":"filter","base":<metric>,"where":<predicate>}base 的一个按维度过滤的变体。谓词会折进聚合的参数里 (CASE WHEN pred THEN arg ENDCOUNT 类基础指标折成 THEN 1), 而绝不是折成 WHERE —— 不匹配的时间分桶依然存在(取值为 NULL), 正是这一点让该变体在 ROWS 窗框下也安全。M1 限制谓词只能作用于基础指标自己的数据集 (跨数据集谓词在 M4 之前是 not_implemented),且 base 必须是一个非派生的 朴素单聚合。引擎会把子集关系(subset_of)记进指标元数据。
  • compose —— {"type":"compose","expr":<arithmetic>,"fill":<n>}: 在指标名之上做标量算术("revenue - total_refunds")。 标识符严格解析为指标名 —— 带限定符或未知的名字都是错误并附候选项, 绝不会悄悄当成列来读。聚合调用会被拒绝(组合绝不重新聚合)。 fill 在分支对齐之后、算术之前替换某个成员的 NULL (用于修正跨事实表时"缺失分桶毒化整个求和"的问题)。 引擎会识别线性组合(Σ cᵢ·metricᵢ),记录成员、系数和叶子度量的分解, 并在编译期固定下一致维度集(在唯一连接路径规则下,每个成员分支都能到达的维度) —— dosi list 会暴露它,而在该集合之外分组的查询会得到 unconformed_dimension, 并点名是哪个成员够不到这个维度。成员之间没有共同维度时, 编译期会给出 compose_no_conformed_dimensions 告警。 归因元数据只有在 SUM/COUNT 层级度量之上的线性组合中才标记为 exact (一个被过滤的 COUNT(DISTINCT) 与它的补集会重叠 —— 永远不可能 exact)。

指标引用之间成环会是 metric_reference_cycle,并给出完整路径。 元数据在每个宿主上都会暴露(dosi list、REST、MCP、Python): derive_familyderive_basesubset_ofderive_membersleaf_measuresattributionconformed_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 —— 显式度量名

位置:一个 Metriccustom_extensions载荷键measure,形如 {"name": <identifier>}默认(不存在时):由净化器生成的 stem(S-MEASURE-1)。 规范design/rfc-minimal-declarations.md §2。

显式命名该指标唯一的那个合成度量。度量名原本是把聚合参数净化成纯 ASCII 得到的, 于是两个只在非 ASCII 内容上有差别的聚合(比如 '新品' / '老品' 这样的 CASE 字面量)会塌成同一个 stem,触发 measure_name_collision —— 模型根本编译不过。 显式名字是一个标签:去重仍然以聚合签名为键(一个没有声明的文本孪生体会共享这个 已声明的名字),这个名字也会进入同一套冲突检查(一个签名上有两个显式名字, 或一个名字落在两个签名上,同样会大声冲突),并且它只适用于单聚合指标。 由于度量名会出现在结果列和元数据里,忽略这个键等于悄悄改名 —— 所以请把它列进 requires,让更旧的引擎直接拒绝 (这是 requires 发布以来第一个 silent 影响级别的键)。

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. 优先级与默认值汇总

关注点 OSI 核心 Datus 扩展 不存在时的引擎默认
关系的连接类型 未定义(假定 LEFT) D-JOIN join_type left
缺失分组的指标值 未定义 D-FILL fill_nulls_with 计数→0(S-FAN-6),其余 NULL
主时间维度 未定义(只有 is_time D-TIME time_dimension(指标 > 数据集) 数据集里唯一的 is_time 字段,否则无
原生时间粒度 未定义 D-GRAIN time_granularity 未知 —— 任意粒度均可
COUNT(*) 的归属数据集 在多数据集模型里未定义 D-DATASET dataset 由 SQL 推导,否则 count_star_needs_dataset
窗口派生 无法表达(窗口 SQL 被拒绝) D-WINDOW window 朴素的基础聚合

5. 校验与引擎模式

  • OSI 关卡scripts/validate_osi.py):不受影响 —— custom_extensions 是合法 OSI,所以所有 fixture 仍然能 PASS ossie。
  • Datus 自校验(引擎处于 --osi-datus 模式,即默认模式,且读到一条 DATUS 条目时):载荷必须是一个 JSON 对象;已知键必须匹配其声明的类型/枚举; 否则 invalid_datus_extension。未知键放行(前向兼容), 除非它被 requires 点名。信封的版本闸门(§2)在这里运行,每个载荷一次 —— 包括 SemanticModel 这个载体,它自己不定义任何键,但仍要遵守信封规则。
  • 基础模式--osi-basic):引擎把 DATUS 当作任何未知厂商来对待 —— 扩展是惰性的(符合规范的原样透传),编译过程会为每个携带它的元素收集一条 ignored_vendor_extension 告警 —— SemanticModel、Dataset、Field、 Relationship、Metric 一视同仁 —— 并点名改为适用的那个默认值 (D-JOIN → LEFT,D-FILL → 除 S-FAN-6 外不做填充, D-TIME → 只做单 is_time 推断,D-GRAIN → 任意粒度, D-DATASET → 只做 SQL 推导的归属,D-WINDOW → 朴素的基础聚合)。 载荷不会被解析,所以格式不对的载荷同样只是被忽略并告警 —— 而且在基础模式下根本不会产生任何版本诊断。同一个模型文件在两种模式下都合法; 区别只在语义。(单 is_time 的主时间推断不是扩展 —— 它在基础模式下同样适用,所以只要纯 OSI 能确定一条唯一的时间轴, metric_time 就仍然可用。)
  • 实现边界:所有扩展读取逻辑 —— 以及模式分支本身 —— 都住在一个模块里, crates/dosi-compiler/src/ext.rs。将来的扩展也以同样的形态落在那里; 引擎的其余部分对扩展一无所知。

6. 非目标/边界

  • Datus 扩展绝不重新定义一个 OSI 对象的身份或一个指标的 SQL —— 只在 OSI 未定义的接缝处影响引擎行为。
  • 它们不是夹带厂商 SQL 的地方。指标表达式仍然放在 OSI 的 expression.dialects 里;引擎依旧从那段 SQL 推断指标语义。
  • 那些本该属于 OSI 核心的语义(粒度、时间轴表、窗口/累计指标) 已在 ../design/rfc-time-semantics.md 里起草并提交上游; 一旦在那里被采纳,对应的 Datus 扩展(D-GRAIN、D-WINDOW)就会退役, 改用核心字段。

7. 变更记录

每个已发布的小版本,以及它新增的键。这张表的机器可读形式是 capabilities().history —— dosi info --format jsonGET /v1/capabilities

版本 日期 新增的键 忽略时的影响
1.0 2026-07-11 join_type (D-JOIN)、fill_nulls_with (D-FILL) documenteddocumented
1.1 2026-08-02 time_dimension (D-TIME)、time_granularity (D-GRAIN)、dataset (D-DATASET) degradeddocumenteddegraded
1.3 2026-08-09 (无 —— window 的族内扩展) (继承 windowdocumented
1.4 2026-08-12 derive(D-DERIVE M1)、measure(D-MEASURE) degradedsilent
  • 1.0(2026-07-11):初版 —— D-JOIN(关系的 join_type已实现:dosi-compiler 把它读进 RelationshipIr.join_kind, 规划器据此发出 INNER/LEFT)、D-FILL(指标的 fill_nulls_with, 2026-07-12 已实现MetricIr.fill_nulls_with → 规划器对指标输出做 COALESCE; 覆盖 S-FAN-6 的计数默认值,对任何种类的指标都适用)。
  • 1.1(2026-08-02):时间与归属 —— D-TIME(数据集/指标的 time_dimension已实现DatasetIr.primary_time_dimension + MetricIr.time_dimension,保留的 metric_time 查询名, 按分支的时间范围回退)、D-GRAIN(字段的 time_granularity已实现FieldIr.time_granularity,更细粒度的请求 → grain_too_fine)、 D-DATASET(指标的 dataset已实现COUNT(*) 的归属)。 基础模式的 ignored_vendor_extension 告警现在覆盖全部五种载体对象 (此前只有 Relationship 和 Metric)。
  • 1.1(2026-08-04,未提升版本号):信封版本闸门本身 —— v 现在会被读取并强制执行(§2),引入了 requires, 每个键都带上了声明的忽略影响,整份登记表通过 dosi infoGET /v1/capabilitiesdosi_engine.DATUS_EXT 对外发布。 没有新增或改变任何键,所以版本保持在 1.1。不带 v 的载荷 —— 也就是 Datus Agent 今天输出的那种 —— 不受影响。
  • 1.2(2026-08-06):窗口指标 —— D-WINDOW(指标的 window已实现MetricIr.window → 规划器的 WindowStage 投影根; pop/rolling/cumulative 语法糖 + 通用的 offset/frame 形态, 带扫描扩展 + 窗口后裁剪的累计重置;忽略影响为 documented —— 指标退回到它朴素的基础聚合;规范见 window-extension.md)。 把 13 个 baisheng 的派生时间用例从 Deferred 提升为 Supported(55/2)。 最初只在 DuckDB 上执行;全方言启用(按方言的偏移移位下降、 全方言快照、真机语料验证、datus_window 语料场景)作为后续落地 —— 各方言状态见 ../design/window-metrics-v2.md §1。 族内新增(同一小版本,在早于它们的引擎上会 fail-closed): 放宽了混合族/混合移位/混合的带起始重置, 以及有限窗框上的 require_full_window(2026-08-06)—— 见 window-extension.md#restrictions
  • 1.3(2026-08-09):窗口族与函数 —— 在族内扩展 window 键, 没有新的登记表行(capabilities().history 仍然停在 1.2; 这个版本号标示的是载荷方言)。新增内容:rank 族 (在指标值之上的 row_number / rank / dense_rank / ntile / percent_rank / cume_dist)、value 族(first_value / last_value / nth_value)、 偏移的 direction: "forward"(LEAD —— 下一期参照)、窗框的 order / partition / units / second 修饰符 (按值排序的窗框和 RANGE 窗框、分区模式、双参数输入), 以及登记表层级 W2stddev_pop/sampvar_pop/samp
  • W3covar_pop/sampcorr)。每一项新增在 1.2 引擎上都会 fail-closed: 未知的顶层族键会落进"恰好一个族"的错误,未知的族内键会撞上 deny_unknown_fields,未知的函数名会在枚举解析时失败 —— 而且声明 "v": "1.3" 本身还会触发版本闸门。规范见 window-extension.md(各族章节 + 函数登记表)。
  • 1.4(2026-08-12):派生指标与度量命名 —— D-DERIVE M1 (derive 键,已实现filter + compose 两族、带成环检测的 resolve_metric_refs 阶段、表达式等价性闸门、编译期一致维度集, 以及在每个宿主面上暴露的 derive 元数据;reagg / 跨数据集的 filter 谓词 / window.base 属保留形态,会以 not_implemented 拒绝并点名各自的里程碑 —— 注意 window.base 此前会落进"未知键静默忽略"那一档,现在改为 fail closed), 以及 D-MEASUREmeasure 键,已实现:显式度量命名, 按签名去重和冲突检查保持不变 —— 这是对非 ASCII stem 冲突的修复, 也是第一个 silent 影响级别的键:请把它列进 requires)。规范见 design/rfc-derived-metrics.mddesign/rfc-minimal-declarations.md

尚未实现(路线图)

排队中的这些 RFC 全部是增量的,所以每一个都以一次小版本提升落地, 带上自己的登记表行。顺序可能变化;每一条都独占一个小版本。

小版本 RFC 载体 影响 需要 requires
1.5 rfc-semi-additive-metrics §4 semi_additive Metric silent —— 期末余额会被跨快照加总
1.6 rfc-minimal-declarations §3 D-DISTINCT-STATE distinct_state Field / Dataset silent —— 一个物化的 bitmap 列会被当作普通列聚合
≥1.5 rfc-derived-metrics M2-M4 window.basederive 的 reagg 与跨数据集 filter Metric degraded/错误 —— 保留形态今天就以 not_implemented 拒绝,绝不静默计算
window-extension.md 的 rank/share/streak 族、W2+ 函数 将来的 window 形态(族内新增,在旧引擎上 fail-closed) Metric degraded

第一个 silent 键在 1.4 发布(measure,D-MEASURE)。如果你在一个会输出 silent 影响级别键的 Datus Agent 上编写模型,请留意它产生的 requires 列表: 正是它阻止了更旧的引擎悄悄返回错误数字,而这之所以行得通, 是因为 requires 本身在 1.1 就已发布 —— 早于第一个需要它的键。