@guandata/guanmetric 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +11 -0
- package/package.json +6 -6
- package/skills/guanmetric/SKILL.md +49 -1
- package/skills/guanmetric/references/metric-api-contracts.md +7 -0
- package/skills/guanmetric/references/metric-bi84-compatibility.md +66 -0
- package/skills/guanmetric/references/metric-bi84-test71-validation.md +54 -0
- package/skills/guanmetric/references/metric-dsl-dependencies-example.json +30 -0
- package/skills/guanmetric/references/metric-dsl-dependencies.schema.json +66 -0
- package/skills/guanmetric/references/metric-dsl-document-output-example.json +18 -0
- package/skills/guanmetric/references/metric-dsl-document-output.schema.json +34 -0
- package/skills/guanmetric/references/metric-dsl-generate-1.1.schema.json +57 -0
- package/skills/guanmetric/references/metric-dsl-generate-bundle-example.json +35 -0
- package/skills/guanmetric/references/metric-dsl-generate-example.json +19 -0
- package/skills/guanmetric/references/metric-dsl-generate.schema.json +42 -0
- package/skills/guanmetric/references/metric-dsl-selection-example.json +6 -0
- package/skills/guanmetric/references/metric-dsl-selection.schema.json +14 -0
- package/skills/guanmetric/references/metric-dsl.md +298 -0
- package/skills/guanmetric/references/metric-flow-export-example.json +30 -0
- package/skills/guanmetric/references/metric-flow-export.schema.json +34 -0
- package/skills/guanmetric/references/metric-flow-handoff-example.json +29 -0
- package/skills/guanmetric/references/metric-flow-handoff.schema.json +48 -0
- package/skills/guanmetric/references/metric-flow.md +15 -1
- package/skills/guanmetric/references/metric-public-dimension-api-contract.schema.json +44 -0
- package/skills/guanmetric/references/metric-public-dimension.md +2 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://guandata.dev/schemas/guanmetric/metric-dsl-generate-1.0.json",
|
|
4
|
+
"title": "guanmetric dsl generate output 1.0",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "command", "generated", "metricCount", "files", "diagnostics"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": { "const": "1.0" },
|
|
10
|
+
"command": { "const": "dsl.generate" },
|
|
11
|
+
"generated": { "type": "boolean" },
|
|
12
|
+
"projectDir": { "type": "string", "minLength": 1 },
|
|
13
|
+
"metricCount": { "type": "integer", "minimum": 0 },
|
|
14
|
+
"files": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"items": { "type": "string", "minLength": 1 },
|
|
17
|
+
"uniqueItems": true
|
|
18
|
+
},
|
|
19
|
+
"diagnostics": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"items": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"required": ["code", "severity", "message"],
|
|
25
|
+
"properties": {
|
|
26
|
+
"code": { "type": "string", "minLength": 1 },
|
|
27
|
+
"severity": { "enum": ["error", "warning"] },
|
|
28
|
+
"sheet": { "type": "string" },
|
|
29
|
+
"row": { "type": "integer", "minimum": 1 },
|
|
30
|
+
"field": { "type": "string" },
|
|
31
|
+
"message": { "type": "string" }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"allOf": [
|
|
37
|
+
{
|
|
38
|
+
"if": { "properties": { "generated": { "const": true } }, "required": ["generated"] },
|
|
39
|
+
"then": { "required": ["projectDir"] }
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://guandata.dev/schemas/guanmetric/metric-dsl-selection-1.0.json",
|
|
4
|
+
"title": "guanmetric DSL selective plan/apply selection v1.0",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["operation", "requested", "resolved", "autoIncluded"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"operation": {"enum": ["create", "edit"]},
|
|
10
|
+
"requested": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
|
|
11
|
+
"resolved": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
|
|
12
|
+
"autoIncluded": {"type": "array", "items": {"type": "string", "minLength": 1}}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# 指标 DSL(Phase 1)
|
|
2
|
+
|
|
3
|
+
`guanmetric dsl preview` 把受限 JavaScript 指标源码编译为 Metric IR 1.0。它是纯本地命令,不读取认证、不访问 BI、不写 Flow state,也不创建或编辑指标。
|
|
4
|
+
|
|
5
|
+
## 工程结构
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
metric-project/
|
|
9
|
+
|- metric.config.js
|
|
10
|
+
|- catalog.context.js
|
|
11
|
+
|- catalog.datasets.js
|
|
12
|
+
|- catalog.metrics.js
|
|
13
|
+
|- catalog.biz-attrs.js
|
|
14
|
+
`- atomic_sales.js
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`metric.config.js`:
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
defineMetricProject({
|
|
21
|
+
dslVersion: "1.0",
|
|
22
|
+
acceptance: Acceptance.USABLE,
|
|
23
|
+
project: projectRef("project_sales"),
|
|
24
|
+
directory: directoryRef("dir_sales"),
|
|
25
|
+
businessOwner: currentUser()
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`catalog.datasets.js`:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
registerDatasets({
|
|
33
|
+
orders: dataset("ds_orders", "订单", {
|
|
34
|
+
amount: { id: "fd_amount", name: "订单金额", type: "NUMBER" },
|
|
35
|
+
date: { id: "fd_date", name: "订单日期", type: "DATE" },
|
|
36
|
+
region: { id: "fd_region", name: "区域", type: "STRING" },
|
|
37
|
+
city: { id: "fd_city", name: "城市", type: "STRING" }
|
|
38
|
+
})
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`atomic_sales.js`:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
const sales = atomicMetric("sales", "销售额")
|
|
46
|
+
.fromDataset(DS.orders)
|
|
47
|
+
.aggregate(field(DS.orders, "订单金额"), Agg.SUM)
|
|
48
|
+
.dimensions(field(DS.orders, "订单日期"), field(DS.orders, "区域"))
|
|
49
|
+
.timeDimension(field(DS.orders, "订单日期"))
|
|
50
|
+
.format({ formatType: "CURRENCY", decimalPlaces: 2, prefix: "¥" })
|
|
51
|
+
.filter(condition(field(DS.orders, "区域"), Filter.IN, ["华东"]))
|
|
52
|
+
.businessAttributes(
|
|
53
|
+
bizAttr(ATTR["业务域"], "销售"),
|
|
54
|
+
bizAttr(ATTR["适用渠道"], ["线上", "门店"])
|
|
55
|
+
)
|
|
56
|
+
.englishName("sales_amount")
|
|
57
|
+
.description("订单金额合计");
|
|
58
|
+
|
|
59
|
+
registerMetric(sales);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
三类创建指标都支持 `.format({...})` 和 `.filter(...)`。`format` 接受非空、可 JSON 序列化的对象并原样进入 Metric IR 和创建请求;具体格式字段仍由 BI 服务端最终校验。筛选必须通过类型化 helper 构造:单条件使用 `condition(fieldRef, Filter.*, values)`,AND/OR 组合分别使用至少两个参数的 `allOf(...)` / `anyOf(...)`,不接受手写任意筛选对象;构造结果及嵌套值会冻结,后续赋值不能改变 IR。`Filter.NOT_IN` 会生成服务端规范值 `NI`,`Filter.IS_NOT_NULL` 会生成 `NOT_NULL`,空值筛选的 values 会规范为空数组。例如:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
.filter(allOf(
|
|
66
|
+
condition(field(DS.orders, "区域"), Filter.IN, ["华东", "华南"]),
|
|
67
|
+
anyOf(
|
|
68
|
+
condition(field(DS.orders, "城市"), Filter.CONTAINS, ["杭"]),
|
|
69
|
+
condition(field(DS.orders, "城市"), Filter.IS_NOT_NULL, [])
|
|
70
|
+
)
|
|
71
|
+
))
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
复合指标可调用 `.useCommonDimensions()` 让服务端按公式引用求共有维度,也可使用显式 `.dimensions(...)`;这两种维度模式互斥。共用维度模式仍可单独指定 `.timeDimension(...)`,以保留服务端原有的时间字段。显式维度模式与原子指标一样,时间维度必须同时包含在 dimensions 中。衍生指标的维度仍由基础指标和衍生定义决定,不开放 dimensions/timeDimension。
|
|
75
|
+
|
|
76
|
+
原子指标也可以使用字段表达式取数;`aggregate()` 与 `expression()` 严格二选一:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
registerMetric(
|
|
80
|
+
atomicMetric("tax_sales", "含税销售额")
|
|
81
|
+
.fromDataset(DS.orders)
|
|
82
|
+
.expression("SUM([fd_amount]) + SUM([fd_tax])")
|
|
83
|
+
.dimensions(field(DS.orders, "订单日期"))
|
|
84
|
+
.timeDimension(field(DS.orders, "订单日期"))
|
|
85
|
+
.description("订单金额与税额之和")
|
|
86
|
+
);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
表达式中的方括号必须写 `catalog.datasets.js` 中所选数据集的真实字段 ID,不能写字段中文名、别名或其他数据集字段。首版安全子集支持 `SUM`、`COUNT`、加减运算和 `CASE WHEN`;不接受乘除、其他函数、嵌套聚合和动态参数。DSL 会在本地按完整安全子集语法检查操作数、运算符、`CASE WHEN/THEN/ELSE/END`、聚合上下文、函数集合和字段归属,并把引用字段作为类型化 IR 输出;创建请求只生成 `formulaField.fdFormula`,不生成 `fdId/aggrType`。这些检查是 DSL 版本契约,服务端仍负责最终业务校验。
|
|
90
|
+
|
|
91
|
+
执行:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
guanmetric dsl init --project-id project_sales --dir-id dir_sales \
|
|
95
|
+
--dataset orders=ds_orders --metric existing_sales=metric_sales \
|
|
96
|
+
-d ./metric-project
|
|
97
|
+
guanmetric dsl generate --file ./standard.xlsx -d ./generated-metric-project
|
|
98
|
+
guanmetric dsl preview ./metric-project
|
|
99
|
+
guanmetric dsl document ./metric-project \
|
|
100
|
+
--out ./metric-project/.guanmetric/docs
|
|
101
|
+
guanmetric dsl plan ./metric-project --operation create
|
|
102
|
+
guanmetric dsl plan ./metric-project --operation create --key sales_ratio
|
|
103
|
+
guanmetric dsl apply ./metric-project --operation create \
|
|
104
|
+
--state ./metric-project/.guanmetric/metric-state.json
|
|
105
|
+
guanmetric dsl apply ./metric-project --operation create \
|
|
106
|
+
--state ./metric-project/.guanmetric/metric-state.json --confirm
|
|
107
|
+
guanmetric dsl checkout <metric_id_1> <metric_id_2> -d ./metric-project
|
|
108
|
+
guanmetric dsl diff ./metric-project
|
|
109
|
+
guanmetric dsl plan ./metric-project --operation edit
|
|
110
|
+
guanmetric dsl plan ./metric-project --operation edit --metric-id <metric_id>
|
|
111
|
+
guanmetric dsl apply ./metric-project --operation edit
|
|
112
|
+
guanmetric dsl apply ./metric-project --operation edit \
|
|
113
|
+
--state ./metric-project/.guanmetric/edit-state.json --confirm
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`preview` 成功时输出 `schemaVersion=1.0`、`command=dsl.preview`、`valid=true` 和类型化 `document`。`plan --operation create` 把 IR 转成现有 Flow 1.0 body,解析 `currentUser()`,并复用 Flow 的 filter、前端差集规则、原子 save-precheck、依赖和 planHash;存在 `.guanmetric/catalog-manifest.json` 时,plan/apply 还会先校验当前 profile/base URL/domain 及 project/directory 绑定。它可以访问 BI 做只读预检,但不会创建指标。失败时仍先输出 `diagnostics` 或嵌套 Flow plan,然后以非零状态退出;程序按稳定字段分支,不匹配 `message` 文案。
|
|
117
|
+
|
|
118
|
+
`dsl generate --file <standard.xlsx> -d <new_project_dir>` 是标准 Excel -> DSL 的单向生成入口。它要求输入来自当前版本 `template normalize`,复用 Flow generate 对数据集字段、远端指标、业务属性、批内引用、保存方式和服务端预检的解析,再生成 `metric.config.js`、四类 catalog 与三类指标脚本。目标目录必须不存在;单组输入直接生成一个 DSL 工程,多主题、目录或保存方式输入按 `(projectId,parentDirId,acceptance)` 稳定拆成 `project_NNN` 子工程,并在根目录生成 `guanmetric-dsl-bundle.json`。每个子工程仍保持单一默认值并可独立 preview/plan/apply;跨子工程的批内依赖无法由独立工程无损表达,因此只要引用名称属于当前 Excel 的其他子工程就稳定拒绝,不能回退为线上同名指标。生成先在同一父目录的临时目录完成并用当前 DSL 编译器自校验,再原子提交;不会创建或编辑 BI 指标。若某些分组存在真实语义错误,合法分组仍作为一个明确的 bundle 原子落盘,stdout 为 `generated=true,complete=false` 且命令非零退出,错误 diagnostics 通过 `projectKey` 指向被阻断分组;调用方只能继续清单中已生成的工程。单组失败或所有分组均失败时仍不留下目标目录。生成后 DSL 工程成为后续 review/plan/apply 的事实源;不支持对 Excel 与 DSL 双向编辑后自动合并。
|
|
119
|
+
|
|
120
|
+
生成器保持严格无损边界:每个子工程仍只使用一个主题、目录、保存方式和工程级 `currentUser()`。模板责任人先解析为真实用户 ID;显式名称/邮箱/ID 若等于当前登录用户可无损收敛为 `currentUser()`,只有不同用户的单指标覆盖才拒绝。动态参数、复合公式字段映射、当前 DSL 无法表达的公式函数、最近 N 的非 `[N-1,0]` 窗口、累计偏移、复合业务限定筛选等仍返回稳定诊断。原子/复合/组合衍生公式只接受 DSL 已支持的指标引用、数字、括号和四则运算;筛选转换为 `condition/allOf/anyOf`,格式和业务属性保留。复合时间维度名称在多个引用中不唯一时,应在标准模板填写真实字段 ID;为支持从现有定义复刻,生成器可在全主题范围完整分页查找已发布、受支持类型的同名同公式复合指标,且所有匹配定义给出同一个时间字段 ID 时回溯复用。模板留空但本批衍生指标需要该复合时间维度时,也仅在上述来源唯一一致的条件下继承;独立复合指标留空仍保持留空。若新旧复合公式引用的指标 ID 不同,不能仅凭名称推定等价,应在模板填写真实时间字段 ID。无法唯一确认时返回 `FLOW_GENERATE_TIME_DIM_UNRESOLVED`,绝不按引用顺序或同名字段猜测。单工程输出保持 `schemaVersion=1.0` 兼容,见 `metric-dsl-generate.schema.json` 和 `metric-dsl-generate-example.json`;多工程 bundle 使用 `schemaVersion=1.1`,见 `metric-dsl-generate-1.1.schema.json` 和 `metric-dsl-generate-bundle-example.json`。
|
|
121
|
+
|
|
122
|
+
同比环比可在 `comparative({...})` 中保留服务端 `fixedBase`(含 `period` 非空字符串数组与 `withMacro` 布尔值),或使用 `offsetType`/`offset`;`offsetType` 包含 `ENDOFMONTH`、`ENDOFQUARTER`、`ENDOFYEAR`、`YEARBYWEEK`。最近 N 和累计的 `baseline` 支持 `DATE_RANGE_PARAM_LEFT` 与 `DATE_RANGE_PARAM_RIGHT`。四类普通时间衍生可显式传 `dateFdId`,保留原定义中的粒度字段 ID;省略时仍按基础指标时间维度补齐。期末值原定义省略 `calculationMode` 时,生成的 DSL 使用服务端默认的 `NATURAL_LAST_DAY`。
|
|
123
|
+
|
|
124
|
+
`document` 是 Phase 2 的纯本地工程说明入口。它复用和 `preview` 相同的完整编译与校验,成功后在 `--out` 目录固定生成 `metric-dependencies.json` 和 `metric-documentation.md`。JSON 按节点 ID、边的 from/to/relation 稳定排序,包含 create/edit/dataset/remoteMetric 节点、datasetSource/localMetric/remoteMetric 边,以及与 Flow 相同、以稳定源码顺序打破同层并列的 `createOrder`;Markdown 从同一模型生成摘要、表格和 Mermaid 图。编辑节点只表达本次 attach 操作与其新数据集/远端指标依赖,不假装知道未修改的服务端定义。命令不访问 BI;任一固定输出文件存在时默认拒绝,明确追加 `--force` 才替换。机器契约和样例见 `metric-dsl-dependencies.schema.json`、`metric-dsl-dependencies-example.json`、`metric-dsl-document-output.schema.json` 和 `metric-dsl-document-output-example.json`。
|
|
125
|
+
|
|
126
|
+
大型工程可以选择性 plan/apply。`--operation create` 使用可重复的 `--key`:每个目标会自动带上全部传递本地上游,按 Flow 拓扑顺序执行,但不会带入依赖目标的下游或无关指标。`--operation edit` 使用可重复的 `--metric-id`,只读取、预检和原地更新选中的 checkout 指标;编辑项之间不自动扩展。`--key` 与 `--metric-id` 不能跨 operation 混用,未知目标返回 `METRIC_DSL_SELECTION_INVALID`。不传选择参数时输出和全量行为保持不变。
|
|
127
|
+
|
|
128
|
+
选择发生在完整 DSL 编译成功之后,因此未选脚本的语法、引用或类型错误仍会阻断,不允许用选择参数隐藏坏源码。选择成功时 plan/apply stdout 新增可选 `selection`,其中 `requested` 是去重后的用户输入,`resolved` 是规范执行顺序,`autoIncluded` 是 create 自动纳入的上游;edit 的 `autoIncluded` 固定为空。state、派生计划和 edit `sourceHash` 只绑定选中后的规范子文档,未选脚本后续变化不影响当前续跑;但续跑必须使用同一 state,并解析为同一 `resolved` 子集,省略选择或换成不同子集会因 source/plan 不一致而拒绝。不同 requested 若规范化为相同 resolved 子集,则视为同一执行目标。selection 的稳定契约与样例见 `metric-dsl-selection.schema.json` 和 `metric-dsl-selection-example.json`。
|
|
129
|
+
|
|
130
|
+
`apply --operation create` 默认输出 `dryRun=true` 的计划并以非零状态提醒尚未写入。只有 `--confirm` 与明确 `--state` 同时存在时才会创建指标;confirmed apply 在编译 DSL 或访问 BI 前取得与 Flow 共用的跨进程 state 锁,随后完全复用 Flow 的创建、逐项持久化、依赖阻断、发布/草稿回读、查询验收和续跑逻辑。创建请求未返回可信 ID 时保持 `blocked/reconcile`,再次 apply 不会重发。
|
|
131
|
+
|
|
132
|
+
confirmed create apply 使用同一 DSL 和 state 续跑时,会先校验环境、planHash 与冻结的派生 Flow 计划;已有 metric ID 或待 reconcile 的条目不会再作为新指标执行远端创建预检,避免它们与自身口径冲突。尚未提交创建的条目仍执行实时预检;源码、环境或冻结计划不一致时拒绝跳过。普通 `dsl plan` 和不带 `--confirm` 的 apply preview 始终按新的创建计划完整预检,不能据此替代 state 续跑。
|
|
133
|
+
|
|
134
|
+
confirmed apply 会把精确 Flow 1.0 计划保存为 `<state>.plan.json`,并记录到 state 的 `planFile`。已有计划与当前 DSL 不一致时拒绝覆盖,以保留 reconcile 证据。后续命令:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# 审批完成或查询暂时失败后重新验证
|
|
138
|
+
guanmetric flow verify --state ./metric-project/.guanmetric/metric-state.json
|
|
139
|
+
|
|
140
|
+
# 导出供 guanvis 等下游按 ID 继续回读的独立 handoff
|
|
141
|
+
guanmetric flow export \
|
|
142
|
+
--state ./metric-project/.guanmetric/metric-state.json \
|
|
143
|
+
--out ./metric-project/.guanmetric/created-metrics.json
|
|
144
|
+
|
|
145
|
+
# 创建结果不确定时,先只读核对候选,再使用 state.planFile 预览和确认绑定
|
|
146
|
+
guanmetric flow reconcile \
|
|
147
|
+
--file ./metric-project/.guanmetric/metric-state.json.plan.json \
|
|
148
|
+
--state ./metric-project/.guanmetric/metric-state.json \
|
|
149
|
+
--key sales --metric-id <metric_id>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
WASI/非原生运行时继续禁用 apply、verify、reconcile 和 export;preview/plan 可用,document 只依赖宿主工程文件读写能力,init/checkout/diff 是否可用取决于宿主是否提供对应的 BI 请求与工程文件能力。
|
|
153
|
+
|
|
154
|
+
## Init 与 catalog 刷新
|
|
155
|
+
|
|
156
|
+
`dsl init` 是只读 BI、本地写文件的初始化入口。它生成 `catalog.context.js`、`catalog.datasets.js`、`catalog.metrics.js`、`catalog.biz-attrs.js` 和 `.guanmetric/catalog-manifest.json`;工程不存在 `metric.config.js` 时同时生成默认的 `Acceptance.USABLE` 配置。数据集和远端指标分别通过可重复的 `--dataset <alias>=<datasetId>`、`--metric <alias>=<metricId>` 明确选择,两类至少提供一种。alias 必须是 JavaScript 标识符,例如生成 `DS.orders` 和 `METRIC.existing_sales`。字段对象以真实字段 ID 为键,日常仍推荐通过 `field(DS.orders, "订单金额")` 使用唯一字段名;重名时改用真实字段 ID。
|
|
157
|
+
|
|
158
|
+
远端指标 catalog 保存名称、类型、状态、`canUse`、时间维度和适用维度摘要,使用 `metricRef(METRIC.existing_sales)` 进入复合或衍生表达式;现有 `metricRef("metricId")` 继续兼容。catalog 状态仅是生成时快照,不做发布/可用性的本地硬拒绝;plan/apply 会重新读取服务端权威状态。Metric IR 始终只保存真实指标 ID,不把名称带入 apply 阶段解析。
|
|
159
|
+
|
|
160
|
+
业务属性 catalog 自动包含当前环境全部启用配置,以显示名称作为稳定键,例如 `ATTR["业务域"]`;启用配置重名时 init 直接失败,已停用配置不暴露。创建原子、复合和衍生指标都可调用 `.businessAttributes(bizAttr(...), ...)`。`TEXT/TEXT_AREA/ENUM_SINGLE` 接受字符串,`ENUM_MULTIPLE` 接受字符串数组;枚举值会对 init 快照做本地提前校验。Metric IR 和最终请求只保存 `{id,values}`;现有 plan 仍会读取当前配置执行前端差集校验,必填、默认值和最终合法性由服务端裁决。
|
|
161
|
+
|
|
162
|
+
重复执行同一命令会按资源 alias、字段 ID、业务属性名称稳定排序并逐文件原子刷新 context/datasets/metrics/biz-attrs 快照和 catalog manifest,不覆盖已有 `metric.config.js`、`atomic_*.js`、`composite_*.js`、`derived_*.js` 或 `edit_*.js`。catalog manifest 保存非敏感环境身份;当前环境与已有 catalog 或 checkout manifest 不一致时,在读取 BI 资源前拒绝刷新。生成的 context 会在 preview 时校验配置中的 project/directory,防止手工配置与快照交叉使用。
|
|
163
|
+
|
|
164
|
+
此纵切复用六个既有只读调用:`GET /api/metric-platform/projects/{id}`(`METRIC_PLATFORM.READ`)、`GET /api/directory/METRIC_PLATFORM/unauthorized-tree?projectId=...`(`BASE.READ`)、`GET /api/user/profile`(`BASE.READ`)、`GET /api/data-source/{id}`(`DATASET.READ`)、`GET /api/metric-platform/metrics/{id}`(`METRIC_PLATFORM.READ`)和 `GET /api/metric-attr-config`(`BASE.READ`)。最后一个路径经代理还原为 `GET /metric-attr-config`,路由和权限由 `MetricAttrConfigNgController.query` 确认;其余路径沿用 `metric-hierarchy-api-contracts.md`、`metric-flow.md`、`metric-api-contracts.md` 的现有证据。CLI 没有新增 BI method/path。账号密码已有真实读取证据,PAT 有权/无权/资源不存在对照尚未执行,状态仍为“未验证”,不能由本地测试替代。
|
|
165
|
+
|
|
166
|
+
## Checkout 与本地 diff
|
|
167
|
+
|
|
168
|
+
`dsl checkout` 是本地工程写入操作,但对 BI 只有读取:逐项优先读取 `GET /api/metric-platform/metrics/{id}?fromDraft=true`,只有结构化业务状态明确表示草稿不存在时才回退正式详情。它将详情读模型转换成三类指标既有 Update DTO 白名单,生成:
|
|
169
|
+
|
|
170
|
+
纯草稿详情可能不返回 `parentDirId/dirPath`。此时 checkout 会读取该指标所属主题,并使用主题的根目录 `dirId` 作为 DSL 工程目录上下文;编辑请求本身不会提交目录字段,也不会移动指标。主题 ID 缺失、主题回读不一致或根目录缺失时仍会拒绝生成工程,禁止猜测目录。
|
|
171
|
+
|
|
172
|
+
- `.guanmetric/manifest.json`:非敏感环境身份、metric ID、类型、详情来源、utime、base 路径、canonical fingerprint 和可编辑字段基线;
|
|
173
|
+
- `.guanmetric/base/metrics/*.json`:详情快照、canonical definition 和 fingerprint;
|
|
174
|
+
- `edit_*.js`:只调用对应 `attachAtomic/attachComposite/attachDerived` 与 `registerEdit` 的 no-op 脚本;
|
|
175
|
+
- `metric.config.js`:仅在工程中不存在时根据 checkout 指标生成;已有配置和已有编辑脚本都不会覆盖。
|
|
176
|
+
|
|
177
|
+
同一工程只允许同一 BI 环境、同一指标主题。manifest 和 base JSON 使用稳定 `schemaVersion=1.0`;fingerprint 为 canonical definition 的 SHA-256,不从展示 JSON 反推。
|
|
178
|
+
|
|
179
|
+
当前 `dsl diff` 是纯本地字段级基线检查:编译工程,确认每个 attach 目标都存在于 manifest、类型一致、manifest 中每个指标都有脚本,并核对 manifest、base snapshot、canonical editableFields 与 fingerprint 四者一致;`baseFile` 不能逃逸工程目录。checkout 生成的 no-op 工程输出 `valid=true, changed=false`,所有 `changedFields` 和 `operations` 都为空;增加 setter 后输出 base、target、稳定 operation 和排序后的 changedFields。
|
|
180
|
+
|
|
181
|
+
三类 attach 当前共享以下字段级 setter:
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
registerEdit(
|
|
185
|
+
attachAtomic("metric_xxx", "销售额")
|
|
186
|
+
.rename("净销售额")
|
|
187
|
+
.englishName("net_sales")
|
|
188
|
+
.description("扣除退款后的销售额")
|
|
189
|
+
.format({ type: "NUMBER", digits: 2 })
|
|
190
|
+
.dimensions(field(DS.orders, "城市"))
|
|
191
|
+
.timeDimension(field(DS.orders, "日期"))
|
|
192
|
+
.filter(condition(field(DS.orders, "城市"), Filter.IN, ["杭州"]))
|
|
193
|
+
.businessAttributes([])
|
|
194
|
+
.publish(true)
|
|
195
|
+
);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
清空筛选用 `clearFilter()`,清空时间维度用 `clearTimeDimension()`。编辑业务属性使用 `.businessAttributes([bizAttr(ATTR["属性名"], value), ...])`,传 `[]` 表示清空;非空项不接受手写 `{id,values}`,同一属性 ID 不能重复。衍生指标不支持 dimensions/timeDimension;同一脚本重复设置同一目标字段会返回 `METRIC_DSL_EDIT_OPERATION_CONFLICT`。不接受任意 JSON path。
|
|
199
|
+
|
|
200
|
+
类型专属 setter 使用与创建 DSL 相同的领域表达,但编辑已有指标时引用必须是明确远端 ID:
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
// 原子:两种取数模式互斥;切换时自动移除另一模式字段
|
|
204
|
+
attachAtomic("metric_sales", "销售额")
|
|
205
|
+
.fieldSource(DS.orders, field(DS.orders, "订单金额"), Agg.SUM);
|
|
206
|
+
attachAtomic("metric_tax_sales", "含税销售额")
|
|
207
|
+
.expressionSource(DS.orders, "SUM([fd_amount]) + SUM([fd_tax])");
|
|
208
|
+
|
|
209
|
+
// 复合:公式只允许 metricRef;共有维度开关可显式设 true/false
|
|
210
|
+
attachComposite("metric_aov", "客单价")
|
|
211
|
+
.formula(div(metricRef("metric_sales"), metricRef("metric_orders")))
|
|
212
|
+
.useCommonDimensions(true);
|
|
213
|
+
|
|
214
|
+
// 普通衍生沿用 from + 六类模式;组合衍生不设置 from
|
|
215
|
+
attachDerived("metric_sales_mom", "销售额月环比")
|
|
216
|
+
.from(metricRef("metric_sales"))
|
|
217
|
+
.comparative({
|
|
218
|
+
granularity: Time.MONTH,
|
|
219
|
+
window: currentPeriod(),
|
|
220
|
+
offsetType: Time.MONTH,
|
|
221
|
+
offset: 1,
|
|
222
|
+
valueType: Compare.RATE,
|
|
223
|
+
growthRateType: Growth.NORMAL,
|
|
224
|
+
replenishDimData: true
|
|
225
|
+
});
|
|
226
|
+
attachDerived("metric_sales_index", "销售指数")
|
|
227
|
+
.composite(add(metricRef("metric_recent_sales"), metricRef("metric_mtd_sales")));
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
普通衍生同时支持 `recent/cumulative/last/businessCondition`,参数与创建 DSL 一致。`comparative.replenishDimData` 是可选布尔值,对应标准模板的“维度值补齐”;省略时不向服务端请求体写入该字段。`comparative/last` 的 `dateFdId` 不由 DSL 手填:本地 diff 暂用 checkout 基线中的日期字段,远端 edit plan 会读取目标基础指标并冻结其当前时间维度。LAST plan 还会读取基础指标的 `isLastSupported`(旧响应缺失时至少识别表达式 `formulaField`);不支持期末值的基础指标会在计划阶段被阻断,服务端仍执行最终校验。`businessCondition` 同时设置 `deriveSetting` 和顶层 `filter`。类型专属操作是不可拆分的多字段语义操作,所以例如 `fieldSource` 与同一 attach 上的第二个取数 setter、`businessCondition` 与 `filter/clearFilter` 会被视为字段冲突。从已有 `businessCondition` 切换到其它衍生模式时,未显式设置筛选会自动清空旧业务限定 filter;同一 attach 同时使用 `filter/clearFilter` 时,以显式筛选操作为准,结果不受 setter 调用顺序影响。
|
|
231
|
+
|
|
232
|
+
`dsl plan --operation edit` 会重新读取当前权威详情并生成 `MetricMutationPlan 1.0`:
|
|
233
|
+
|
|
234
|
+
- 未被 DSL 修改的字段发生远端变化:保留当前远端值;
|
|
235
|
+
- 目标字段相对 checkout 基线变化且不同于 DSL 目标:`METRIC_DSL_REMOTE_DRIFT` 阻断该条目;
|
|
236
|
+
- 目标字段远端已经等于 DSL 目标:该字段 no-op;
|
|
237
|
+
- 环境、subType 或 projectId 变化:`METRIC_DSL_IMMUTABLE_DRIFT` 阻断;
|
|
238
|
+
- 计划继续执行 Update DTO 白名单、身份字段、原子取数完整性和 filter 本地校验,并冻结 current fingerprint、target body 与 planHash。
|
|
239
|
+
- 多指标计划中的远端读取、字段漂移或单项 DTO 校验失败记录为该指标的 `blocked` item 和 warning diagnostic,不把其他独立指标的 plan 判为无效;DSL、manifest、环境和 attach 覆盖等结构错误仍会阻断整批。
|
|
240
|
+
|
|
241
|
+
edit plan 只有 GET 详情读取,不调用 update-impact,不写 BI。真正执行编辑使用 `dsl apply --operation edit`:
|
|
242
|
+
|
|
243
|
+
- 不带 `--confirm` 时是 apply preview:重新生成冻结计划,并对所有 planned 条目执行原子 save-precheck(适用时)和三类 `validate-update-impact`,输出 `checks`;这些是服务端预检,不更新指标;
|
|
244
|
+
- `confirmation-required` 表示存在口径冲突或下游影响,调用方必须展示 `precheck/impact`;只有用户明确接受后才能用同一 DSL 追加 `--confirm --state <path>`;
|
|
245
|
+
- confirmed apply 在任何写入前取得 state 锁,绑定源码 hash、环境、planHash 和不可静默覆盖的 `<state>.plan.json`,并先对所有条目统一检查 immutable drift/currentFingerprint;漂移条目标记 blocked,其他独立 eligible 条目继续;
|
|
246
|
+
- 每项先持久化精确 `submittedBody` 和 `updating` 状态,再调用对应原地更新接口,随后优先回读草稿(结构化草稿不存在时才回退正式详情),按类型校验公共字段、原子取数、复合公式或衍生定义;
|
|
247
|
+
- 再次使用同一 DSL、state 和命令可续跑。已达到目标的条目直接标记 verified,不重复写;若上次在更新请求期间中断且回读未达到目标,则进入 `blocked/reconcile`,不会自动重发可能已被服务端接受的请求;核对后应重新 checkout,并使用新的 state 发起新计划;
|
|
248
|
+
- 多指标编辑按 metric ID 稳定顺序执行。开始写入前会统一做漂移和服务端预检,但服务端不提供跨指标事务;执行期间后续指标仍可能因并发变化或服务端失败而停在部分完成状态,state 是恢复和审计事实源。
|
|
249
|
+
|
|
250
|
+
confirmed edit apply 复用现有三类接口:原子指标在需要时调用 save-precheck,三类都调用 `validate-update-impact`、原地 update 和详情回读。没有新增 API;权限/PAT 约束沿用 `metric-api-contracts.md`,PAT 写入对照仍是交付前需补的环境验证项。
|
|
251
|
+
|
|
252
|
+
## 固定加载顺序
|
|
253
|
+
|
|
254
|
+
1. 内嵌 Builder。
|
|
255
|
+
2. `metric.config.js`。
|
|
256
|
+
3. 存在时依次加载 `catalog.context.js`、`catalog.datasets.js`、`catalog.metrics.js`、`catalog.biz-attrs.js`;每个 `register*` 只能在对应分域文件调用,文件阶段由 Go 宿主传入的闭包状态判定,DSL 源码不能切换或伪装阶段,也不能从后续 catalog 跨域补注册。catalog 项必须由 `dataset/remoteMetric/businessAttribute` 等对应构造器创建,普通同形对象会被拒绝。四类 catalog 加载完成后注册表会立即封存,helper 和指标/编辑脚本不能晚注册、替换或注入数据集与字段。
|
|
257
|
+
4. 存在 `helpers/` 时,按文件名排序加载该目录第一层的 `*.js`;子目录和非 JavaScript 文件不加载。
|
|
258
|
+
5. 按文件名排序加载 `atomic_*.js`、`composite_*.js`、`derived_*.js`、`edit_*.js`。
|
|
259
|
+
|
|
260
|
+
大型工程可把重复的常量、纯函数和 Builder 组合放入 `helpers/`,并用 `10_format.js`、`20_dimensions.js` 这样的数字前缀明确 helper 之间的依赖顺序。例如 helper 可以返回一个已设置数据源、聚合、维度和格式的 Builder,指标脚本再补充名称、口径并显式调用 `registerMetric(...)`。helper 加载时不得调用 `registerMetric/registerEdit`,也不得修改 project 配置或四类 catalog;编译器会比较每个 helper 加载前后的 DSL 状态,发现副作用时返回 `METRIC_DSL_HELPER_SIDE_EFFECT`。Builder 的内部 spec、类型品牌和注册状态保存在闭包中,注册时会冻结一份 IR 快照;用户脚本不能靠同形对象、注册后修改 Builder 或改写常用原生校验方法改变已注册 IR。这些措施是确定性的加载期完整性门禁,不把 DSL 承诺为针对恶意源码的安全沙箱;注册动作仍应保留在指标或编辑脚本中。
|
|
261
|
+
|
|
262
|
+
创建 DSL 当前开放:
|
|
263
|
+
|
|
264
|
+
- 原子字段聚合:`atomicMetric().fromDataset().aggregate()`。
|
|
265
|
+
- 原子字段表达式:`atomicMetric().fromDataset().expression()`;支持 `SUM/COUNT`、加减和 `CASE WHEN` 安全子集,字段引用必须使用所选数据集的真实字段 ID。
|
|
266
|
+
- 复合指标:`compositeMetric().formula()`,公式使用 `ref/metricRef/add/sub/mul/div/number` AST。
|
|
267
|
+
- 普通衍生:`derivedMetric().from().comparative/recent/cumulative/last/businessCondition()`。
|
|
268
|
+
- 组合衍生:`derivedMetric().composite()`。
|
|
269
|
+
- 三类创建指标的业务属性:`.businessAttributes(bizAttr(ATTR["属性名"], value), ...)`。
|
|
270
|
+
- 三类创建指标的格式和筛选:`.format({...})` 与 `.filter(condition/allOf/anyOf)`。
|
|
271
|
+
- 原子和复合指标的显式适用维度与时间维度;复合指标必须在显式维度与 `.useCommonDimensions()` 之间二选一。
|
|
272
|
+
|
|
273
|
+
`ref("key")` 只引用本工程创建项,并生成 Flow 的 `${metric:key}` / `[${metric:key}]` 占位符;`metricRef("metricId")` 只接受明确远端 ID。preview 会拒绝未知引用、自引用、循环引用、复合指标引用本批衍生指标,以及 draft 模式的任何批内本地引用。同比/环比和期末值会从本批基础指标的 `timeDimension` 继承日期字段;远端基础指标在 plan 阶段回读详情后冻结日期字段。
|
|
274
|
+
|
|
275
|
+
创建衍生指标时,plan/apply 的只读预检还会核对远端基础指标的 `timeDim` 是否属于该指标的 `applicableDims`;本批本地基础指标仅在显式列出适用维度时做同样检查。共用维度复合指标若显式指定 `timeDimension`,plan 会检查其是否至少存在于一个公式引用指标的适用维度;本批引用的维度尚不能确定时会警告并延后。apply 在提交创建前重新回读全部引用指标,无法核验或时间字段不属于任何引用指标时阻止写入;创建后还会核对回读的 `timeDim` 是否属于最终 `applicableDims`,不一致则不能标记为 verified。已知服务端可能先落库发布无效定义、再于建卡阶段失败,因此写前检查只保证必要条件,不能代替服务端最终校验和失败回滚;仍须以回读及取数闭环验收,也不会自动映射同名日期字段。
|
|
276
|
+
|
|
277
|
+
本批本地基础指标的 `LAST` 支持性无法在 plan 时从未来的指标 ID 判断。confirmed create apply 在该基础指标创建并验证后、提交 LAST 创建请求前,读取服务端详情中的 `isLastSupported`:`false` 以 `FLOW_BASE_LAST_UNSUPPORTED` 阻断;字段缺失或详情不可读以 `FLOW_BASE_LAST_CHECK_FAILED` 阻断。两种情况均不发送 LAST 创建请求,已创建的基础指标仍保留在同一 state 中,不做删除重建。服务端仍须对真实创建最终校验。
|
|
278
|
+
|
|
279
|
+
当前创建 DSL 的 `businessOwner` 仍是工程级默认值,不支持单个指标覆盖;原子表达式也不支持动态参数。后续标准 Excel -> DSL 单向生成遇到这两类无法表达的输入时必须以稳定诊断拒绝,不能忽略字段或降级生成语义不同的 DSL。
|
|
280
|
+
|
|
281
|
+
创建 Flow state 整体完成 `usable` 验收后,可显式执行 `flow export --state ... --out ...` 生成版本化 `created-metrics.json`。该 manifest 只传递环境、planHash、逻辑 key、已验证的真实 metric ID、类型和可选服务端定义指纹;部分成功、审批中、draft 或环境不一致均拒绝导出。下游仍必须把 ID 交给 `guanvis metric-init` 回读服务端,不能从 handoff 合成 `metrics.js`。
|
|
282
|
+
|
|
283
|
+
## 运行边界
|
|
284
|
+
|
|
285
|
+
- 默认执行超时 2 秒、调用栈 1024 层。
|
|
286
|
+
- 最多 1024 个脚本、2 MiB 源码、1000 个注册指标、8 MiB 导出对象;脚本上限覆盖每个指标一个脚本以及固定 config/catalog 文件。
|
|
287
|
+
- `helpers/` 文件计入脚本数和源码大小限制。
|
|
288
|
+
- 不提供文件、网络、进程和环境变量 API。
|
|
289
|
+
- 禁用 `Date`、`Math.random`、`eval` 和 `Function`。
|
|
290
|
+
- 原生 CLI 使用进程级内存边界;Goja 不提供单 Runtime 硬内存配额,因此该 DSL 面向本地受信工程源码,不定义为不可信代码沙箱。
|
|
291
|
+
- WASI 编译已验证通过,guest 内存硬边界由宿主负责;`apply/verify/reconcile/export` 的 WASI 禁用规则不变。
|
|
292
|
+
|
|
293
|
+
## Phase 0 验证结论
|
|
294
|
+
|
|
295
|
+
- 原生单元测试、`go vet ./...`、`go build ./...` 通过。
|
|
296
|
+
- `GOOS=wasip1 GOARCH=wasm` 的 metricdsl 测试包和完整 guanmetric 编译通过。
|
|
297
|
+
- 本地完整 wasm 约 35 MiB;最终发布体积应在平台构建链路中持续观测。
|
|
298
|
+
- 当前开发机没有 WASI runtime,因此尚未在本机执行 wasm;目标宿主仍需补运行级集成测试。
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0",
|
|
3
|
+
"command": "flow.export",
|
|
4
|
+
"valid": true,
|
|
5
|
+
"output": ".guanmetric/created-metrics.json",
|
|
6
|
+
"metricCount": 1,
|
|
7
|
+
"manifest": {
|
|
8
|
+
"schemaVersion": "1.0",
|
|
9
|
+
"environment": {
|
|
10
|
+
"profile": "demo",
|
|
11
|
+
"baseUrl": "https://bi.example.com",
|
|
12
|
+
"domain": "example"
|
|
13
|
+
},
|
|
14
|
+
"planHash": "example-plan-hash",
|
|
15
|
+
"acceptance": {
|
|
16
|
+
"mode": "usable"
|
|
17
|
+
},
|
|
18
|
+
"metrics": [
|
|
19
|
+
{
|
|
20
|
+
"key": "sales",
|
|
21
|
+
"metricId": "metric_sales_xxx",
|
|
22
|
+
"subType": "ATOMIC",
|
|
23
|
+
"status": "verified",
|
|
24
|
+
"eligibleForGuanvis": true,
|
|
25
|
+
"definitionFingerprint": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"diagnostics": []
|
|
30
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://guandata.dev/schemas/guanmetric/metric-flow-export-1.0.json",
|
|
4
|
+
"title": "guanmetric flow export result v1.0",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "command", "valid", "metricCount", "diagnostics"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": {"const": "1.0"},
|
|
10
|
+
"command": {"const": "flow.export"},
|
|
11
|
+
"valid": {"type": "boolean"},
|
|
12
|
+
"output": {"type": "string"},
|
|
13
|
+
"metricCount": {"type": "integer", "minimum": 0},
|
|
14
|
+
"manifest": {"$ref": "metric-flow-handoff.schema.json"},
|
|
15
|
+
"diagnostics": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": {"$ref": "#/$defs/diagnostic"}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"$defs": {
|
|
21
|
+
"diagnostic": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"required": ["code", "severity", "message"],
|
|
25
|
+
"properties": {
|
|
26
|
+
"code": {"type": "string"},
|
|
27
|
+
"severity": {"enum": ["error", "warning"]},
|
|
28
|
+
"key": {"type": "string"},
|
|
29
|
+
"field": {"type": "string"},
|
|
30
|
+
"message": {"type": "string"}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0",
|
|
3
|
+
"environment": {
|
|
4
|
+
"profile": "demo",
|
|
5
|
+
"baseUrl": "https://bi.example.com",
|
|
6
|
+
"domain": "example"
|
|
7
|
+
},
|
|
8
|
+
"planHash": "example-plan-hash",
|
|
9
|
+
"acceptance": {
|
|
10
|
+
"mode": "usable"
|
|
11
|
+
},
|
|
12
|
+
"metrics": [
|
|
13
|
+
{
|
|
14
|
+
"key": "sales",
|
|
15
|
+
"metricId": "metric_sales_xxx",
|
|
16
|
+
"subType": "ATOMIC",
|
|
17
|
+
"status": "verified",
|
|
18
|
+
"eligibleForGuanvis": true,
|
|
19
|
+
"definitionFingerprint": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"key": "sales_ratio",
|
|
23
|
+
"metricId": "metric_sales_ratio_xxx",
|
|
24
|
+
"subType": "COMPOSITE",
|
|
25
|
+
"status": "verified",
|
|
26
|
+
"eligibleForGuanvis": true
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://guandata.dev/schemas/guanmetric/metric-flow-handoff-1.0.json",
|
|
4
|
+
"title": "guanmetric created metrics handoff v1.0",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schemaVersion", "environment", "planHash", "acceptance", "metrics"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": {"const": "1.0"},
|
|
10
|
+
"environment": {"$ref": "#/$defs/environment"},
|
|
11
|
+
"planHash": {"type": "string", "minLength": 1},
|
|
12
|
+
"acceptance": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"required": ["mode"],
|
|
16
|
+
"properties": {"mode": {"const": "usable"}}
|
|
17
|
+
},
|
|
18
|
+
"metrics": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"minItems": 1,
|
|
21
|
+
"items": {"$ref": "#/$defs/metric"}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"$defs": {
|
|
25
|
+
"environment": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"properties": {
|
|
29
|
+
"profile": {"type": "string"},
|
|
30
|
+
"baseUrl": {"type": "string"},
|
|
31
|
+
"domain": {"type": "string"}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"metric": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"additionalProperties": false,
|
|
37
|
+
"required": ["key", "metricId", "subType", "status", "eligibleForGuanvis"],
|
|
38
|
+
"properties": {
|
|
39
|
+
"key": {"type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$"},
|
|
40
|
+
"metricId": {"type": "string", "minLength": 1},
|
|
41
|
+
"subType": {"enum": ["ATOMIC", "COMPOSITE", "DERIVED"]},
|
|
42
|
+
"status": {"const": "verified"},
|
|
43
|
+
"eligibleForGuanvis": {"const": true},
|
|
44
|
+
"definitionFingerprint": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -20,6 +20,9 @@ guanmetric flow apply --file metric_flow.json --state metric_flow.state.json --c
|
|
|
20
20
|
# 审批通过或故障恢复后重新验证,不会重复创建
|
|
21
21
|
guanmetric flow verify --state metric_flow.state.json
|
|
22
22
|
|
|
23
|
+
# 全部 usable 指标验证成功后,导出供下游按 ID 消费的独立 handoff
|
|
24
|
+
guanmetric flow export --state metric_flow.state.json --out created-metrics.json
|
|
25
|
+
|
|
23
26
|
# 创建结果未知时,先只读核对候选;默认只预览,不修改 state
|
|
24
27
|
guanmetric flow reconcile --file metric_flow.json --state metric_flow.state.json \
|
|
25
28
|
--key sales --metric-id metric_xxx
|
|
@@ -171,6 +174,7 @@ Flow 会在每个指标创建后立即完成当前模式的验收,再创建依
|
|
|
171
174
|
- 创建 POST 前持久化的 canonical `submittedDefinition`,用于结果未知后的精确 reconcile;
|
|
172
175
|
- `planned`、`creating`、`created`、`verified`、`pending`、`blocked`、`failed` 状态;
|
|
173
176
|
- 当前失败阶段、定义回读摘要和查询摘要;
|
|
177
|
+
- verify/reconcile 从服务端回读定义计算的可选 `definitionFingerprint`;旧 state 没有该证据时保持缺省;
|
|
174
178
|
- 可选的结构化 `error`,包含 flow 稳定错误码、错误类别、HTTP/业务状态码和服务端原始消息;
|
|
175
179
|
- 创建、验证、待审批和失败计数。
|
|
176
180
|
|
|
@@ -178,7 +182,7 @@ Flow 会在每个指标创建后立即完成当前模式的验收,再创建依
|
|
|
178
182
|
|
|
179
183
|
每个创建成功的 ID 会立刻写入状态文件。再次执行 `flow apply` 时,有 ID 的条目只重新验证,不重复创建。原子指标发布前若检测到口径冲突,flow 固定 `blocked`;它不会把 `--confirm` 解释成忽略口径冲突。部分成功后也不会删除已创建指标做回滚,因为指标 ID、权限、审批和下游引用不能由客户端无损恢复。
|
|
180
184
|
|
|
181
|
-
原生 Unix/Windows 中,confirmed `flow apply` 会在读取计划或访问 BI 前取得 state 锁,`flow verify` 和 `flow
|
|
185
|
+
原生 Unix/Windows 中,confirmed `flow apply` 会在读取计划或访问 BI 前取得 state 锁,`flow verify`、`flow reconcile` 和 `flow export` 也在读取 state 前取得锁;四者对同一个 state 路径持有完整生命周期的跨进程独占锁,避免两个 Agent 同时创建、覆盖状态或导出中间快照。apply preview 不读取 state,因此不持锁。锁文件固定为 `<state>.lock`,只是 OS advisory lock 的稳定载体,可以长期保留;进程正常退出或崩溃时锁由操作系统自动释放,不依赖删除文件判断是否过期。锁已被占用时命令直接失败,不降级成无锁执行。WASI/非原生运行时没有可审计的跨 guest 文件锁契约,因此明确拒绝这四个生命周期命令,只保留 `flow generate`、`flow plan` 等只读能力。
|
|
182
186
|
|
|
183
187
|
提交创建请求前,状态会把 `creating` 与本次真正提交的 canonical `submittedDefinition` 一次性落盘。若进程在请求期间中断,或服务端响应没有返回可记录的 ID,后续执行会进入 `blocked/reconcile`,不会自动重试写请求;这是为了避免“服务端其实已创建、客户端却因断线再次创建”的重复指标。`reconcile` 只允许把候选详情与这个不可变快照比较,不重新按当前服务端维度元数据生成请求;旧状态若缺少该快照会拒绝绑定。Agent 可以按环境、项目、类型、真实名称和指标定义只读核对线上候选,但不能把 Flow 逻辑 key 当作指标名称,也不能只凭同名就认领资源。
|
|
184
188
|
|
|
@@ -197,6 +201,16 @@ Flow 会在每个指标创建后立即完成当前模式的验收,再创建依
|
|
|
197
201
|
- 有 `pending`、`blocked` 或 `failed`:保存状态,非零退出。
|
|
198
202
|
- 所有指标达到所选验收模式:`status=succeeded`,零退出。
|
|
199
203
|
|
|
204
|
+
## Created metrics handoff
|
|
205
|
+
|
|
206
|
+
`flow export --state <state.json> --out <created-metrics.json>` 从 Flow 恢复状态生成独立的跨工具机器契约,不访问 BI、不创建卡片,也不把 state 本身暴露给下游。命令先取得同一 state 的生命周期锁,再校验当前 CLI 环境身份;只有 `acceptance.mode=usable`、state 整体 `succeeded` 且每一项都有真实 `metricId` 并处于 `verified` 时才导出。部分成功、审批中、blocked、failed、draft、环境不一致、重复 key/ID 或非法类型都会输出 `valid=false` 的 `flow.export` 结果和稳定诊断,并且不写文件。
|
|
207
|
+
|
|
208
|
+
输出文件是 `schemaVersion=1.0` 的 `created-metrics` manifest,包含环境、planHash、acceptance,以及按 key 稳定排序的 `key/metricId/subType/status/eligibleForGuanvis`。`definitionFingerprint` 仅在当前版本 verify/reconcile 已从服务端详情转换为 canonical 更新模型并计算 SHA-256 时出现;旧 state 缺少该证据时可以省略,禁止用 `submittedDefinition`、Flow plan 或展示摘要伪造。文件已存在时默认拒绝,明确追加 `--force` 才替换。
|
|
209
|
+
|
|
210
|
+
manifest 不包含适用维度、时间维度、格式或指标详情。guanvis 等下游必须先核对 manifest 环境,再把其中的真实 ID 传给 `guanvis metric-init` 回读服务端生成自己的 `metrics.js`;不得从 manifest、Metric IR、Flow plan 或 Flow state 合成可视化快照。manifest 的完整样例和 Schema 分别见 `metric-flow-handoff-example.json`、`metric-flow-handoff.schema.json`;命令 stdout 的 `flow.export` 结果契约见 `metric-flow-export-example.json`、`metric-flow-export.schema.json`。
|
|
211
|
+
|
|
212
|
+
`flow export` 的结构化诊断使用 `FLOW_HANDOFF_INELIGIBLE`、`FLOW_HANDOFF_STATE_INVALID` 和 `FLOW_ENVIRONMENT_MISMATCH`;调用方按 `code/field` 分支,不能匹配中文 message。该命令不新增 BI API,当前环境身份只来自本地 clifetch 配置。由于导出期间使用原生 state 文件锁,WASI/非原生运行时与 apply/verify/reconcile 一样禁用该命令。
|
|
213
|
+
|
|
200
214
|
创建合法性由 BI 后端最终裁决。Flow 的单指标业务校验只实现当前 Web 创建页已有而后端创建链路缺失的规则;后端已有规则不复制,前后端都没有的规则不新增。`FLOW_FRONTEND_RULE_INVALID` 表示命中这一差集,具体字段见 diagnostic 的 `field`。Flow 另外负责计划结构、批内依赖、占位符、环境/状态一致性和验收编排。当前职责证据和逐条差异见 `metric-validation-ownership.md`。
|
|
201
215
|
|
|
202
216
|
## Domain and Contract
|
|
@@ -22,6 +22,50 @@
|
|
|
22
22
|
"properties": {"name": {"$ref": "#/$defs/nonEmptyString"}, "businessOwner": {"$ref": "#/$defs/nonEmptyString"}, "relatedFields": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/relatedFieldWrite"}}},
|
|
23
23
|
"additionalProperties": false
|
|
24
24
|
},
|
|
25
|
+
"writeRequest84": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"required": [
|
|
28
|
+
"name",
|
|
29
|
+
"businessOwner",
|
|
30
|
+
"relatedFields"
|
|
31
|
+
],
|
|
32
|
+
"properties": {
|
|
33
|
+
"name": {
|
|
34
|
+
"$ref": "#/$defs/nonEmptyString"
|
|
35
|
+
},
|
|
36
|
+
"businessOwner": {
|
|
37
|
+
"$ref": "#/$defs/nonEmptyString"
|
|
38
|
+
},
|
|
39
|
+
"relatedFields": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"minItems": 1,
|
|
42
|
+
"items": {
|
|
43
|
+
"$ref": "#/$defs/relatedFieldWrite"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"alias": {
|
|
47
|
+
"type": "string"
|
|
48
|
+
},
|
|
49
|
+
"dirId": {
|
|
50
|
+
"type": "string"
|
|
51
|
+
},
|
|
52
|
+
"description": {
|
|
53
|
+
"type": "string"
|
|
54
|
+
},
|
|
55
|
+
"valueSource": {
|
|
56
|
+
"anyOf": [
|
|
57
|
+
{
|
|
58
|
+
"$ref": "#/$defs/relatedFieldWrite"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"type": "null"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"additionalProperties": false,
|
|
67
|
+
"description": "BI >= 8.4.0; source commit 4f27c3f0fdb7df5d9cce88f8475195497ccca1f1 PublicDimensionRO"
|
|
68
|
+
},
|
|
25
69
|
"mutationResponse": {
|
|
26
70
|
"type": "object",
|
|
27
71
|
"required": ["result", "response"],
|