跳转到内容

语义层

指标不只是一个字段或查询结果。它是一份纳入版本管理的协议,规定智能体可以分析的业务数值: 它表示什么、排除什么、如何计算,以及可以用在什么场景。Marivo 可以检查声明和匹配的预览证据 是否已在技术上就绪;只有对该指标负责的业务负责人才能批准这份契约是否适用于预期决策。

你可以直接使用团队已经维护的语义层,也可以从新项目开始建立。

项目中的 models/ 已经包含语义对象时,先复用这些定义。安装依赖、配置本地环境变量, 再让智能体检查现有对象是否适用于当前问题。没有缺口时,直接开始分析;不要重新生成已有 对象。克隆后的模型发生变化,或者流程要求技术认证时,再运行 限定范围的就绪检查。

向智能体说明数据源、业务目标和已经确定的规则。智能体使用 marivo-semantic 检查证据并 起草对象;用户根据业务需要确认和调整口径。技术验证通过不等于业务定义已经获批。

marivo-semantic 只提供稳定的步骤顺序和职责边界。当前构造器的文件位置、签名、 前置条件、加载后的对象获取方式与错误恢复,由 marivo.help(...)、结果卡片、 .contract() 和 structured 错误提供。读取用户数据前,智能体必须先确认 accountable 领域负责人、目标业务概念等数据本身无法回答的必要输入;sampling 不能回答这些问题。

marivo doctor 会确认当前解释器、已安装包版本、包路径和项目状态,然后交接到 Python。 环境没有激活时,应使用该环境自己的 marivo 可执行文件。 marivo.help(...) 是唯一公开的专项-帮助协调入口;限定后的 datasource.*semantic.*analysis.* 内容仍由各自原生 registry 拥有。mdms 只执行所属 领域能力,不提供独立的 .help() 别名。

数据源编写错误会显示稳定的代码和 stage。有界获取在执行阶段 失败时,只有调用者剩余的数据访问预算允许,才能对完全相同的获取最多重试一次; 调用者声明的读取次数、行数与超时上限优先。如果相同的 structured 代码和后端名称 再次出现,应停止并报告后端阻塞项,不能继续采样或绕过 Marivo。

语义对象是 models/ 下的 Python 源文件,应与项目代码一起纳入版本管理:

  • 团队成员通过克隆项目复用同一组定义;
  • 修改口径时使用分支和拉取请求,展示定义、负责人、ai_contextguardrails 和受影响指标;
  • 审查不只检查 Python 是否可运行,还要检查业务含义是否得到负责人确认;
  • 数据源只提交 *_env 引用,不提交凭据值;
  • 不提交 .marivo/ 运行状态、本地缓存或生成的密钥;
  • 语义变更合并后,让智能体检查配置和受影响对象的限定范围的就绪检查。

Git 保存可协作审查的语义声明,不会替代业务审批,也不会证明当前数据已经就绪。

智能体可以检查数据源证据、起草语义对象、完成技术验证,并说明仍未解决的问题。用户需要 判断的是:这个指标是否真正代表了要分析的业务结果。

让智能体创建或修改指标时,说明对业务有影响的规则即可,例如重要的纳入或排除条件、采用 哪个业务事件时间、单位是什么,以及指标有哪些使用限制。你不需要把这些规则转换成 Marivo 字段或 Python 代码。

例如:

为这个项目增加已完成订单收入指标。排除已取消和全额退款的订单,使用订单完成时间; 如果项目中的信息不足以确定某项业务规则,再向我确认。

智能体会通过 marivo-semantic 处理证据检查和编写流程。你只需要从业务角度检查它提出的 定义:衡量的结果是否正确,重要边界是否已经说明。含义不对时先纠正,再批准使用。通过技术 验证或就绪检查,并不代表业务定义已经获得批准。

智能体什么时候应该停下来确认

Section titled “智能体什么时候应该停下来确认”

现有证据无法确定业务规则,或者两种合理定义会明显改变结果时,智能体应该向用户确认。 声明、预览和就绪检查等技术步骤,不需要用户逐项安排。

Datasource → Domain → Entity → dimensions / time dimension / measures → metric

Python 声明就是契约。装饰器函数体返回 Ibis 表达式,而不是原始 SQL 字符串。 需要用 SQL 与既有查询做一致性检查时,SQL 只能作为 provenance=ms.from_sql(sql=..., dialect=...) 元数据存在。

这个简化表示包含一个 orders 实体、一个 region 维度、一个 order_date 时间维度、一个 amount 度量,以及一个 revenue 聚合指标。 以下简洁声明用于说明对象关系;生产环境中的对象还需要包含下文所述、经过用户批准的 ai_context

import marivo.datasource as md
import marivo.semantic as ms
sales = ms.domain(name="sales", owner="Mina Zhang")
warehouse = ms.ref.datasource("warehouse")
orders = ms.entity(name="orders", datasource=warehouse, source=md.table("orders"), primary_key=["order_id"])
region = ms.dimension_column(name="region", entity=orders, column="region")
order_date = ms.time_dimension_column(name="order_date", entity=orders, column="order_date", granularity="day", is_default=True)
amount = ms.measure_column(name="amount", entity=orders, column="amount", additivity="additive", unit="CNY")
revenue = ms.aggregate(name="revenue", measure=amount, agg="sum")

用户批准并加载模型后,日常分析先读取指标,再调用 observe

revenue_entry = catalog.metrics.get("sales.revenue")
revenue_entry.details().show()
frame = session.observe(
revenue_entry,
time_scope=mv.time_scope(start="2026-01-01", end="2026-04-01"),
grain=mv.grain("month"),
)

如果指标刚刚新建或修改,先完成下文所述的限定范围的编写收尾,再进入这条日常分析流程。

当业务周期不能安全地由 Gregorian 粒度推导时(例如财务周期或 4-4-5 日历),使用 PeriodCalendar。通过 ms.period_calendar(...) 声明民用日期主轴和命名层级,然后用 persist_values=True 一次获取数据,并把不可变的 DiscoverySnapshot 传给 catalog.preview(calendar_ref, using=snapshot)。认证要求覆盖完整范围,并发布绑定快照的 权威;之后目录导航不会再次读取数据源。

calendar_ref = ms.period_calendar(
name="fiscal",
date=ms.ref.time_dimension("sales.calendar.calendar_date"),
boundary_timezone="UTC",
coverage=(date(2026, 1, 1), date(2027, 1, 1)),
levels={"week": ms.ref.dimension("sales.calendar.fiscal_week")},
)
snapshot = md.inspect(ms.ref.datasource("warehouse"), md.table("calendar")).sample(
scope=md.unpruned(max_rows=400, timeout_seconds=30),
columns=("calendar_date", "fiscal_week"),
persist_values=True,
)
catalog.verify(calendar_ref)
catalog.preview(calendar_ref, using=snapshot)
calendar = catalog.period_calendars.get("sales.fiscal")
grain = calendar.grain("week")
week_scope = calendar.period("week", "FY2026-W01")
assert week_scope.kind == "calendar_period"
week_scope.contract().show() # 精确边界和认证快照身份

保留的 day 层级由认证覆盖范围推导,不会持久化成每天一条记录。 calendar.periods(level, limit=..., cursor=...) 返回有界的精确范围;覆盖缺失、定义过期或 证据损坏都会以结构化语义错误失败即停止。 精确查找语法由 marivo.help("analysis.calendar.period") 提供有界说明。

对于节日、促销、发布、事故等可能有间隙或重叠的命名窗口,使用独立的 TemporalSet。通过 ms.temporal_set(...) 声明显式的开始和 exclusive end, 再用一次 exhaustive、persist_values=TrueDiscoverySnapshot 完成本地认证:

campaigns_ref = ms.temporal_set(
name="campaigns",
occurrence_id=ms.ref.dimension("sales.campaigns.campaign_id"),
start=ms.ref.time_dimension("sales.campaigns.starts_at"),
end=ms.ref.time_dimension("sales.campaigns.ends_at"),
boundary_timezone="Asia/Shanghai",
coverage=(date(2026, 1, 1), date(2027, 1, 1)),
category=ms.ref.dimension("sales.campaigns.category"),
)
snapshot = md.inspect(ms.ref.datasource("warehouse"), md.table("campaigns")).sample(
scope=md.unpruned(max_rows=400, timeout_seconds=30),
columns=("campaign_id", "starts_at", "ends_at", "category"),
persist_values=True,
)
catalog.preview(campaigns_ref, using=snapshot)
campaigns = catalog.temporal_sets.get("sales.campaigns")
launch = campaigns.occurrence("spring-2026")
frame = session.observe(revenue, time_scope=launch, grain=mv.grain("day"))

mv.grain("day") 始终使用会话的报告时区作为边界。发生项使用其他 边界时区时,应将会话配置为相同的 report_timezone(或使用已认证的 calendar day 粒度);Marivo 不会静默地用发生项时区重解释裸 built-in day 粒度。

occurrence(key) 返回绑定精确快照的 TimeScope;分页有界,区间筛选使用半开区间重叠规则, 导航不会再次读取数据源。TemporalSet 不推断 recurrence,也不会按名称猜测跨年的发生项 是否对应。两个精确发生项范围选出的 day-粒度 frame 可以使用 mv.occurrence_progress(anchor="start" | "end", unmatched="fail" | "drop") 按本地日进度比较。

当业务数据源已经给出每个 civil 日期的最终工作状态时,使用独立的 WorkSchedule。它不依赖节日或促销窗口;认证要求有限覆盖率内每个日期恰好一条 boolean 记录,运行时不会推断 recurrence 或重新执行节假日规则。

schedule_ref = ms.work_schedule(
name="cn_sales_schedule",
date=ms.ref.time_dimension("sales.calendar.date"),
is_working=ms.ref.dimension("sales.calendar.is_working"),
boundary_timezone="Asia/Shanghai",
coverage=(date(2026, 1, 1), date(2027, 1, 1)),
)
schedule_snapshot = md.inspect(
ms.ref.datasource("warehouse"), md.table("calendar")
).sample(
scope=md.unpruned(max_rows=400, timeout_seconds=30),
columns=("date", "is_working"),
persist_values=True,
)
catalog.preview(schedule_ref, using=schedule_snapshot)
schedule = catalog.work_schedules.get("sales.cn_sales_schedule")

将目录条目传给 mv.working_day_progress(schedule=schedule, unmatched="fail" | "drop"),即可在两个 day-粒度时间-series 或面板 frame 之间按从选定范围起算的零基工作日 ordinal 配对。 精确 schedule 快照会写入比较契约;缺日、重复日、sub-day 或覆盖率 之外的日期都会失败即停止。

数据源在 models/datasources/*.py 中声明,每种后端对应一个类型化辅助函数。 辅助函数会注册连接。语义文件通过精确的 ms.ref.datasource("warehouse") factory 引用 数据源。

models/datasources/warehouse.py
import marivo.datasource as md
import marivo.semantic as ms
import marivo.analysis as mv
md.duckdb(
name="warehouse",
path="warehouse.duckdb",
ai_context=ms.ai_context(
business_definition="Local DuckDB warehouse for sales analysis.",
guardrails=["Use only for development or approved local analysis."],
),
)

每个辅助函数(md.duckdbmd.sqlitemd.mysqlmd.postgresmd.trinomd.clickhouse)都共享以下参数:

参数类型必填默认含义
namestr全局 lowercase snake_case 数据源名称。供 ms.ref.datasource(name) 使用。
ai_contextAiContextValueNone面向智能体的上下文,通过 ms.ai_context(...) 构造。
extradictNone类型化辅助函数未建模的、罕见的 JSON 安全 ibis 关键字参数。

各后端特有的参数:

辅助函数必填可选
md.duckdbpath(默认 ":memory:")、read_only(默认 False)、带范围的 HTTP 认证字段
md.sqlitepath(默认 ":memory:")、read_only(默认 False)、type_map
md.mysqlhostdatabaseport(3306)、autocommituser_envpassword_env
md.postgreshostdatabaseport(5432)、schemaautocommituser_envpassword_env
md.trinohostcatalogport(8080)、schemasourcetimezonehttp_schemeclient_tagssession_propertiesuser_envauth_env
md.clickhousehostport(9000 / 9440 secure)、databasesecuresettingsuser_envpassword_env

ClickHouse 数据源默认关闭 clickhouse-connect 自动生成的会话 id,这样分析 缓存后端时,重复查询或并发查询不会共享同一个 ClickHouse server 会话 lock。 如果数据源需要临时表等 ClickHouse 会话状态,可以显式传 extra={"autogenerate_session_id": True}settings={...} 用于 server/查询 settings;Ibis/clickhouse-connect client keyword arguments 放在 extra={...}。 Marivo 的有界只读路径使用 ClickHouse server setting readonly=1,保留其他已声明 settings,且绝不会通过可写连接重试。

SQLite 数据源通过 md.table(...) 使用表和视图。read_only=True 会启用 SQLite 连接级查询-仅模式;当类型亲和推断不足时,可用 type_map={...} 把声明的 SQLite 类型名映射为 Ibis 类型字符串。Parquet、CSV 和 JSON descriptor 仍只属于 DuckDB。 当前 Ibis SQLite 后端不支持 median、分位数指标或字符串 strptime 表达式;Marivo 会将其报告为结构化的不支持-操作错误。此时应改用受支持的聚合与原生时间 列。

models/datasources/lake.py
import marivo.datasource as md
md.trino(
name="lake",
host="trino.example.internal",
catalog="hive",
user_env="TRINO_USER",
auth_env="TRINO_AUTH",
)

受保护的 DuckDB HTTP JSON 来源,其认证应定义在数据源,而不是 md.json(...)。项目只保存环境变量名,以及允许携带凭据的最窄 URL 前缀:

md.duckdb(
name="hawkeye",
http_scope="http://hawkeye.example/report/api/",
http_bearer_token_env="HAWKEYE_TOKEN",
)

自定义请求头使用“请求头名称 → 密钥环境变量”的映射,可以声明单个 请求头,也可以声明机器鉴权所需的一组请求头:

md.duckdb(
name="change_focus",
http_scope="http://change-focus.example/api/v2/change/list",
http_headers_env={
"x-secretid": "CHANGE_FOCUS_SECRET_ID",
"x-signature": "CHANGE_FOCUS_SIGNATURE",
},
)

它与 bearer 模式互斥。Marivo 在建立连接时解析每个环境变量,并创建仅对 http_scope 生效的临时 DuckDB HTTP 密钥;POST 执行会复用连接内存中的 这些限定范围的请求头。

ms.domain(...) 打开一个命名空间。每个 _domain.py 调用一次。它返回一个 Ref[domain],也可以作为 domain= 传入,用来覆盖同级文件中对象的活动领域。

参数类型必填默认含义
namestr领域命名空间,例如 "sales"。对象成为 <name>.<object>
ownerstr负责人姓名,负责该领域的语义正确性与质量,例如 "Mina Zhang"
defaultboolTrueTrue 时,本文件中的 decorators 在未传 domain= 时解析到该领域。
ai_contextAiContextValueNone面向智能体的上下文,通过 ms.ai_context(...) 构造。
import marivo.semantic as ms
ms.domain(name="sales", owner="Mina Zhang")

实体表示一个物理源(一张表或一个文件)及其主键。维度、度量和指标 都依附在实体上。

参数类型必填默认含义
namestr实体名称。成为 <domain>.<name>
datasourceRef[datasource]ms.ref.datasource("warehouse")
source来源构建器数据源-owned md.table(...)md.parquet(...)md.csv(...)md.json(...)
primary_keylist[str]None构成主键的稳定输出列别名。
versioningms.snapshot | ms.validityNone快照或 SCD2 有效性版本控制(见下)。
domainRef[domain]文件默认覆盖活动领域。
ai_contextAiContextValueNone面向智能体的上下文,通过 ms.ai_context(...) 构造。
warehouse = ms.ref.datasource("warehouse")
orders = ms.entity(
name="orders",
datasource=warehouse,
source=md.table("orders"),
primary_key=["order_id"],
ai_context=ms.ai_context(business_definition="One row per order."),
)
构建器必填可选用于
md.table(name)namedatabasecolumns数据源中的目录表或类型化投影(Trino/MySQL 用 database="schema")。
md.parquet(path)pathhive_partitioning通过 DuckDB 读取的 self-describing Parquet 文件来源。
md.csv(path, schema=...)path、类型化 schemaheaderdelimiter通过 DuckDB 读取的 CSV 文件来源。
md.json(path, schema=...)path、类型化 schemaformatrecords_pathquery_paramsmethodbodyJSON 文件、glob、GET URL 或参数化请求体的 POST URL;records_path 用于选择包装响应中的记录数组。

md.table(...) 支持两种封闭模式。省略 columns= 时使用目录发现;传入 columns={alias: md.source_column(...)} 时声明类型化投影,并为实体提供稳定输出别名:

events = ms.entity(
name="events",
datasource=warehouse,
source=md.table(
"raw.events",
columns={
"event_time": md.source_column("event.timestamp", data_type="timestamp"),
"score": md.source_column("generated.score", data_type="float64"),
},
),
primary_key=["event_time"],
)

声明的类型是断言,不是类型转换;该映射也是完整的投影允许列表。对于投影来源,语义层的 primary_key 以及直接字段的 column= 必须引用这些稳定别名。若目录元数据无法确认绑定, 检查结果会给出仅声明警告。此时应选择显式有界样本,获取一次快照,执行限定范围的预览, 再运行零查询就绪检查。静态加载和校验只证明声明内部一致;预览才是运行时权威。该来源记录为 TABLE_PROJECTION,各别名仍是同一物理表内的绑定,不会被伪装为多个物理来源。

维度是用于分组或过滤的分类属性。直接实体输出列使用 ms.dimension_column(...);需要表达式 (例如 table.region.upper())时,使用 @ms.dimension 装饰器,其请求体返回一个针对 实体表的 ibis 表达式。

参数类型必填默认含义
namestr维度名称。成为 <domain>.<entity>.<name>
entityRef[entity]所属实体。
columnstr实体表上的稳定输出列别名。
domainRef[domain]文件默认覆盖活动领域。
ai_contextAiContextValueNone面向智能体的上下文,通过 ms.ai_context(...) 构造。
region = ms.dimension_column(
name="region",
entity=orders,
column="region",
ai_context=ms.ai_context(business_definition="Sales reporting region."),
)

时间维度是携带粒度和解析元数据的特殊维度。只有时间维度才能作为 session.observe 的时间轴。

参数类型必填默认含义
namestr维度名称。
entityRef[entity]所属实体。
columnstr实体表上的稳定输出列别名。
granularity粒度字面量yearquartermonthweekdayhourminutesecond —— 查询有意义的最细粒度。
parse解析变体None源列如何变成时间值(见下)。原生时间列可省略——分析时自动推断解析变体。
is_defaultboolFalse当实体有多个时间轴时,标记默认时间轴。省略 time_dimension=observe 会使用它。
domainRef[domain]文件默认覆盖活动领域。
ai_contextAiContextValueNone面向智能体的上下文,通过 ms.ai_context(...) 构造。

parse= 值声明列的物理编码。省略时,分析会根据列的 ibis dtype 自动推断解析变体(原生 datedatetimetimestamp 列无需显式指定解析)。对于字符串或整数列,需提供 ms.strptime(format)ms.hour_prefix(prefix)。该变体必须与 granularity 兼容(例如 hour 粒度需要带时间的格式)。

构建器源列是…关键参数
(省略 parse原生时间列
ms.datetime()原生 datetimetimezone(IANA)、sample_interval
ms.timestamp()原生 timestamptimezone(IANA)、sample_interval
ms.strptime(format)需要解析的字符串/整数timezonesample_interval
ms.hour_prefix(prefix)仅含小时的分区sample_interval —— prefix 是提供日期的 day 粒度 Ref[time_dimension]

对于原生但 naive 的 datetimetimestamp,必须声明其来源时区(例如 ms.datetime(timezone="Asia/Shanghai"))。显式 ms.datetime()ms.timestamp() 未提供 timezone= 时,就绪检查会返回 undeclared_naive_time_axis 阻塞项:运行时会回退到数据源 read 时区,而 分析窗口使用报告时区,可能在日或小时边界错位。零查询门禁会报告这些 运行时解析的上下文,但绝不猜测业务时区。形如 (5, "minute")sample_interval 标记一个被周期采样的时间轴,供半可加折叠使用。

对于时间-series 和面板 frame,bucket_start 始终是报告时区下的桶 标签,并以时区-naive 时间戳或日期输出。例如 report_timezone="Asia/Shanghai" 时,上海业务日的第一个小时桶显示为 2026-06-20 00:00:00,而不是等价的 UTC instant。

# Day partition stored as the string "20260131"
log_date = ms.time_dimension_column(
name="log_date",
entity=orders,
column="dt",
granularity="day",
parse=ms.strptime("%Y%m%d"),
is_default=True,
)
# Native UTC timestamp, usable for sub-day buckets
event_ts = ms.time_dimension_column(
name="event_ts",
entity=orders,
column="event_ts",
granularity="minute",
parse=ms.timestamp(timezone="UTC"),
)

度量是你打算聚合的行级数量事实(例如金额或数量)。直接实体输出列使用 ms.measure_column(...);需要基于一个或多个列的表达式时使用 @ms.measure。度量 携带可加性和可选的单位。

参数类型必填默认含义
namestr度量名称。成为 <domain>.<entity>.<name>
entityRef[entity]所属实体。
columnstr实体表上的稳定输出列别名。
additivity可加性值"additive""non_additive"ms.semi_additive(...)
unitstrNoneUCUM 单位令牌:"USD""CNY""%""ms""{order}"
domainRef[domain]文件默认覆盖活动领域。
ai_contextAiContextValueNone面向智能体的上下文,通过 ms.ai_context(...) 构造。
amount = ms.measure_column(
name="amount",
entity=orders,
column="amount",
additivity="additive",
unit="CNY",
)

指标不是一次查询的结果,而是智能体可以安全 observe 的、可用于分析的业务数值。 它的定义和计算声明在版本控制中;实体、维度、时间行为、单位和可加性说明它如何 被分析;ai_context 说明业务含义和约束;就绪检查用于在新建或修改后认证该定义, 再声明变更已经完成。

需要使用示例
需要按行级数值汇总时ms.aggregateamount 得到 revenue
需要计数时ms.count已完成订单数
需要按行级度量计算加权均值时ms.weighted_mean按请求数加权的延迟
需要由已有指标计算时ms.ratioms.linear转化率
需要累计时ms.cumulative年初至今收入
现有构建器无法表达时@ms.metric一个未被上述形式覆盖的计算

按需要选择对应的构建器。@ms.metric 仅用于现有构建器无法表达的计算。

来自度量的简单指标 —— ms.aggregate

Section titled “来自度量的简单指标 —— ms.aggregate”

聚合一个度量。无需请求体;可加性从度量继承。

参数类型必填默认含义
namestr指标名称。
measureRef[measure]要聚合的度量。
agg聚合方式"sum""mean""min""max" 等。
fold折叠None半可加度量的时间-折叠覆盖。
filterWhereFilterNonems.where(dimension=value, ...) 构建的 AND 过滤;标量表示等值,非空元组/list 表示成员过滤。
unitstr继承覆盖从度量推导的单位。
domain / ai_context同其他对象。
revenue = ms.aggregate(name="revenue", measure=amount, agg="sum")
us_revenue = ms.aggregate(name="us_revenue", measure=amount, agg="sum", filter=ms.where(region="US"))

需要统计实体行数时使用 ms.count,不要为了计行数额外声明冗余度量。这个辅助函数 只接受引用,并会从实体引用推断指标所属领域。传入 filter=ms.where(...) 可对子集 计数(如失败数),无需手写指标请求体。

order_count = ms.count(name="order_count", entity=orders, ai_context=ms.ai_context(business_definition="订单总数。"))
failed_count = ms.count(name="failed_count", entity=orders, filter=ms.where(state="FAILED"))

ms.where(**conditions) 构建 AND 组合的等值或成员过滤。键必须是指标目标实体 上已声明的本地语义维度名,而不是任意物理列。标量 str / int / float / bool 表示等值;非空元组/list 会统一为成员过滤。 加载会拒绝缺失的 filter 维度,预览/分析在提交查询前根据已解析的 Ibis 类型检查 literal 是否可比较。 若合法已编写 literal 无法与物理 dtype 比较,两条路径都会在查询提交前返回 filter_value_runtime_incompatible,并携带 query_executed=Falsedeclaration_preserved=True。Marivo 保留原业务 literal,由用户或业务负责人确认 必要的代码/label 映射;不会根据物理类型或样本值改写规则。运行时证据 不可用期间,仍可继续不依赖该证据的静态验证与 semantic_static 就绪检查。

terminal_count = ms.count(
name="terminal_count",
entity=queries,
filter=ms.where(type=(2, 4)),
)

ms.weighted_mean 接受同一实体上的两个度量,由 Marivo 自动完成逐行乘权和聚合:

avg_latency = ms.weighted_mean(
name="avg_latency",
value=latency,
weight=request_count,
)

它只对值与权重都非 null 的行计算 SUM(value * weight) / NULLIF(SUM(weight), 0)。权重必须是可加度量; 结果继承值度量的单位,并持久化精确的 numerator / weight 组件, 供 weighted_mix 归因使用。它也接受 filterunitdomainai_context

派生指标由其他指标组合而成,不需要请求体;计算完全来自组成成分。

构建器必填计算
ms.ratio(name, numerator, denominator)两个引用numerator / denominator(如客单价、各类比率)
ms.linear(name, add, subtract)add(共 ≥2 项)add 之和减去 subtract(如 net = gross - refunds

它们也都接受 unitdomainai_context

net_revenue = ms.linear(name="net_revenue", add=[gross_revenue], subtract=[refunds])
aov = ms.ratio(name="aov", numerator=total_amount, denominator=orders_count)

观察(observe)时,除法按固定的 zero_division="null" 策略求值:某个桶的分母 (ms.ratio)或成对权重和(ms.weighted_mean)存在但为零时,结果为 null——绝不会出现 +/-inf——受影响的行数记录在观察结果的 meta.zero_denominator_rowsquality_summary.zero_denominator_rows 中。

当业务问题是”截至桶 t 累计了多少”时使用 ms.cumulative(...)。 基础必须是使用 sumcountcount_distinct 或加权组件累计的 tier-1 ms.aggregate(...)ms.count(...)ms.weighted_mean(...) 指标。 anchor 参数选择累计形态。

参数类型必填默认含义
namestr语义指标名称。
baseRef[metric]使用 sumcountcount_distinct 的 tier-1 简单聚合指标。
overRef[time_dimension] | NoneNone累计时间轴。除非基础根实体只有一个时间维度,否则需显式传入。
anchorGrainToDate | Trailing | NoneNone累计锚点。None = 全部历史累计;ms.grain_to_date(...) = MTD/QTD/YTD 重置;ms.trailing(...) = 滚动 N。
unit / domain / ai_context同其他指标。
user_id = ms.measure_column(name="user_id", entity=events, column="user_id", additivity="non_additive")
active_users = ms.aggregate(name="active_users", measure=user_id, agg="count_distinct")
cumulative_active_users = ms.cumulative(
name="cumulative_active_users",
base=active_users,
over=event_time,
)

对于 count_distinct 基础指标,累计值使用首次出现语义:每个去重实体在最早出现的桶 中计入,因此累计值单调非递减。累计指标可作为比率组成成分——用 ms.ratio(...) 组合累计 分子和分母可得到累计比率。

anchor=None(默认)是全部历史累计:observe 窗口裁剪展示行,但不会重置累计值。两个值对象 构造器开启额外的锚点类型。

ms.grain_to_date(grain=...) 在每个重置粒度边界重置累计值(MTD / QTD / YTD / WTD)。在一个 重置周期内值持续累计;到达边界时回落到该周期首个桶的流量。内置 grain 取值为 weekmonthquarteryear;认证的自定义日历则使用 ms.calendar_grain(...) 返回的 类型化 TemporalGrain

mtd_revenue = ms.cumulative(
name="mtd_revenue",
base=revenue,
over=event_time,
anchor=ms.grain_to_date(grain=mv.grain("month")),
)

对于 fiscal 或其他已认证日历,将日历权威来源保存在粒度值中,不要在分析参数里重复传入 可互相冲突的层级:

fiscal_mtd = ms.cumulative(
name="fiscal_mtd",
base=revenue,
over=event_time,
anchor=ms.grain_to_date(
grain=ms.calendar_grain(
calendar=ms.ref.period_calendar("sales.fiscal"),
level="fiscal_month",
)
),
)
fiscal_week = session.catalog.period_calendars.get("sales.fiscal").grain("fiscal_week")
frame = session.observe(
fiscal_mtd,
time_scope=mv.time_scope(start="2026-01-01", end="2026-03-01"),
grain=fiscal_week,
)

日历快照负责 fiscal membership 及其边界时区;改变会话报告时区只会改变 展示,不会改变周期归属。

ms.trailing(count=..., unit=...) 是固定大小的滚动窗口:每个桶的值是该桶结束边界 为止的 span 上的基础聚合。空窗口为真零,不会向前 carry。部分窗口(span 触及数据起点之前) 展示实际的部分累计值,并在覆盖率中标记为 partialunit 仅接受固定大小单位(secondminutehourdayweek);日历可变单位(monthquarteryear)会被拒绝并给出 教学错误。 尾随 day 始终等于 86,400 秒,尾随 week 始终等于 604,800 秒;它们不是报告 时区下的民用日历周期,因此 DST 切换不会改变滚动窗口长度。

rolling7_active = ms.cumulative(
name="rolling7_active",
base=active_users,
over=event_time,
anchor=ms.trailing(count=7, unit="day"),
)

两条跨锚点规则约束查询时的粒度选择:

  • 粒度兼容规则grain_to_date):每个展示桶必须完整落入一个重置周期内。在 month / quarter / year 重置下使用 week 查询粒度是非法的(week 桶会跨越月边界); dayhour 合法。
  • 整数倍规则trailing):窗口 span 必须是查询粒度的整数倍(W_buckets = span / grain)。 尾随要求时间粒度。

派生指标可以组合累计组件。只有所有外层组件都是累计,且共享 完全相同的锚点时才支持比较,其中包括 all_history。它的结果是携带精确 evaluation cutoff 的观测层级差,不声明等价于区间流程,且来源 revision 未验证。混合锚点以及累计与非累计 组件混合都会被拒绝。完成 compare 后,attribute 可接受当前累计差值:业务维度解释 端点层级变化;对于直接 sum/count 结构,精确的累计 over 轴解释基础流程。时间与 业务轴混合、累计 count_distinct 以及组件时间 bridge 会失败即停止。decomposeforecast 在累计 frame 上仍不支持。

import marivo.analysis as mv
import marivo.semantic as ms
会话 = mv.会话.get_or_create(
名称="revenue-investigation",
问题="Why did Q4 revenue drop?",
)
目录 = 会话.目录
revenue_entry = 目录.指标.get("sales.revenue")
region_entry = 目录.维度.get("sales.orders.region")
revenue_entry.详情().show()
当前 = 会话.observe(
revenue_entry,
粒度=mv.粒度("month"),
维度=[region_entry],
)

details().show() 展示人类编写的定义、约束、组合、有效实体、候选 普通/时间维度与度量血缘。候选轴来自指标的有效实体;跨实体关系 与扩散仍由 session.observe(...) 验证。只有在读取验证/预览/就绪检查等机械下一步时 才调用 .contract().show().show() 打印的内容与 .render() 返回的有界卡片相同。 如果对象缺失或定义存在争议,先回到语义编写;否则该观测会成为会话 中有证据支撑的调查的第一个产物。新建或修改的对象把限定范围的就绪检查作为 编写收尾的一部分。

声明完成后,加载目录并检查它:

import marivo.semantic as ms
catalog = ms.load() # SemanticCatalog
catalog.show() # 全部 typed collections 与数量
catalog.domains.show() # top-level domains
catalog.metrics.show() # just metrics across all domains
sales = catalog.domains.get("sales")
orders = sales.entities.get("orders")
revenue = orders.metrics.get("revenue") # one metric object
region = orders.dimensions.get("region") # one dimension object

每个类型化集合都接受当前范围内的本地名称、精确 full 路径或同类型引用。 限定范围的集合不会通过路径或引用返回范围外的对象。当身份来自配置、 持久化状态或日志时,使用严格且只接受引用的 catalog.require(ref)。条目卡片会显示精确 类型、full 路径、.ref 和有界的当前候选轴;被省略的成员会指向 details().show()

全局集合为 domainsdatasourcesentitiesdimensionstime_dimensionsmeasuresmetricsrelationshipseventsstate_modelsrepr(catalog) 会指向 catalog.show() 打印的同一个有界、 零查询目录卡片。集合帮助会完整展示 .items.refs.get(...).render().show();lookup 仍是精确查找,不提供 fuzzy search。

新建或修改对象后,运行就绪检查,认证所选定义及其限定范围的运行时证据仍然有效:

catalog.verify(revenue).show()
catalog.preview(revenue, using=snapshot).show()
report = catalog.readiness(refs=[revenue, region])
if report.status == "blocked":
report.show() # blockers, with the next step for each

这些运行时调用接受当前条目或它的精确引用,并立即归一化为引用。就绪检查输出、 预览证据、持久化、重放与 recovery 仍然只保存引用。

另外两个检查支持编写:

  • ms.richness() —— 建议性的覆盖度/深度报告;永不阻塞。
  • ms.parity_check("sales.revenue") —— 用指标的 provenance SQL 运行并比对结果。 需要 provenance=ms.from_sql(...)

就绪检查如何判断“是否就绪”,见 就绪检查。 分析如何记录结论,见 证据链。 关于会话如何加载并验证指标输入,继续阅读 分析流程;关于变更认证,见上面的限定范围的就绪检查。

日常编写会用到两个命名空间:

import marivo.datasource as md # connections 与 physical sources
import marivo.semantic as ms # meaning (ms.entity, ms.metric, ...)

每个对象都归属于一个 领域,并通过限定引用引用:

  • 领域级对象:<domain>.<object> —— 例如 sales.revenuesales.orders
  • 实体级对象(维度和度量):<domain>.<entity>.<field> —— 例如 sales.orders.region

一个项目由一组声明文件组成。数据源在 models/datasources/ 下声明一次; 语义放在 models/semantic/<domain>/ 下,每个领域有一个 _domain.py

your-project/
marivo.toml
models/
datasources/
warehouse.py # md.duckdb(name="warehouse", ...)
semantic/
sales/
_domain.py # ms.domain(name="sales", owner="Mina Zhang") + entities, metrics, ...

在多仓库语义层场景中,每个业务域仓库仍保持相同的已编写 models/ 布局, 中央分析项目只需要引用这些根:

[semantic]
layer_paths = ["../finance-domain/models"]

Marivo 会先加载中央项目的 models/,再按配置顺序加载外部 models/ 根。 所有数据源、领域与语义对象会进入同一个目录,因此名称必须全局唯一。 marivo doctorcatalog.previewcatalog.verify 与分析会话都会按同一组 配置根解析数据源-backed 语义对象。

每个语义对象都由一个 sealed、不可变的 ms.Ref 值标识。运行时只有一个 具体 Ref 类;泛型类型(如 Ref[metric]Ref[dimension])负责静态角色检查。 只能通过 ms.ref.metric("sales.revenue") 这类精确 factory,或返回同一值类型的 编写声明创建引用。

所有引用都有以下只读属性:

属性类型含义
.pathstr类型内的限定路径,例如 "sales.revenue"
.kindSemanticKind对象种类——以下八个值之一。
.keystr规范运行时键,例如 "metric:sales.revenue"
.namestr最后的本地名称分段。

str(ref) 返回 .key,相等性和哈希基于 (kind, path)。不能直接调用 Ref(...),也不能继承 Ref。公开边界不会把字符串或目录条目当作引用。

类型Factory常见编写 producer表达式绑定
domainms.ref.domain(path)ms.domain(...)
datasourcems.ref.datasource(path)数据源声明的 .ref
entityms.ref.entity(path)ms.entity(...)
dimensionms.ref.dimension(path)ms.dimension_column(...)@ms.dimension在绑定的表达式请求体内使用 ms.bind(ref, entity_alias)
time_dimensionms.ref.time_dimension(path)ms.time_dimension_column(...)@ms.time_dimension在绑定的表达式请求体内使用 ms.bind(ref, entity_alias)
measurems.ref.measure(path)ms.measure_column(...)@ms.measure在绑定的表达式请求体内使用 ms.bind(ref, entity_alias)
metricms.ref.metric(path)ms.aggregate(...)@ms.metricms.ratio(...)
relationshipms.ref.relationship(path)ms.relationship(...)

引用值本身始终不可调用。在表达式请求体内,必须通过 ms.bind(amount, orders) 将字段引用显式绑定到已声明的实体 alias。Loader 会把该绑定 记录到编译后的目录。若在请求体外绑定、使用错误 alias,或绑定非字段引用,都会得到 带修复提示的错误。

编写引用可以直接传给分析。如果从字面身份开始,先做一次精确目录 membership 校验,再直接传入当前条目:

revenue = ms.aggregate(name="revenue", measure=amount, agg="sum")
frame = session.observe(
revenue,
time_scope=mv.time_scope(start="2026-01-01", end="2026-04-01"),
)
revenue_entry = catalog.require(ms.ref.metric("sales.revenue"))
frame = session.observe(
revenue_entry,
time_scope=mv.time_scope(start="2026-01-01", end="2026-04-01"),
)

catalog.require(ref) 是精确的全局查找;目录集合负责浏览和本地名称查找, 也支持完整路径查找。已冻结的分析 consumer 接受当前目录条目或其精确引用; 编写、持久化和配置边界仍以引用为准。持久化负载使用带版本的 {schema, kind, path} 记录,裸字符串不是公开 interchange 格式。

ai_context:人与智能体之间的契约

Section titled “ai_context:人与智能体之间的契约”

每个语义对象和数据源都接受可选的 ai_context 参数,并通过 ms.ai_context(...) 构造。业务含义和约束放在这里;智能体使用对象前会读取这部分上下文。所有参数都是可选的, 但未知的关键字参数会在调用时被 Python 拒绝。

字段类型必填默认含义
business_definitionstrNone用一两句话说明对象的业务含义。
guardrailslist[str][]智能体必须遵守的规则:必要的过滤、排除项、范围限制。
ai_context=ms.ai_context(
business_definition="Gross order amount before refunds.",
guardrails=["Validate refund exclusions before using as net revenue."],
)

对于行会随时间变化的实体,需要声明如何读取当前状态:

  • ms.snapshot(partition_field, grain="day", timezone=None, format=None) —— 按天分区的快照;partition_field 必须是 Ref[dimension]Ref[time_dimension]
  • ms.validity(valid_from, valid_to, interval, open_end, timezone=None) —— SCD2 有效性区间;valid_fromvalid_to 必须是 Ref[dimension]Ref[time_dimension]interval"closed_open"[from, to))或 "closed_closed"open_end 列出表示“仍然有效”的哨兵值(例如 SQL NULL(None,),或 ("9999-12-31",))。

当指标无法用 ms.aggregate(...)ms.count(...) 或派生指标构建器表达时, 才使用这个 escape hatch。请求体返回一个 ibis 聚合;additivity 由你显式声明。

参数类型必填默认含义
namestr函数名指标名称。
entitieslist[Ref[entity]]请求体读取的实体。
additivity可加性值"additive""non_additive"ms.semi_additive(...)
root_entityRef[entity]单个实体entities 多于一个时必填。
fanout_policy"block" | "aggregate_then_join""block"如何处理跨实体连接的 fan-out。
unitstrNoneUCUM 单位令牌。
provenanceSqlProvenanceNone用于一致性校验的 ms.from_sql(sql=..., dialect=...)
domain / ai_context同其他对象。
@ms.metric(
entities=[orders],
additivity="additive",
name="revenue",
provenance=ms.from_sql(
sql="SELECT SUM(amount) AS revenue FROM orders",
dialect="duckdb",
),
ai_context=ms.ai_context(business_definition="Gross order amount before refunds."),
)
def revenue(table):
return table.amount.sum()

可加性与时间行为决定智能体更改分析粒度或增加切分时,数值是否仍有意义。SQL 来源信息 不会执行指标;它为 ms.parity_check(...) 提供参考查询,以检查已声明的语义是否与既有实现一致。

  • ms.semi_additive(over, fold) —— 用于在大多数轴上可加、但需要在某个时间轴上折叠的 快照/状态事实。over 必须是 @ms.time_dimension(...) 返回的状态时间 维度引用;fold"last""first""mean""max"("percentile", 0.95)
  • ms.from_sql(sql, dialect) —— 把 SQL 作为仅来源信息 附加,用于启用 ms.parity_check(...)。它永远不会作为指标请求体执行。
snapshot_date = ms.time_dimension_column(
name="snapshot_date",
entity=inventory_daily,
column="snapshot_date",
granularity="day",
)
on_hand_units = ms.measure_column(
name="on_hand_units",
entity=inventory_daily,
column="on_hand_units",
additivity=ms.semi_additive(over=snapshot_date, fold="last"),
)

关系声明两个实体如何连接,使指标和维度能够跨越这两个实体。 键使用维度引用,而不是原始列名。

参数类型必填默认含义
namestr关系名称。
from_entityRef[entity]源实体。
to_entityRef[entity]目标实体。
keyslist[JoinKey]一个或多个 ms.join_on(from_key, to_key) 配对。
domain / ai_context同其他对象。
ms.relationship(
name="orders_to_customers",
from_entity=orders,
to_entity=customers,
keys=[ms.join_on(order_customer_id, customer_id)],
)

先 inspect,只样本一次,再 author

Section titled “先 inspect,只样本一次,再 author”

对数据源-backed 语义对象,使用一个显式证据快照。查阅 marivo.help("datasource.authoring") 了解数据源检查与范围,查阅 marivo.help("semantic.authoring") 了解语义依赖阶梯,查阅 marivo.help("semantic.ai_context") 了解共享的 ms.ai_context(...) 契约:

warehouse = ms.ref.datasource("warehouse")
orders = md.table("orders")
inspection = md.inspect(warehouse, orders)
inspection.show()
inspection.partitions().show()
scope = md.partition({"dt": "20260710"}, max_rows=1000, timeout_seconds=30)
snapshot = inspection.sample(
scope=scope,
columns=("order_id", "region", "created_at", "amount"),
)
snapshot.entity(columns=("order_id",)).show()
snapshot.dimensions(columns=("region",)).show()
snapshot.time_dimensions(columns=("created_at",)).show()
snapshot.measures(columns=("amount",)).show()

日期或时间戳窗口继续复用同一个有界范围类型:

scope = md.time_range(
"created_at",
start="2026-07-10T00:00:00+00:00",
end="2026-07-11T00:00:00+00:00",
max_rows=1000,
timeout_seconds=30,
)

谓词采用半开区间 [start, end)。两个边界必须同为日期或同为时间戳,并保持 时区感知方式一致;带时区的时间戳会统一规范化为 UTC。projected source 必须暴露该 时间列。与等值分区范围不同, time_range 可以限定经过转换的时间分区。

md.duckdb(...) 声明数据源。md.table(...) 选择该数据源内部的 表或 view。md.parquet(...)md.csv(...) 是 DuckDB 文件来源 descriptor;md.json(...) 是 DuckDB-backed JSON 文件或 HTTP API 来源 descriptor。它们都需要配合数据源引用使用,并且都不是数据源 声明。

events = md.json("data/events/*.json", schema={"event_id": "string"}, format="newline_delimited")
api_events = md.json(
"https://api.example.com/events",
schema={"event_id": "string", "occurred_at": "timestamp"},
records_path="$.result.items",
query_params={
"query": "sum(pending_containers) by (cluster)",
"start": md.source_param("start"),
"end": md.source_param("end"),
"step": "60s",
},
)
gpu_servers = md.json(
"https://root.example/api/v1/graphql",
schema={
"name": "string",
"bs": "string",
"gpuAbstract": "string",
"status": "string",
},
method="POST",
body={"query": "{ queryServers { name bs gpuAbstract status } }"},
records_path="$.data.queryServers",
query_params={"policy-domain": "gpus"},
)
orders_parquet = md.parquet("data/orders/*.parquet")
orders_csv = md.csv("data/orders/*.csv", schema={"order_id": "string"})
md.inspect(warehouse, events).show()

records_path 刻意只支持一小部分 JSONPath:以 $ 开头,后接对象成员, 例如 $.data$.result.items。它不支持过滤器、通配符、递归下降或数组下标。 路径存在且值为空数组时会物化为 0 行。对于选出的每条记录,声明的字段会按 schema 顺序投影:缺少字段会变成带类型的 NULL,额外字段会被忽略;存在的字段 必须能够转换为声明的类型。路径缺失或值不是数组时则在执行阶段失败,避免把认证 错误或响应包装变化误判成空数据。

最小 POST API 场景使用 method="POST",并通过 body= 提供一个 JSON 对象。md.source_param(...) 可以出现在对象或数组中的任意完整 JSON 值 位置。请求只会在结果表执行时发出。POST 要求 HTTP(S) URL 和 format="auto";自动分页、重试与增量采集不在这一契约内。数据源中 声明的 bearer 或自定义请求头只会在 http_scope 范围内发送,凭据不会进入 来源 descriptor。

changes = md.json(
"http://change-focus.example/api/v2/change/list",
schema={"change_id": "int64", "title": "string"},
method="POST",
body={
"platform_id": 1,
"source_type": 2,
"specific_source": [md.source_param("app_id")],
"env_id": [1],
"page_num": md.source_param("page_num"),
"page_size": 100,
},
records_path="$.data.change_infos",
)

query_params 把固定请求参数留在来源 descriptor 中;md.source_param(name) 声明一个必填、非密钥的运行时值,并且只能占据完整的查询参数值。 Marivo 负责 URL 编码,不支持字符串片段模板。分析时通过执行范围绑定, 而不是给 observe 增加 API 专用参数:

with session.source_bindings({
ms.ref.entity("monitoring.api_events"): {
"start": "now-3600",
"end": "now",
},
}):
frame = session.observe(ms.ref.metric("monitoring.pending_containers"))

绑定可以嵌套,并通过执行上下文隔离;它还会绑定到所属会话运行时, 同一任务里的另一个会话无法消费这些值。缺少或多余参数会在读取来源前失败; 这些非密钥值会进入快照与分析身份。数据源编写证据 使用相同映射:inspection.sample(..., source_params={...})

检查是元数据-仅,并在 sampling 前报告结构、物理 extent、分区 状态和 enforceable 能力。范围必须有正整数行/timeout guards 和明确列; LIMIT 不保证 bytes scanned。快照投影不查询数据源,值默认只在内存。 对于 ClickHouse Distributed 来源,本地 system.parts 观察只会作为 scope=local_node_only 说明展示;源表级行与 bytes 保持 unknown,Marivo 不会默认发起 cluster-wide 扩散查询。

ClickHouse 检查还会在 projectable_columns 中列出安全的 adapter-only 物理列。该表有界展示,每一项都给出可复制的 md.source_column(...) 声明, 保留精确物理名与规范化类型。active parts 间类型冲突或无法解析的后端类型只会 产生 warning,并且不会列为可投影列。这并不枚举动态 MapV2 key:未物化 key 必须在上游物化、通过数据库 view 暴露,或仅在终端 md.raw_sql(...) 中使用。 只有接受有界值证据明文写入项目本地 cache 时才使用 persist_values=True。 非常见格式、键、时区、聚合、单位、可加性、关系 cardinality 和业务含义仍由智能体负责。

证据卡片会从有界 row/null count 推导 null rate。维度或 values 样本出现 NULL 时,现有证据判断会要求在 ai_context.guardrails 中说明 null_semantics,例如它表示“不适用于该事件分支”还是“值未知”。Marivo 不会把 高 null rate 自动判为数据质量故障,也不会隐式过滤 NULL

检查卡片会显示精确来源 descriptor,以及真实结构的列名和类型。分区 卡片会显示已捕获值的来源、完整性、截断状态、有界值和无需新查询即可使用的范围 模板。快照卡片会显示范围、selected 列、覆盖率与值/cache 状态; 每个投影都明确标记为 data_access=none

连接测试以 md.test(ref).show() 为停止点。失败的 DatasourceTestResult 会公开结构化 .failure、专项 .repair,以及 .contract() 中被阻塞的 validate_connection 转换。快照的连接、来源解析、timeout 与 执行失败同样保持结构化,并明确查询是否已经执行;后端 message 会被有界化和 脱敏。

当证据无法机械地决定业务口径时, result.contract().judgment_requirements 会提供冻结的判断要求,包括稳定 ID、主体、 证据 IDs 和 authority="user_or_business_owner"。它不会推荐值、创建构造器 转换或记录审批。所有本地投影复用同一个快照;只有缺少必需列或值 证据、快照已过期,或数据源/来源/范围身份不匹配时才重新采样。

专项构造器帮助会把调用明确标为其 loader 路径内的声明 fragment。 加载后使用生成的目录交接:catalog = ms.load()entry = catalog.<collection>.get(...)entry.show()entry.contract().show()

写完一个 Python 声明后,重新加载当前目录条目,依次运行 catalog.verify(entry)catalog.preview(entry, using=snapshot) 和零查询 catalog.readiness(refs=[entry])。只有持久化、日志或配置需要精确身份时才使用 entry.ref

当就绪检查报告多个运行时预览缺失时,仍通过同一个入口修复: catalog.preview_many(report.preview_required_refs, using=snapshot_or_mapping)。 它会合并兼容查询,但仍为每个引用保留独立证据;这不是批量编写捷径。

大多数项目只需要前面的指标建模路径。只有问题依赖有序业务事件、漏斗、耗时或状态回放时, 再增加事件与 StateModel。

事件为一次业务发生声明可复用的身份、occurred 时间和具名参与者角色。 来源实体由 owner(occurred_at) 推导,因此事件定义不重复接收 entity 参数。 所有事件都使用唯一入口 @ms.event(...)

如果一个实体的每一行都代表一次订单创建,那么该事件不需要额外行过滤,但仍要显式 返回 ms.all_rows()

order_id = ms.dimension_column(
name="order_id",
entity=orders,
column="order_id",
)
@ms.event(
name="order_created",
identity=(order_id,),
occurred_at=order_date,
participants=(
ms.participant(name="order", cardinality="one"),
),
ai_context=ms.ai_context(
business_definition="An order creation occurrence."
),
)
def order_created(order_rows):
return ms.all_rows()

共享事件日志实体上的过滤事件则返回受限布尔表达式,例如 ms.bind(event_type, event_rows) == "payment_succeeded"。不要使用无函数体 构造器、裸 True 或另一套 filtered-事件 API。

ms.participant_role(event=order_created, name="order") 会解析出事件分析 使用的类型化角色。省略 path 表示事件来源实体自己就是参与者; 非空路径则沿着已经声明的关系到达另一个实体。作为分析主体 的角色必须使用 cardinality="one"。其 endpoint 实体已声明的主键 就是主体身份,调用方不再重复提供。

StateModel 为一个主体实体声明允许出现的状态及事件驱动的转换。它只描述 业务规范含义。重放窗口、seed、完整性假设、队列和已观测违规 属于分析层,不进入 StateModel。

例如,假设订单源还提供支付时间与订单状态:

paid_at = ms.time_dimension_column(
name="paid_at",
entity=orders,
column="paid_at",
granularity="second",
)
order_status = ms.dimension_column(
name="order_status",
entity=orders,
column="status",
)
@ms.event(
name="order_paid",
identity=(order_id,),
occurred_at=paid_at,
participants=(
ms.participant(name="order", cardinality="one"),
),
ai_context=ms.ai_context(
business_definition="An order entered the paid state."
),
)
def order_paid(order_rows):
return ms.bind(order_status, order_rows) == "paid"
created = ms.lifecycle_state(name="created", initial=True)
paid = ms.lifecycle_state(name="paid", terminal=True)
order_lifecycle = ms.state_model(
name="order_lifecycle",
subject=orders,
states=(created, paid),
transitions=(
ms.inception(on=order_created),
ms.transition(
from_state=created,
on=order_paid,
to_state=paid,
),
),
ai_context=ms.ai_context(
business_definition="Normative commercial order lifecycle."
),
)

状态名是 StateModel 内部的 immutable 编写值。必须恰好有一个 initial 状态;terminal 状态不得再有 outgoing 转换。只有当某个事件恰好有一个 cardinality="one" 参与者到达 StateModel 主体时,才会自动推导触发 角色。如果多个角色都符合条件,应传入精确的 ms.participant_role(...) handle, 而不是分别重复事件和角色字段。

加载后,通过 ms.model_state(model=order_lifecycle, name="paid") 构造分析层接收的 类型化状态身份。为将来的投影 seed 而定义的 StateModel 可以没有 起始,但这种模型不能从起始开始重放。当前构造器契约以 marivo.help("semantic.state_model") 为准;当前对象可执行的 verify/preview/readiness 后续路径以目录条目为准。