@guandata/guanmetric 0.1.21 → 0.1.22

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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## @guandata/guanmetric 0.1.22 - 2026-10-19
4
+
5
+ - 新增指标 DSL 创建与编辑流程,支持预览、文档生成、计划核对和应用。
6
+ - 支持检出已有指标、对比本地变更及按计划原地编辑。
7
+
3
8
  ## @guandata/guanmetric 0.1.21 - 2026-10-12
4
9
 
5
10
  - 共享配置写入增加并发保护,避免覆盖并发更新的认证配置。
package/README.md CHANGED
@@ -12,6 +12,8 @@ npm install -g --foreground-scripts @guandata/guanmetric
12
12
 
13
13
  安装包会通过 npm `optionalDependencies` 自动选择当前系统的原生二进制。不要使用 `--omit=optional` 或 `optional=false`;企业 npm 镜像也需要同步对应的 `@guandata/guanmetric-<平台>-<架构>` 包。若平台包缺失,CLI 会显示对应包名和重新安装方法。
14
14
 
15
+ `dsl preview`、`dsl document` 和 `dsl diff` 等纯本地命令由安装包内置的 DSL 引擎执行,不需要源码仓库。`dsl init`、`dsl checkout`、`dsl plan`、`dsl apply` 以及其他访问 BI 的命令会通过 `guancli` 或 `guancli-lite` 调用 BI API;客户环境必须将其中一个加入 `PATH`,或用 `GUANCLI_PATH` 指向其原生程序,并按对应 CLI 配置认证信息。
16
+
15
17
  ## 常用命令
16
18
 
17
19
  ```bash
@@ -51,6 +53,11 @@ guanmetric metric-tree update-config <metric_tree_id> --file metric_tree_config.
51
53
 
52
54
  ## 版本更新
53
55
 
56
+ ### @guandata/guanmetric 0.1.22
57
+
58
+ - 新增指标 DSL 创建与编辑流程,支持预览、文档生成、计划核对和应用。
59
+ - 支持检出已有指标、对比本地变更及按计划原地编辑。
60
+
54
61
  ### @guandata/guanmetric 0.1.21
55
62
 
56
63
  - 共享配置写入增加并发保护,避免覆盖并发更新的认证配置。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guandata/guanmetric",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "观远 BI 指标写操作工具 - 指标创建编辑删除、指标目录、公共维度、Excel 标准化",
5
5
  "bin": {
6
6
  "guanmetric": "bin/run.js"
@@ -41,11 +41,11 @@
41
41
  "node": ">=14"
42
42
  },
43
43
  "optionalDependencies": {
44
- "@guandata/guanmetric-darwin-arm64": "0.1.21",
45
- "@guandata/guanmetric-darwin-x64": "0.1.21",
46
- "@guandata/guanmetric-linux-x64": "0.1.21",
47
- "@guandata/guanmetric-linux-arm64": "0.1.21",
48
- "@guandata/guanmetric-win32-x64": "0.1.21"
44
+ "@guandata/guanmetric-darwin-arm64": "0.1.22",
45
+ "@guandata/guanmetric-darwin-x64": "0.1.22",
46
+ "@guandata/guanmetric-linux-x64": "0.1.22",
47
+ "@guandata/guanmetric-linux-arm64": "0.1.22",
48
+ "@guandata/guanmetric-win32-x64": "0.1.22"
49
49
  },
50
50
  "publishConfig": {
51
51
  "registry": "https://registry.npmjs.org/",
@@ -32,8 +32,19 @@ guanmetric flow plan --file metric_flow.json
32
32
  guanmetric flow generate --file standard.xlsx --out metric_flow.json
33
33
  guanmetric flow apply --file metric_flow.json --state metric_flow.state.json --confirm
34
34
  guanmetric flow verify --state metric_flow.state.json
35
+ guanmetric flow export --state metric_flow.state.json --out created-metrics.json
35
36
  guanmetric flow reconcile --file metric_flow.json --state metric_flow.state.json --key <flow_key> --metric-id <metric_id>
36
37
  guanmetric flow reconcile --file metric_flow.json --state metric_flow.state.json --key <flow_key> --metric-id <metric_id> --confirm
38
+ guanmetric dsl preview ./metric-project
39
+ guanmetric dsl document ./metric-project --out ./metric-project/.guanmetric/docs
40
+ guanmetric dsl plan ./metric-project --operation create
41
+ guanmetric dsl plan ./metric-project --operation create --key <flow_key>
42
+ guanmetric dsl apply ./metric-project --operation create --state .guanmetric/metric-state.json
43
+ guanmetric dsl apply ./metric-project --operation create --state .guanmetric/metric-state.json --confirm
44
+ guanmetric dsl checkout <metric_id_1> <metric_id_2> -d ./metric-project
45
+ guanmetric dsl diff ./metric-project
46
+ guanmetric dsl plan ./metric-project --operation edit
47
+ guanmetric dsl plan ./metric-project --operation edit --metric-id <metric_id>
37
48
  guanmetric edit <metric_id> --file patch.json --dry-run
38
49
  guanmetric delete <metric_id>
39
50
  guanmetric batch online --id <metric_id_1> --id <metric_id_2> --dry-run
@@ -88,9 +99,31 @@ guanmetric fetch POST /api/metric-platform/<unwrapped_endpoint> --file request.j
88
99
 
89
100
  构造或解析指标主题/指标目录的创建、编辑、详情/列表/目录树和删除请求时,先读取 `metric-hierarchy-api-contracts.md`。主题编辑只提交 `name/icon/iconColor`;目录编辑只提交 `name/parentDirId/dirType`,其中 `dirType` 固定为 `METRIC_PLATFORM`。指标目录没有单条详情接口,`directory get` 是从指定 Project 的目录树按 ID 提取节点。创建和编辑必须写后回读;删除默认只展示目标,必须追加 `--confirm`。本轮不提供指标 move 或指标 owner 转移命令。
90
101
 
91
- 用户通过对话提出一批相互依赖的指标创建需求时,优先使用 `guanmetric flow`,不要逐个调用 `create`/`fetch` 后自行判断成功。标准模板经用户确认且所有真实指标行为“可创建”后,必须优先执行 `flow generate --file <standard.xlsx> --out <metric_flow.json>`,不要由 Agent 手写计划。该命令会解析数据集字段、当前责任人/唯一用户、业务属性、已有指标和批内引用,生成稳定 key 与占位符,并立即运行 `flow plan`;无法唯一解析时输出带 Sheet/行/列的稳定诊断且不写计划。同一模板不能混合“上线”和“草稿”:全部上线生成 `usable`,全部草稿生成 `draft`,混合时必须拆分模板。提交真实创建前必须检查生成计划中的取数定义(`fdId+aggrType` 或 `formulaField`)、适用维度、时间维度、业务属性、责任人、格式、复合/衍生引用和 `publish` 均与确认后的标准模板一致;任一关键字段缺失都不得执行 `flow apply --confirm`。生成后仍可单独运行 `flow plan`;用户确认后运行带 `--confirm` 的 `flow apply`,并始终提供 `--state` 状态文件以便失败或审批后续跑。`acceptance.mode=usable` 要求请求 `publish=true`,最终状态为 `PUBLISHED`、当前用户 `canUse=true`,且 `guancli metric query --limit 1 -f json` 成功;组合衍生引用带固定高级计算粒度的指标时,Flow 会读取引用定义并自动追加唯一兼容的 `--dim "<时间维度> toMonth/toWeek/..."`,同时把实际维度、粒度和策略写入 state 的 `query` 证据。引用粒度不一致时返回 `FLOW_QUERY_GRANULARITY_CONFLICT`,不能靠循环尝试不同粒度掩盖定义冲突。`acceptance.mode=draft` 要求 `publish=false`,只验证草稿定义能够回读。审批中的指标返回 `pending`,不能汇报为已可用。创建结果未知时,`flow apply` 会永久保持该条目为 `blocked/reconcile`,绝不重发创建;创建前已将真正提交的 canonical 请求保存到 state 的 `submittedDefinition`,`reconcile` 只能据此核对候选,不能按实时元数据重建请求,而且同一 state 内一个非空 metricId 只能绑定一个 key。Agent 先只读核对线上候选,再执行不带 `--confirm` 的 `flow reconcile --file ... --state ... --key ... --metric-id ...` 预览,确认环境、planHash、类型、候选 ID 未被占用和关键定义完全一致后,才追加 `--confirm` 把真实 ID 写回 state。原生 Unix/Windows 中,confirmed apply 会在读取计划或访问 BI 前取得 state 锁,verify/reconcile 也在读取 state 或访问 BI 前取得锁,三者由跨进程独占锁串行化;apply preview 不读取 state,因此不持锁。遇到“正由另一个操作占用”时等待对方结束后重试,禁止改用另一个 state 绕过。WASI/非原生运行时因无法证明跨 guest 单写者,明确禁用 apply/verify/reconcile,只保留 generate/plan 等只读能力。部分指标已创建后禁止自动删除回滚,修正问题后复用同一计划和状态文件继续验证。完整契约见 `metric-flow.md`。
102
+ 用户通过对话提出一批相互依赖的指标创建需求时,优先使用 `guanmetric flow`,不要逐个调用 `create`/`fetch` 后自行判断成功。标准模板经用户确认且所有真实指标行为“可创建”后,必须优先执行 `flow generate --file <standard.xlsx> --out <metric_flow.json>`,不要由 Agent 手写计划。该命令会解析数据集字段、当前责任人/唯一用户、业务属性、已有指标和批内引用,生成稳定 key 与占位符,并立即运行 `flow plan`;无法唯一解析时输出带 Sheet/行/列的稳定诊断且不写计划。同一模板不能混合“上线”和“草稿”:全部上线生成 `usable`,全部草稿生成 `draft`,混合时必须拆分模板。提交真实创建前必须检查生成计划中的取数定义(`fdId+aggrType` 或 `formulaField`)、适用维度、时间维度、业务属性、责任人、格式、复合/衍生引用和 `publish` 均与确认后的标准模板一致;任一关键字段缺失都不得执行 `flow apply --confirm`。生成后仍可单独运行 `flow plan`;用户确认后运行带 `--confirm` 的 `flow apply`,并始终提供 `--state` 状态文件以便失败或审批后续跑。`acceptance.mode=usable` 要求请求 `publish=true`,最终状态为 `PUBLISHED`、当前用户 `canUse=true`,且 `guancli metric query --limit 1 -f json` 成功;组合衍生引用带固定高级计算粒度的指标时,Flow 会读取引用定义并自动追加唯一兼容的 `--dim "<时间维度> toMonth/toWeek/..."`,同时把实际维度、粒度和策略写入 state 的 `query` 证据。引用粒度不一致时返回 `FLOW_QUERY_GRANULARITY_CONFLICT`,不能靠循环尝试不同粒度掩盖定义冲突。`acceptance.mode=draft` 要求 `publish=false`,只验证草稿定义能够回读。审批中的指标返回 `pending`,不能汇报为已可用。创建结果未知时,`flow apply` 会永久保持该条目为 `blocked/reconcile`,绝不重发创建;创建前已将真正提交的 canonical 请求保存到 state 的 `submittedDefinition`,`reconcile` 只能据此核对候选,不能按实时元数据重建请求,而且同一 state 内一个非空 metricId 只能绑定一个 key。Agent 先只读核对线上候选,再执行不带 `--confirm` 的 `flow reconcile --file ... --state ... --key ... --metric-id ...` 预览,确认环境、planHash、类型、候选 ID 未被占用和关键定义完全一致后,才追加 `--confirm` 把真实 ID 写回 state。原生 Unix/Windows 中,confirmed apply、verify、reconcile 和 export 对同一 state 使用跨进程独占锁串行化;apply preview 不读取 state,因此不持锁。遇到“正由另一个操作占用”时等待对方结束后重试,禁止改用另一个 state 绕过。WASI/非原生运行时因无法证明跨 guest 单写者,明确禁用 apply/verify/reconcile/export,只保留 generate/plan 等只读能力。部分指标已创建后禁止自动删除回滚,修正问题后复用同一计划和状态文件继续验证。完整契约见 `metric-flow.md`。
92
103
  `flow verify` 的 `usable` 只证明指标已发布、可使用且查询成功,不证明它与另一个指标数值等价。只有用户明确要求新旧指标或两组指标数值对比时,才使用 `guancli metric batch-query` 执行独立的等价验证;必须固定相同粒度、筛选范围、排序和 raw 数值,按维度键对齐,并把可能截断的结果报告为 `inconclusive`,禁止用无排序的默认前 50 行按位置比较后宣称指标定义错误。详细 recipe 见 guancli skill 的 `references/metric-batch-query.md`。
93
104
 
105
+ 本批本地基础指标上的期末值(`LAST`)必须等基础指标实际创建并验证后,再由 confirmed Flow/DSL apply 回读其服务端 `isLastSupported`。明确不支持、字段缺失或详情不可读时阻断 LAST 创建并保留 state;不能因为 plan 通过就认为该期末值可创建,也不能绕过保护重试或删除重建已创建的基础指标。
106
+
107
+ 需要把 Flow 创建结果交给 guanvis 等下游时,使用 `flow export --state <state> --out <created-metrics.json>`,不要让下游直接读取 Flow state。只有 usable state 整体 succeeded 且每一项 verified 时才允许导出;partial/pending/blocked/failed/draft 或环境不一致都会阻断。输出只包含环境、planHash、逻辑 key、真实 metric ID、类型、验收状态和可选的服务端 canonical 定义指纹;已有文件默认拒绝覆盖,明确追加 `--force` 才替换。下游仍必须使用 `guanvis metric-init <metricId...>` 回读服务端生成 `metrics.js`,禁止从 handoff、Flow state、Flow plan 或 Metric IR 合成指标详情。完整契约见 `metric-flow.md`、`metric-flow-handoff.schema.json`、`metric-flow-handoff-example.json`、`metric-flow-export.schema.json` 和 `metric-flow-export-example.json`。
108
+
109
+ `guanmetric dsl init --project-id <id> --dir-id <id> --dataset <alias>=<datasetId> --metric <alias>=<metricId> -d <project_dir>` 只读校验主题、目录、当前用户和显式选择的数据集/远端指标,并读取当前全部启用的业务属性,生成 context、datasets、metrics、biz-attrs catalog 与环境绑定 manifest;dataset/metric 至少选一种。远端指标通过 `metricRef(METRIC.<alias>)` 引用,状态与维度只作快照,plan/apply 仍读取服务端权威状态。业务属性以 `ATTR["显示名称"]` 引用;启用配置重名时 init 拒绝,已停用配置不暴露。创建指标使用 `.businessAttributes(bizAttr(...), ...)`,字符串/字符串数组和枚举值会按快照提前校验,但必填、默认值和最终合法性仍由 plan/服务端裁决。重复 init 刷新生成快照但不覆盖已有配置和指标脚本,跨环境拒绝。
110
+
111
+ 需要评审 DSL 工程血缘时,使用纯本地 `dsl document <project_dir> --out <output_dir>`。命令在完整编译通过后固定生成 `metric-dependencies.json` 和 `metric-documentation.md`:前者是版本化机器契约,后者包含摘要、节点/边表、稳定创建顺序和 Mermaid 图。节点区分 create、edit、dataset、remoteMetric,边区分 datasetSource、localMetric、remoteMetric;不会访问 BI。已有任一固定输出文件时默认拒绝,明确追加 `--force` 才替换。完整契约见 `metric-dsl.md`、`metric-dsl-dependencies.schema.json` 和 `metric-dsl-document-output.schema.json`。
112
+
113
+ 大型工程可对 `dsl plan/apply` 做选择性执行。create 使用可重复的 `--key <flow_key>`,会自动包含全部传递本地上游但不包含下游;edit 使用可重复的 `--metric-id <metric_id>`,只处理明确选择的 checkout 指标。两类参数不能混用,不传时保持全量行为。无论选择什么,命令都先完整编译整个工程,未选脚本的语法或语义错误也会阻断;随后 state、planHash 和 edit sourceHash 只绑定规范化后的选中子集,未选脚本变更不会使该 state 失效。续跑必须使用同一个 state,并解析为同一 `resolved` 子集;省略选择或换成不同子集会拒绝复用。stdout 的可选 `selection` 明确列出 requested、resolved 和自动纳入的 autoIncluded。完整契约见 `metric-dsl.md` 和 `metric-dsl-selection.schema.json`。
114
+
115
+ 确认后的标准 Excel 可以通过 `guanmetric dsl generate --file <standard.xlsx> -d <new_project_dir>` 单向生成完整 DSL 工程。该命令复用 Flow generate 的字段、指标引用、业务属性和远端预检,目标目录必须不存在;它在临时目录生成 config、四类 catalog 和指标脚本,完整 DSL 编译通过后才原子落盘,不创建或编辑 BI 指标。生成后以 DSL 为事实源,不做 Excel/DSL 双向合并。单组输入仍直接生成一个工程;跨主题、目录或保存方式时自动按 `(projectId,parentDirId,acceptance)` 生成 `project_NNN` 子工程和 `guanmetric-dsl-bundle.json` 清单,不要再人工拆改 Excel。部分分组被真实语义错误阻断时,合法分组仍会原子生成,stdout 为 `generated=true,complete=false` 并以非零状态退出;只能继续清单中已生成的子工程,不能忽略对应 `projectKey` 的 diagnostics。不同子工程不能保留批内指标依赖;生成器发现引用名称属于同一 Excel 的其他子工程时会明确拒绝,绝不会退化成线上同名指标。逐项责任人解析后若都等于当前登录用户,可无损映射为工程级 `currentUser()`;不同用户的单指标覆盖、动态参数、复合公式字段映射或其它当前 DSL 无法无损表达的语义仍以稳定诊断拒绝。复制现有复合指标且同名时间字段在多个引用中不唯一时,优先在标准模板“时间维度”写真实字段 ID;生成器只有在能找到同名同公式的现有复合定义且其时间字段 ID 唯一一致时才回溯复用,禁止按公式顺序猜字段。单工程继续使用兼容的输出契约 1.0;bundle 使用 1.1。完整边界和输出契约见 `metric-dsl.md`、`metric-dsl-generate.schema.json`、`metric-dsl-generate-1.1.schema.json`。
116
+
117
+ 同比/环比的 `comparative({...})` 可选使用布尔字段 `replenishDimData`,用于无损表达标准模板中的“维度值补齐”;省略时不向服务端请求体写入该字段。指定日期对比使用 `fixedBase`,月末/季末/年末和按年周比较使用对应 `offsetType`;最近 N 与累计支持基于日期筛选开始或结束日期。时间衍生可显式保留源定义的 `dateFdId`(含粒度后缀),复合指标使用共用维度时仍可单独指定时间维度。原子 `expression()` 会按首版完整安全子集语法校验 `SUM/COUNT`、加减和 `CASE WHEN`,不会把缺操作数、残缺 CASE 或聚合外字段输出为有效 IR。
118
+
119
+ 多指标 edit plan 中,远端读取、目标字段漂移或单项 DTO 校验失败只把对应指标记录为 `blocked` item 和 warning diagnostic,其他独立指标仍可进入 confirmed apply;DSL、checkout manifest、环境、attach 类型与覆盖等结构错误继续阻断整批。confirmed apply 会把逐项结果持久化到 state,blocked/unknown 项不会自动重发。
120
+
121
+ 四类 catalog 仅允许在固定 catalog 加载阶段注册;catalog 项必须来自对应 DSL 构造器,进入 helper 或指标/编辑脚本前注册表与数据集字段容器会封存,禁止普通对象伪造、晚注册、替换或注入。筛选、公式和业务属性值同样必须来自对应类型化 helper,不接受手写同形对象;Builder 内部状态不向脚本暴露,注册时冻结 IR 快照,注册后修改 Builder 不影响结果。编辑已有业务限定衍生指标并切换到其它模式时,未显式设置筛选会清理旧 filter;同一 attach 显式调用 `filter/clearFilter` 时显式值优先,结果不受 setter 调用顺序影响。
122
+
123
+ 每个 catalog 注册函数只能出现在对应分域文件,执行阶段由 Go 宿主闭包提供,DSL 脚本不能自行切换;create plan/apply 在 catalog manifest 存在时必须校验当前环境和 project/directory 绑定。编辑业务属性的非空值必须使用 `bizAttr(ATTR[...], value)`,拒绝重复属性 ID;空数组表示清空。
124
+
125
+ 指标 DSL 当前处于分阶段交付。`guanmetric dsl preview <project_dir>` 是纯本地、确定性的编译入口,不访问 BI,也不创建或编辑指标;`dsl plan <project_dir> --operation create` 把同一 IR 转成 Flow 1.0 并复用 Flow 的只读远端校验,但仍不写 BI。`dsl apply <project_dir> --operation create` 默认仍是 preview;只有同时追加 `--confirm --state <path>` 才会在取得 Flow state 锁后真实创建并验证。确认执行会在 state 旁保存不可静默覆盖的派生 Flow 计划,并把路径写入 state.planFile;续跑继续用同一 DSL、state 和命令,verify 使用 `flow verify --state`,结果不确定时按 state.planFile 调用 `flow reconcile --file ... --state ...`。`dsl checkout <metricId...> -d <project_dir>` 只读回读草稿(仅在结构化“草稿不存在”时回退正式详情),按三类 Update DTO 白名单生成 `.guanmetric/manifest.json`、base 快照和默认 no-op `edit_*.js`;已有编辑脚本不会被覆盖。编辑 attach 已支持 rename、englishName、description、format、filter/clearFilter、dimensions、timeDimension/clearTimeDimension、businessAttributes 和 publish 字段级 setter,并支持原子 `fieldSource/expressionSource`、复合 `formula/useCommonDimensions`、衍生 `from + comparative/recent/cumulative/last/businessCondition` 与组合衍生 `composite`;编辑引用只允许明确远端 `metricRef()`,不提供任意 JSON path。`dsl diff` 纯本地展示相对 checkout 基线的字段差异;`dsl plan --operation edit` 重新回读当前草稿/正式详情,允许未修改字段漂移,目标字段远端变化且不同于 DSL 目标时以 `METRIC_DSL_REMOTE_DRIFT` 阻断,远端已等于目标时生成 no-op;同比/环比和期末值会只读回读新基础指标并冻结其时间维度。edit plan 只读,不执行 update-impact 或更新。`dsl apply --operation edit` 不带确认时执行 save-precheck/update-impact 并输出 checks;用户明确接受冲突与影响后才可追加 `--confirm --state` 原地更新。confirmed edit apply 会锁定并保存源码 hash、环境、冻结计划和逐项 state,写前统一复核所有 currentFingerprint,写后按三类 DTO 回读;同一命令可续跑,已达到目标不重复写,结果不确定且未达到目标时进入 blocked/reconcile 并禁止自动重发。多指标编辑没有服务端事务,部分完成以 state 为恢复和审计事实源。创建 DSL 已支持原子字段聚合与字段表达式、复合指标,以及对比、最近 N、累计、期末值、业务限定和组合衍生六类衍生指标;三类创建指标都可使用 catalog 驱动的 `businessAttributes(bizAttr(...))`、非空 JSON `format({...})` 和类型化 `filter(condition/allOf/anyOf)`。原子和复合可显式设置 `dimensions/timeDimension`;复合的显式维度与 `useCommonDimensions()` 互斥,时间维度必须属于适用维度。表达式使用 `expression()`,当前只接受 `SUM/COUNT`、加减和 `CASE WHEN` 安全子集,方括号内必须是所选数据集的真实字段 ID,最终请求只使用 `formulaField.fdFormula`。创建 DSL 暂不支持单指标责任人覆盖和动态参数;后续 Excel -> DSL 生成遇到这些输入必须拒绝,不能静默丢字段。本地/远端指标引用分别使用 `ref()`/`metricRef()`,draft 模式禁止批内本地引用。工程必须包含 `metric.config.js`,catalog 按 `catalog.context.js`、`catalog.datasets.js`、`catalog.metrics.js`、`catalog.biz-attrs.js` 固定顺序加载;存在 `helpers/` 时按文件名加载第一层 `*.js`,建议使用数字前缀明确依赖顺序,helper 只声明常量、纯函数和 Builder 组合,不得在加载时修改配置/catalog 或注册指标;最后按文件名加载指标脚本。helper 同样计入脚本数与源码大小限制,加载期副作用返回 `METRIC_DSL_HELPER_SIDE_EFFECT`。当前 DSL 不是不可信代码沙箱:原生 CLI 使用进程级内存边界,WASI 使用宿主 guest 内存边界;CLI 另外限制执行时间、调用栈、脚本数、源码大小、注册指标数和导出大小,并禁用时间、随机数、动态代码执行及外部 I/O 能力。完整的当前语法、输出契约和限制见 `metric-dsl.md`;创建完整复用 Flow,编辑复用三类专用原地更新链路,禁止把 preview/plan 成功解释为写入成功。
126
+
94
127
  指标定义是否合法由 BI 后端最终裁决。Flow 的单指标业务校验必须取“当前 Web 创建页已有规则”减去“当前后端创建链路已有规则”的差集:只补前端有而后端没有的规则,不复制后端已有规则,也不擅自新增前后端都没有的规则。当前差集包括草稿名称格式/长度、英文名格式/长度、业务口径最大长度、原子 `fdId+aggrType` 配对完整性、普通衍生 `baseMetricId+advType` 必填、自定义业务属性文本长度以及原子 save-precheck;计划结构、批内依赖、占位符、环境/状态一致性和验收编排属于 Flow 自身契约。创建接口还会执行落库、权限和上线等后置步骤,因此任何未返回可记录 `metricId` 的创建错误都必须保留结构化业务状态并进入 `reconcile`,不能自动重试。校验职责基线见 `metric-validation-ownership.md`。
95
128
  查询加速写操作已封装为 `accelerate` 专用命令;查询加速列表查询使用 `guancli metric accelerate list <metric_id>`。查询加速筛选条件使用和指标定义一致的 `WebFilterData` 结构,可放在完整请求体的 `filter` 字段,也可通过 `--filter-json` 或 `--filter-file` 传入;合法节点仅为 `condition/operator/composition`,禁止使用 `group/combineType/conditions`。查询加速无法从指标适用维度自动补齐另一数据集的字段元数据,因此条件必须显式提供 `dsId/fdId/name/fdType/metaType`;清空筛选条件可传 `--filter-json null`。完整契约和嵌套示例见 `metric-common-fields.md`。
96
129
  指标树及其目录的创建、修改和删除已封装为 `metric-tree` 专用命令,优先使用专用命令,不要再用裸 `fetch` 直接调用写接口。构造或解析指标树及目录的请求时,先读取 `metric-tree-api-contracts.md`;详情/搜索名称字段是 `metricTreeName`,父目录列表及编辑返回对象名称字段是 `name`,目录树用 `id/name`、目录详情用 `dirId/dirName`,读模型不得整体回写。指标树和目录删除默认只展示详情,明确追加 `--confirm` 才提交。指标树目录必须用 `metric-tree directory` 管理并固定 `dirType=METRIC_TREE`;不要把 `METRIC_PLATFORM` 指标目录 ID 当作指标树目录 ID。用户给出指标拆解公式时,按 `metric-tree-mutation.md` 解析成 `metricRelationTree`;遇到复合指标时读取指标详情中的 `formula`,按规则递归展开或在计划中标注不能无损展开的原因。
@@ -119,7 +152,20 @@ guanmetric fetch POST /api/metric-platform/<unwrapped_endpoint> --file request.j
119
152
  - `metric-flow.md`
120
153
  - `metric-flow-generate-example.json`
121
154
  - `metric-flow-generate.schema.json`
155
+ - `metric-flow-export-example.json`
156
+ - `metric-flow-export.schema.json`
157
+ - `metric-flow-handoff-example.json`
158
+ - `metric-flow-handoff.schema.json`
159
+ - `metric-dsl-dependencies-example.json`
160
+ - `metric-dsl-dependencies.schema.json`
161
+ - `metric-dsl-document-output-example.json`
162
+ - `metric-dsl-document-output.schema.json`
163
+ - `metric-dsl-selection-example.json`
164
+ - `metric-dsl-selection.schema.json`
165
+ - `metric-dsl-generate-example.json`
166
+ - `metric-dsl-generate.schema.json`
122
167
  - `metric-validation-ownership.md`
168
+ - `metric-dsl.md`
123
169
  - `metric-api-contracts.md`
124
170
  - `metric-api-contract-examples.json`
125
171
  - `metric-api-contract.schema.json`
@@ -86,6 +86,13 @@
86
86
  编辑接口接收完整更新写模型,不是 JSON Merge Patch。`guanmetric edit` 可以让用户提供 patch,
87
87
  但 CLI 必须先把详情读模型规范化成完整写模型,再合并 patch。
88
88
 
89
+ 业务属性配置由既有 `GET /api/metric-attr-config` 读取,代理还原后的 Spring 路由为
90
+ `GET /metric-attr-config`,`MetricAttrConfigNgController.query` 要求 `BASE / READ`。DSL init
91
+ 只把启用配置的 id、显示名称、类型、默认值和枚举选项保存为本地快照;创建请求仍只提交
92
+ `bizAttrs: [{id,values}]`。现有 plan 会读取当前配置执行前端差集校验,服务端负责最终复核。
93
+ 该读取的 PAT 有权、无权和
94
+ 资源不存在对照尚未执行,不能由本地测试替代。
95
+
89
96
  ## 原子指标
90
97
 
91
98
  ### 字段来源模式
@@ -0,0 +1,30 @@
1
+ {
2
+ "schemaVersion": "1.0",
3
+ "dslVersion": "1.0",
4
+ "acceptance": "usable",
5
+ "defaults": {
6
+ "projectId": "project-1",
7
+ "directoryId": "directory-1",
8
+ "businessOwnerId": "current"
9
+ },
10
+ "summary": {
11
+ "createCount": 2,
12
+ "editCount": 1,
13
+ "datasetCount": 1,
14
+ "remoteMetricCount": 1,
15
+ "edgeCount": 3
16
+ },
17
+ "nodes": [
18
+ {"id": "create:sales", "kind": "create", "key": "sales", "name": "销售额", "subType": "ATOMIC"},
19
+ {"id": "create:sales_ratio", "kind": "create", "key": "sales_ratio", "name": "销售占比", "subType": "COMPOSITE"},
20
+ {"id": "dataset:dataset-sales", "kind": "dataset", "datasetId": "dataset-sales"},
21
+ {"id": "edit:metric-profit", "kind": "edit", "metricId": "metric-profit", "subType": "DERIVED", "operationCount": 1},
22
+ {"id": "remoteMetric:metric-cost", "kind": "remoteMetric", "metricId": "metric-cost"}
23
+ ],
24
+ "edges": [
25
+ {"from": "create:sales", "to": "create:sales_ratio", "relation": "localMetric"},
26
+ {"from": "dataset:dataset-sales", "to": "create:sales", "relation": "datasetSource"},
27
+ {"from": "remoteMetric:metric-cost", "to": "edit:metric-profit", "relation": "remoteMetric"}
28
+ ],
29
+ "createOrder": ["sales", "sales_ratio"]
30
+ }
@@ -0,0 +1,66 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://guandata.dev/schemas/guanmetric/metric-dsl-dependencies-1.0.json",
4
+ "title": "guanmetric DSL dependency document v1.0",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "dslVersion", "acceptance", "defaults", "summary", "nodes", "edges", "createOrder"],
8
+ "properties": {
9
+ "schemaVersion": {"const": "1.0"},
10
+ "dslVersion": {"const": "1.0"},
11
+ "acceptance": {"enum": ["usable", "draft"]},
12
+ "defaults": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "required": ["projectId", "directoryId", "businessOwnerId"],
16
+ "properties": {
17
+ "projectId": {"type": "string"},
18
+ "directoryId": {"type": "string"},
19
+ "businessOwnerId": {"type": "string"}
20
+ }
21
+ },
22
+ "summary": {"$ref": "#/$defs/summary"},
23
+ "nodes": {"type": "array", "items": {"$ref": "#/$defs/node"}},
24
+ "edges": {"type": "array", "items": {"$ref": "#/$defs/edge"}},
25
+ "createOrder": {"type": "array", "items": {"type": "string"}}
26
+ },
27
+ "$defs": {
28
+ "summary": {
29
+ "type": "object",
30
+ "additionalProperties": false,
31
+ "required": ["createCount", "editCount", "datasetCount", "remoteMetricCount", "edgeCount"],
32
+ "properties": {
33
+ "createCount": {"type": "integer", "minimum": 0},
34
+ "editCount": {"type": "integer", "minimum": 0},
35
+ "datasetCount": {"type": "integer", "minimum": 0},
36
+ "remoteMetricCount": {"type": "integer", "minimum": 0},
37
+ "edgeCount": {"type": "integer", "minimum": 0}
38
+ }
39
+ },
40
+ "node": {
41
+ "type": "object",
42
+ "additionalProperties": false,
43
+ "required": ["id", "kind"],
44
+ "properties": {
45
+ "id": {"type": "string", "minLength": 1},
46
+ "kind": {"enum": ["create", "edit", "dataset", "remoteMetric"]},
47
+ "key": {"type": "string"},
48
+ "metricId": {"type": "string"},
49
+ "datasetId": {"type": "string"},
50
+ "name": {"type": "string"},
51
+ "subType": {"enum": ["ATOMIC", "COMPOSITE", "DERIVED"]},
52
+ "operationCount": {"type": "integer", "minimum": 1}
53
+ }
54
+ },
55
+ "edge": {
56
+ "type": "object",
57
+ "additionalProperties": false,
58
+ "required": ["from", "to", "relation"],
59
+ "properties": {
60
+ "from": {"type": "string", "minLength": 1},
61
+ "to": {"type": "string", "minLength": 1},
62
+ "relation": {"enum": ["datasetSource", "localMetric", "remoteMetric"]}
63
+ }
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "schemaVersion": "1.0",
3
+ "command": "dsl.document",
4
+ "valid": true,
5
+ "outputDir": ".guanmetric/docs",
6
+ "files": [
7
+ ".guanmetric/docs/metric-dependencies.json",
8
+ ".guanmetric/docs/metric-documentation.md"
9
+ ],
10
+ "summary": {
11
+ "createCount": 2,
12
+ "editCount": 1,
13
+ "datasetCount": 1,
14
+ "remoteMetricCount": 1,
15
+ "edgeCount": 3
16
+ },
17
+ "diagnostics": []
18
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://guandata.dev/schemas/guanmetric/metric-dsl-document-output-1.0.json",
4
+ "title": "guanmetric dsl document output v1.0",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "command", "valid", "files", "summary", "diagnostics"],
8
+ "properties": {
9
+ "schemaVersion": {"const": "1.0"},
10
+ "command": {"const": "dsl.document"},
11
+ "valid": {"type": "boolean"},
12
+ "outputDir": {"type": "string"},
13
+ "files": {"type": "array", "items": {"type": "string"}},
14
+ "summary": {"$ref": "metric-dsl-dependencies.schema.json#/$defs/summary"},
15
+ "diagnostics": {
16
+ "type": "array",
17
+ "items": {
18
+ "type": "object",
19
+ "additionalProperties": false,
20
+ "required": ["code", "severity", "message"],
21
+ "properties": {
22
+ "code": {"type": "string"},
23
+ "severity": {"enum": ["error", "warning"]},
24
+ "file": {"type": "string"},
25
+ "line": {"type": "integer", "minimum": 1},
26
+ "column": {"type": "integer", "minimum": 1},
27
+ "metricKey": {"type": "string"},
28
+ "field": {"type": "string"},
29
+ "message": {"type": "string"}
30
+ }
31
+ }
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,57 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://guandata.dev/schemas/guanmetric/metric-dsl-generate-1.1.json",
4
+ "title": "guanmetric dsl generate bundle output 1.1",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["schemaVersion", "command", "generated", "complete", "metricCount", "files", "diagnostics"],
8
+ "properties": {
9
+ "schemaVersion": { "const": "1.1" },
10
+ "command": { "const": "dsl.generate" },
11
+ "generated": { "type": "boolean" },
12
+ "complete": { "type": "boolean" },
13
+ "projectDir": { "type": "string", "minLength": 1 },
14
+ "metricCount": { "type": "integer", "minimum": 0 },
15
+ "files": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true },
16
+ "projects": {
17
+ "type": "array",
18
+ "items": {
19
+ "type": "object",
20
+ "additionalProperties": false,
21
+ "required": ["key", "projectDir", "projectId", "directoryId", "acceptance", "metricCount", "files"],
22
+ "properties": {
23
+ "key": { "type": "string", "pattern": "^project_[0-9]{3}$" },
24
+ "projectDir": { "type": "string", "minLength": 1 },
25
+ "projectId": { "type": "string", "minLength": 1 },
26
+ "directoryId": { "type": "string", "minLength": 1 },
27
+ "acceptance": { "enum": ["usable", "draft"] },
28
+ "metricCount": { "type": "integer", "minimum": 1 },
29
+ "files": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true }
30
+ }
31
+ }
32
+ },
33
+ "diagnostics": {
34
+ "type": "array",
35
+ "items": {
36
+ "type": "object",
37
+ "additionalProperties": false,
38
+ "required": ["code", "severity", "message"],
39
+ "properties": {
40
+ "code": { "type": "string", "minLength": 1 },
41
+ "severity": { "enum": ["error", "warning"] },
42
+ "projectKey": { "type": "string", "pattern": "^project_[0-9]{3}$" },
43
+ "sheet": { "type": "string" },
44
+ "row": { "type": "integer", "minimum": 1 },
45
+ "field": { "type": "string" },
46
+ "message": { "type": "string" }
47
+ }
48
+ }
49
+ }
50
+ },
51
+ "allOf": [
52
+ {
53
+ "if": { "properties": { "generated": { "const": true } }, "required": ["generated"] },
54
+ "then": { "required": ["projectDir", "projects"], "properties": { "projects": { "minItems": 1 } } }
55
+ }
56
+ ]
57
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "schemaVersion": "1.1",
3
+ "command": "dsl.generate",
4
+ "generated": true,
5
+ "complete": false,
6
+ "projectDir": "metric-bundle",
7
+ "metricCount": 2,
8
+ "files": [
9
+ "guanmetric-dsl-bundle.json",
10
+ "project_001/atomic_metric_001.js",
11
+ "project_001/metric.config.js"
12
+ ],
13
+ "projects": [
14
+ {
15
+ "key": "project_001",
16
+ "projectDir": "metric-bundle/project_001",
17
+ "projectId": "project-1",
18
+ "directoryId": "dir-1",
19
+ "acceptance": "usable",
20
+ "metricCount": 1,
21
+ "files": ["atomic_metric_001.js", "metric.config.js"]
22
+ }
23
+ ],
24
+ "diagnostics": [
25
+ {
26
+ "code": "FLOW_FILTER_INVALID",
27
+ "severity": "error",
28
+ "projectKey": "project_002",
29
+ "sheet": "原子指标",
30
+ "row": 3,
31
+ "field": "业务限定",
32
+ "message": "筛选字段与指标数据集不一致"
33
+ }
34
+ ]
35
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "schemaVersion": "1.0",
3
+ "command": "dsl.generate",
4
+ "generated": true,
5
+ "projectDir": "metric-project",
6
+ "metricCount": 3,
7
+ "files": [
8
+ ".guanmetric/catalog-manifest.json",
9
+ "atomic_metric_001.js",
10
+ "catalog.biz-attrs.js",
11
+ "catalog.context.js",
12
+ "catalog.datasets.js",
13
+ "catalog.metrics.js",
14
+ "composite_metric_002.js",
15
+ "derived_metric_003.js",
16
+ "metric.config.js"
17
+ ],
18
+ "diagnostics": []
19
+ }
@@ -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,6 @@
1
+ {
2
+ "operation": "create",
3
+ "requested": ["sales_ratio"],
4
+ "resolved": ["sales", "sales_ratio"],
5
+ "autoIncluded": ["sales"]
6
+ }
@@ -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 reconcile` 也在读取 state 或访问 BI 前取得锁;三者对同一个 state 路径持有完整生命周期的跨进程独占锁,避免两个 Agent 同时创建或互相覆盖状态。apply preview 不读取 state,因此不持锁。锁文件固定为 `<state>.lock`,只是 OS advisory lock 的稳定载体,可以长期保留;进程正常退出或崩溃时锁由操作系统自动释放,不依赖删除文件判断是否过期。锁已被占用时命令直接失败,不降级成无锁执行。WASI/非原生运行时没有可审计的跨 guest 文件锁契约,因此明确拒绝这三个生命周期命令,只保留 `flow generate`、`flow plan` 等只读能力。
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