@zhuoyuezs/ml-platform 0.1.4 → 0.1.6

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/CI-ARTIFACT.md ADDED
@@ -0,0 +1,21 @@
1
+ # CI npm artifact
2
+
3
+ 每次 `dev`/`main` 分支更新、合并请求或从 GitLab UI 手动运行的 Pipeline 都会生成完整的
4
+ `@zhuoyuezs/ml-platform` tarball。
5
+ GitLab pipeline 的 `npm:ml-platform-release` job 会将 `business-client-js` 放入正式包的
6
+ `runtime/business-client`,并上传已验收的 `.tgz` 与 `.sha256` 文件。
7
+
8
+ 创建与 `npm/ml-platform/package.json` 版本一致的 SemVer tag(例如 `v0.1.5`)后,
9
+ 同一个已验收归档会自动发布到 npmjs。发布 job 需要在 GitLab CI/CD Variables 中配置
10
+ 受保护且 masked 的 `NPM_TOKEN`;普通分支、手动 Pipeline 和 Merge Request 不会发布 npm。
11
+
12
+ 下载 artifact 后只需要 Node.js 18+:
13
+
14
+ ```bash
15
+ sha256sum -c zhuoyuezs-ml-platform-*.tgz.sha256
16
+ npm install --global ./zhuoyuezs-ml-platform-*.tgz
17
+ ml-platform version
18
+ ```
19
+
20
+ 运行已安装的 CLI 不需要 Python、`uv`、Git 仓库或平台源码。`uv` 只在 CI 构建阶段用于
21
+ 执行现有的 release staging 脚本。
package/DEVELOPMENT.md CHANGED
@@ -14,7 +14,8 @@
14
14
  用户级 Skill,不要求额外安装参数。
15
15
  - 管理命令默认输出简洁结果;Agent 和 CI 显式使用 `--json` 获取稳定结构化输出。
16
16
  - npm `postinstall` 不得修改 Agent Skills 目录。
17
- - Python business client 只保留为迁移 oracle,不进入 release
17
+ - Python business client 与迁移 oracle 已删除;release builder 继续防御性排除 Python
18
+ runtime、wheel 和旧 wrapper,npm 包只携带 JavaScript CLI 与纯 Skill payload。
18
19
  - Codex CLI 不重新打包;当前只支持唯一自研 Host。
19
20
  - builder 使用 Python,因此开发构建命令仍通过 `uv run ...` 启动;最终 npm 包和用户侧
20
21
  `ml-platform` 运行时只依赖 Node.js,不依赖 Python 或 `uv`。
@@ -48,7 +49,7 @@ package/
48
49
  └── DEVELOPMENT.md
49
50
  ```
50
51
 
51
- 严格 `files` allowlist 必须排除 `node_modules`、测试 oracle、旧 wheel、Skill runtime、旧 wrapper、
52
+ 严格 `files` allowlist 必须排除 `node_modules`、旧 wheel、Skill runtime、旧 wrapper、
52
53
  Host 副本、日志、会话数据、attic、凭证和实验文件。
53
54
 
54
55
  最终发布单元是 builder 生成并通过 smoke test 的 `.tgz`,不是 `dist/package/` 目录。目录只用于
@@ -61,24 +62,25 @@ Host 副本、日志、会话数据、attic、凭证和实验文件。
61
62
 
62
63
  ### Step 1:完成 business-client-js 迁移(已完成)
63
64
 
64
- - 补齐全部 Python CLI 命令和参数契约;
65
+ - 完成原 Python CLI 命令和参数契约到 JavaScript 的迁移;
65
66
  - 对齐 JSON stdout、stderr 和退出码;
66
67
  - 补齐数值参数、子命令 help、路径展开和 HTTP 错误测试;
67
68
  - 已注册 Dataset 支持 `get-dataset`、`resolve-dataset` 和
68
69
  `build-registered-dataset` API-first 流程,不依赖本地 Manifest;
69
70
  - Catalog 规范化保留 `project`,Registry 只读元数据与可发布契约分离,目标 project
70
71
  不一致时在访问 Registry 前失败;
71
- - 扩大 Catalog normalization differential tests;
72
+ - 扩大 Catalog normalization OpenAPI contract tests;
72
73
  - 完成 Job、Artifact、inference 和 Operator upload 负向测试;
73
74
  - 删除正式运行路径中的旧产品名兼容;
74
75
  - 独立执行 `npm pack --json --dry-run` 机器可读清单校验、clean install 和 CLI smoke test。
75
76
 
76
- 完成标准:Python 只剩 oracle 作用,正式 CLI 不需要 Python、`uv` 或第三方 npm runtime 依赖。
77
+ 完成标准:正式 CLI 不需要 Python、`uv` 或第三方 npm runtime 依赖,旧 Python client 可删除。
77
78
 
78
79
  ### Step 2:重建纯 feature-management Skill(已完成)
79
80
 
80
81
  - 保留 `SKILL.md`、`agents/`、`references/` 和 `assets/catalog-template/`;
81
- - 删除 `runtime/business-client-js/`、`assets/wheels/`、Skill installer 和 CLI launcher
82
+ - npm staging 时排除 `assets/wheels/`、Skill installer 和 CLI launcher;这些旧 Python
83
+ runtime/installer/zip 交付物随后已从仓库彻底删除,staging 排除逻辑保留为防御性边界。
82
84
  - 所有命令示例改为直接调用 `ml-platform`;
83
85
  - 明确 CLI 是外部 prerequisite,并提供 `ml-platform --help` 自省路径;
84
86
  - 更新 `agents/openai.yaml`;
@@ -134,7 +136,7 @@ Host 副本、日志、会话数据、attic、凭证和实验文件。
134
136
 
135
137
  - `git diff --check -- npm business-client-js skills/feature-management agent_poc/codex-host`;
136
138
  - JS business-client tests;
137
- - Python oracle tests;
139
+ - JavaScript OpenAPI contract 与 Catalog tests;
138
140
  - Skill validator 和 forward tests;
139
141
  - npm installer tests;
140
142
  - release builder;
@@ -146,8 +148,9 @@ Host 副本、日志、会话数据、attic、凭证和实验文件。
146
148
  - Codex project/user scope 和显式 Skills 目录;
147
149
  - 最终归档敏感文件和重复 client 检查。
148
150
 
149
- 完成标准:同一个不可变 `.tgz` 通过全部验收,并作为唯一允许发布的输入。没有明确发布授权时,
150
- 仍不执行 npmjs 或其他外部发布。
151
+ 完成标准:同一个不可变 `.tgz` 通过全部验收,并作为唯一允许发布的输入。GitLab CI 仅在
152
+ SemVer tag(`vX.Y.Z` 或 `X.Y.Z`)与 `package.json.version` 一致、且配置了 `NPM_TOKEN` 时
153
+ 自动执行 npmjs 发布;dev、main 和 Merge Request pipeline 只生成并保存 artifact。
151
154
 
152
155
  ## 4. package.json
153
156
 
@@ -260,9 +263,10 @@ npx @zhuoyuezs/ml-platform@<version> install
260
263
  - 不执行 GitHub Release 或其他未授权的外部发布;
261
264
  - candidate 验证后不得用相同版本重新构建不同内容。
262
265
 
263
- 当前允许使用 npm 账号 2FA 完成人工发布。后续迁移到受控 CI 时,优先使用 npm Trusted
264
- Publishing OIDC,并在受支持的公开 CI 环境执行 `npm publish --provenance`。本地登录不能
265
- 替代 CI provenance,未建立可信发布工作流前不得宣称包具有对应来源证明。
266
+ 受控 GitLab CI 使用 masked/protected `NPM_TOKEN` 临时配置 npm auth,并发布 builder 生成的
267
+ 同一个 `.tgz`;token 不写入仓库或归档。本地登录不能替代 CI provenance,当前 token 发布也不
268
+ 宣称包具有 provenance 来源证明。后续可迁移到 npm Trusted Publishing 和 OIDC,并在受支持的
269
+ 公开 CI 环境执行 `npm publish --provenance`。
266
270
 
267
271
  ## 9. 参考与取舍
268
272
 
package/README.md CHANGED
@@ -120,7 +120,7 @@ npx --yes --registry=https://registry.npmjs.org/ \
120
120
  ## 包内容
121
121
 
122
122
  - 一份 JavaScript business CLI;
123
- - 一份纯 `feature-management` Skill;
123
+ - 两份纯 Skill:`feature-management` 与 `model-lifecycle-management`;
124
124
  - 显式安装、升级、状态、校验和回滚逻辑。
125
125
 
126
126
  不包含 Codex CLI、Python business-client wheel、Agent Host、平台服务端、模型训练或 Kubernetes
package/checksums.json CHANGED
@@ -2,28 +2,28 @@
2
2
  "files": [
3
3
  {
4
4
  "path": "runtime/business-client/README.md",
5
- "sha256": "sha256:4bca6d490a0ac3de54accb8f9f43003cdfb0c187bcf1acefea48728b16001e94",
6
- "size_bytes": 1233
5
+ "sha256": "sha256:b12f30609c756ac4473723df434e6b1055f8713b7acfc3278b091b52288f6347",
6
+ "size_bytes": 2459
7
7
  },
8
8
  {
9
9
  "path": "runtime/business-client/package-lock.json",
10
- "sha256": "sha256:a253eb945f8666779adfbf9e7009c539b450746298f145bbe4d0b2ca5f4cbe35",
10
+ "sha256": "sha256:d5e7102ec51df3192ae901908f36cf0380aba5a0421b9824877403ab034d6740",
11
11
  "size_bytes": 381
12
12
  },
13
13
  {
14
14
  "path": "runtime/business-client/package.json",
15
- "sha256": "sha256:bf29516fcbd162fe051f6ea142bd604dead0be99c461bc184a59e7c526766aa4",
16
- "size_bytes": 426
15
+ "sha256": "sha256:9e815ffe0487eb216447f4a4b31d248fe37392e08f0fd4bae2aba9c4f4285547",
16
+ "size_bytes": 501
17
17
  },
18
18
  {
19
19
  "path": "runtime/business-client/src/catalog.js",
20
- "sha256": "sha256:afc2ba28eee2d1dcd833a2e5c4ac923421dc60302b31bf985aa638849bbf3d36",
21
- "size_bytes": 54246
20
+ "sha256": "sha256:8dcc4c4baff7f292a6fb09d6f82693b5ce7da16b9f0a9e03b22e1f18e893240f",
21
+ "size_bytes": 53659
22
22
  },
23
23
  {
24
24
  "path": "runtime/business-client/src/cli.js",
25
- "sha256": "sha256:db25cecb760a2b94ac1433c1d04ab70b429825e78dd1168e72688d8b9a8556be",
26
- "size_bytes": 25911
25
+ "sha256": "sha256:ec2b5366dff1da0e56738e2ba39284701940e6a4574e11529d77aed8e4fe0d61",
26
+ "size_bytes": 41201
27
27
  },
28
28
  {
29
29
  "path": "runtime/business-client/src/config.js",
@@ -32,13 +32,13 @@
32
32
  },
33
33
  {
34
34
  "path": "runtime/business-client/src/http.js",
35
- "sha256": "sha256:95ac9342c09605966f055dde2ac5cd67fdf28e8bc877e2233e3c48077ffdd729",
36
- "size_bytes": 12429
35
+ "sha256": "sha256:e515b39f19e7b5e7d6c2388e9cf2d8e2c9fd18b585bb53bd4f8eeeb7412af81c",
36
+ "size_bytes": 15192
37
37
  },
38
38
  {
39
39
  "path": "skills/feature-management/SKILL.md",
40
- "sha256": "sha256:69842537b8df63e58630ca0581575f29f6ec9e97d22c9c2c30b1575270161f6e",
41
- "size_bytes": 28100
40
+ "sha256": "sha256:ba725b6be2d0f8e4d741d3a6f96e64657112c15935f8e9fd2818cd0b17380816",
41
+ "size_bytes": 30428
42
42
  },
43
43
  {
44
44
  "path": "skills/feature-management/agents/openai.yaml",
@@ -92,8 +92,8 @@
92
92
  },
93
93
  {
94
94
  "path": "skills/feature-management/references/commands.md",
95
- "sha256": "sha256:c20ccf9ad8d505a1880a7e35773edb5040bbbcfeba3a64316e3148a6e337bdc2",
96
- "size_bytes": 11546
95
+ "sha256": "sha256:c748f1877f6d2ac1f6ecc29e45cd270997cbd35b498a190decddb66c74b840ab",
96
+ "size_bytes": 13751
97
97
  },
98
98
  {
99
99
  "path": "skills/feature-management/references/contracts.md",
@@ -107,8 +107,33 @@
107
107
  },
108
108
  {
109
109
  "path": "skills/feature-management/references/platform-capability-guide.md",
110
- "sha256": "sha256:fb606c3637891dab462d5b94004b60a3b8f02eb178aef589716f968c3b87cd5e",
111
- "size_bytes": 2627
110
+ "sha256": "sha256:40d0f43a30688c5852ef09203ec35b4dc2c6b9db4bc7dd26dfae52e12d10e9ee",
111
+ "size_bytes": 4257
112
+ },
113
+ {
114
+ "path": "skills/model-lifecycle-management/SKILL.md",
115
+ "sha256": "sha256:5827d5e2ae6bc4561150e7c2e435a6ed657688d48aeab202ec9cf3a9b8f5e8e6",
116
+ "size_bytes": 2790
117
+ },
118
+ {
119
+ "path": "skills/model-lifecycle-management/agents/openai.yaml",
120
+ "sha256": "sha256:37857849a8baa6b06b6d5d07ae6036080239356fcccd36f77bbdc52c1dfa283f",
121
+ "size_bytes": 249
122
+ },
123
+ {
124
+ "path": "skills/model-lifecycle-management/references/evaluation.md",
125
+ "sha256": "sha256:0139e5fc79beeab84fb31792bdac37c3331676e1f591c57390ecc6336873dcc9",
126
+ "size_bytes": 1797
127
+ },
128
+ {
129
+ "path": "skills/model-lifecycle-management/references/packaging.md",
130
+ "sha256": "sha256:c987d4095f23a1f18ba55effb14f946e0f4cdcb661d7af28af560cc8f72d8b2c",
131
+ "size_bytes": 1101
132
+ },
133
+ {
134
+ "path": "skills/model-lifecycle-management/references/training.md",
135
+ "sha256": "sha256:84426c5cfbc60db1ea8b1692f270da82e16700b057af7dd826907acfb3b12e3f",
136
+ "size_bytes": 1125
112
137
  }
113
138
  ],
114
139
  "schema_version": "data_platform.ml_platform_checksums/v1"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhuoyuezs/ml-platform",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "ML 数据平台 business CLI 与 Agent Skills 管理工具",
5
5
  "bin": {
6
6
  "ml-platform": "scripts/main.js"
@@ -13,10 +13,13 @@
13
13
  "release.json",
14
14
  "checksums.json",
15
15
  "README.md",
16
- "DEVELOPMENT.md"
16
+ "DEVELOPMENT.md",
17
+ "CI-ARTIFACT.md"
17
18
  ],
18
19
  "scripts": {
19
- "test": "node --test test/*.test.js"
20
+ "test": "node --test test/*.test.js",
21
+ "api:check": "node ../../scripts/check_api_contract.js",
22
+ "pack": "npm pack"
20
23
  },
21
24
  "engines": {
22
25
  "node": ">=18"
package/release.json CHANGED
@@ -13,8 +13,8 @@
13
13
  "entrypoint": "src/cli.js",
14
14
  "name": "ml-platform",
15
15
  "path": "runtime/business-client",
16
- "sha256": "sha256:bf935c8c82aa508c26afa4f76dc94dd3d9e23c6e2bb33d797190d548a0c98249",
17
- "version": "0.6.0"
16
+ "sha256": "sha256:28a74ef7e721f3de925384b249588a435c11b6995df92e3be96209682ca81041",
17
+ "version": "0.7.0"
18
18
  },
19
19
  "policy_sha256": "sha256:02fbde0134696b78c0c365d6e0b76c9814227208da5b2b3d4e5dbd82dc4a3606",
20
20
  "public_schema_versions": [
@@ -25,7 +25,7 @@
25
25
  "ml_data_platform.feature_set/v1",
26
26
  "ml_data_platform.dataset_manifest/v1"
27
27
  ],
28
- "release_version": "0.1.4",
28
+ "release_version": "0.1.6",
29
29
  "runtime_requirements": {
30
30
  "node": ">=18",
31
31
  "os": [
@@ -37,10 +37,16 @@
37
37
  "skills": {
38
38
  "feature-management": {
39
39
  "path": "skills/feature-management",
40
- "requires_cli": ">=0.6.0 <0.7.0",
41
- "revision": "0.1.4",
42
- "sha256": "sha256:0ae088bcfbb4b9a0fcfa22752bd978b2a34ab033d63af4ebb4f641d3c2ac65fd"
40
+ "requires_cli": ">=0.7.0 <0.8.0",
41
+ "revision": "0.1.6",
42
+ "sha256": "sha256:15131c1303b143373e4b8671301f5b884013a1906a44cfc85dd6f93f43c5d8c8"
43
+ },
44
+ "model-lifecycle-management": {
45
+ "path": "skills/model-lifecycle-management",
46
+ "requires_cli": ">=0.7.0 <0.8.0",
47
+ "revision": "0.1.6",
48
+ "sha256": "sha256:4107926beb85882c38333310e951a4a4cfcdcf946875359a7b5c1a55fb406347"
43
49
  }
44
50
  },
45
- "source_commit": "2f92b97d658f16ebc6f2081b07a56365aeced649"
51
+ "source_commit": "d3e7aa1256c18453ba59f2a624b4cdc6dbbf9421"
46
52
  }
@@ -1,6 +1,7 @@
1
1
  # Business Client JS
2
2
 
3
- `business-client/` JavaScript 迁移实现。Python client 目前仅作为迁移期 parity oracle;
3
+ 工业时序智能建模平台唯一受支持的业务 CLI API client 实现。客户端合同由平台
4
+ OpenAPI 快照、JavaScript 单元测试和 npm 发布测试直接验证;
4
5
  本目录是业务 CLI 的唯一 JavaScript 实现源码,Skill 通过外部 `ml-platform` 命令调用它,
5
6
  不复制或内嵌本目录的 runtime。
6
7
 
@@ -25,3 +26,31 @@ ml-platform build-registered-dataset <dataset_id> <dataset_version> \
25
26
 
26
27
  `resolve-manifest` 和 `build-dataset <manifest.json>` 仍用于尚未注册的本地契约。Catalog
27
28
  normalizer 保留并校验非默认 `project`;`apply --project` 与 Catalog 内项目不一致时拒绝执行。
29
+
30
+ Registry 资产详情和版本发现使用与 `dev` OpenAPI 一致的命令:
31
+
32
+ ```bash
33
+ ml-platform get-parameter <name> <version> --project <project>
34
+ ml-platform get-feature <name> <version> --project <project>
35
+ ml-platform list-feature-versions <name> --project <project> --all
36
+ ml-platform list-jobs --project <project> --status succeeded --all
37
+ ```
38
+
39
+ 平台资源生命周期与部署绑定的因果上下文读取也由同一 CLI 覆盖:
40
+
41
+ ```bash
42
+ ml-platform list-parameter-references <name> <version> --project <project>
43
+ ml-platform get-parameter-lifecycle <name> <version> --project <project>
44
+ ml-platform fetch-inference-context /absolute/path/to/request.json
45
+ ```
46
+
47
+ Parameter、Feature、FeatureSet 的删除命令执行软删除,并支持带 actor、reason 和幂等键的
48
+ 恢复;调用方应在任何变更前查询直接与传递引用。
49
+
50
+ 仓库中的 `api-spec/latest.json` 从 `origin/dev` 生成。同步或验证契约时运行:
51
+
52
+ ```bash
53
+ uv run python scripts/generate_openapi_schema.py --ref origin/dev
54
+ uv run python scripts/generate_openapi_schema.py --ref origin/dev --check
55
+ npm run api:check --prefix business-client-js
56
+ ```
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@zhuoyuezs/ml-platform-business-client",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@zhuoyuezs/ml-platform-business-client",
9
- "version": "0.6.0",
9
+ "version": "0.7.0",
10
10
  "license": "UNLICENSED",
11
11
  "bin": {
12
12
  "ml-platform": "src/cli.js"
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@zhuoyuezs/ml-platform-business-client",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "private": true,
5
- "description": "JavaScript parity implementation of the ML Platform business client",
5
+ "description": "JavaScript implementation of the ML Platform business client",
6
6
  "bin": {
7
7
  "ml-platform": "src/cli.js"
8
8
  },
@@ -11,7 +11,9 @@
11
11
  "README.md"
12
12
  ],
13
13
  "scripts": {
14
- "test": "node --test test/*.test.js"
14
+ "test": "node --test test/*.test.js",
15
+ "api:check": "node ../scripts/check_api_contract.js",
16
+ "pack": "npm pack"
15
17
  },
16
18
  "engines": {
17
19
  "node": ">=18"
@@ -58,7 +58,6 @@ function normalizeQualityRules(value) { exactKeys(value, ["valid_range", "allow_
58
58
  function normalizeCodeArtifact(value) { object(value, "operator code_artifact"); const sha256 = requiredString(value.sha256, "code_artifact sha256"); if (!wheelDigestPattern.test(sha256)) throw new Error("code_artifact sha256 is invalid"); const filename = requiredString(value.filename, "code_artifact filename"); if (path.basename(filename) !== filename || !wheelFilenamePattern.test(filename)) throw new Error("code_artifact filename is invalid"); if (!Number.isInteger(value.size_bytes) || value.size_bytes <= 0) throw new Error("code_artifact size_bytes must be a positive integer"); if (defaultIfAbsent(value, "format", "wheel") !== "wheel") throw new Error("code_artifact format must be wheel"); return { uri: requiredString(value.uri, "code_artifact uri"), sha256, format: "wheel", filename, size_bytes: value.size_bytes, python_version: optionalString(value.python_version, "code_artifact python_version"), dependency_lock_hash: optionalString(value.dependency_lock_hash, "code_artifact dependency_lock_hash") }; }
59
59
 
60
60
  function normalizeParameter(payload) {
61
- exactKeys(payload, ["schema_version", "project", "name", "display_name", "version", "data_type", "unit", "expected_frequency", "source", "time_semantics", "availability_sla", "value_field", "quality_rules", "rounding", "owner"], ["name", "display_name", "version", "source"], "parameter");
62
61
  const source = object(payload.source, "parameter source");
63
62
  if (Object.prototype.hasOwnProperty.call(source, "parameters")) throw new Error("parameter source uses unsupported field 'parameters'; use 'params'");
64
63
  if (source.mode === "sql" && typeof source.sql === "string" && source.sql.trimEnd().endsWith(";")) throw new Error("SQL Parameter source.sql must not end with a semicolon");
@@ -66,7 +65,7 @@ function normalizeParameter(payload) {
66
65
  return { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.parameter/v1")), project: identifier(defaultIfAbsent(payload, "project", "default"), "parameter project"), name: identifier(payload.name, "parameter name"), display_name: requiredString(payload.display_name, "parameter display_name"), version: identifier(payload.version, "parameter version"), data_type: pythonString(defaultIfAbsent(payload, "data_type", "time_series")), unit: pythonString(defaultIfAbsent(payload, "unit", "")), expected_frequency: pythonString(defaultIfAbsent(payload, "expected_frequency", "10min")), source: { adapter: pythonString(defaultIfAbsent(source, "adapter", "synthetic")), mode: choice(defaultIfAbsent(source, "mode", "direct_column"), ["direct_column", "sql", "derived"], "parameter source mode"), source_metric: optionalString(source.source_metric, "source_metric"), source_group: optionalString(source.source_group, "source_group"), measurement: optionalString(source.measurement, "measurement"), field: optionalString(source.field, "field"), tags: stringMap(defaultIfAbsent(source, "tags", {}), "source tags"), schema: optionalString(aliasIfAbsent(source, "schema", "schema_name"), "schema"), table: optionalString(source.table, "table"), time_column: optionalString(source.time_column, "time_column"), value_column: optionalString(source.value_column, "value_column"), metric_name_column: optionalString(source.metric_name_column, "metric_name_column"), metric_name: optionalString(source.metric_name, "metric_name"), unit_column: optionalString(source.unit_column, "unit_column"), unit: optionalString(aliasIfAbsent(source, "unit", "source_unit"), "source unit"), filters: stringMap(defaultIfAbsent(source, "filters", {}), "source filters"), sql: optionalString(source.sql, "source sql"), params: object(defaultIfAbsent(source, "params", {}), "source params") }, time_semantics: { event_time_field: pythonString(defaultIfAbsent(time, "event_time_field", "event_time")), ingested_at_field: pythonString(defaultIfAbsent(time, "ingested_at_field", "ingested_at")), timezone: pythonString(defaultIfAbsent(time, "timezone", "Asia/Shanghai")), availability: normalizeAvailability(time.availability) }, availability_sla: normalizeAvailabilitySla(payload.availability_sla), value_field: pythonString(defaultIfAbsent(payload, "value_field", "value")), quality_rules: normalizeQualityRules(object(defaultIfAbsent(payload, "quality_rules", {}), "parameter quality_rules")), rounding: normalizeRounding(payload.rounding), owner: pythonString(defaultIfAbsent(payload, "owner", "demo")) };
67
66
  }
68
67
 
69
- function normalizeOperator(payload) { exactKeys(payload, ["schema_version", "project", "name", "version", "type", "function_hash", "entrypoint", "code_hash", "package_uri", "code_artifact", "input_schema", "output_schema", "config_schema", "runtime", "resources", "deterministic", "supports_batch", "supports_online", "owner"], ["name", "version"], "operator"); const artifact = payload.code_artifact == null ? null : normalizeCodeArtifact(payload.code_artifact); const normalized = { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.operator/v1")), project: identifier(defaultIfAbsent(payload, "project", "default"), "operator project"), name: identifier(payload.name, "operator name"), version: identifier(payload.version, "operator version"), type: choice(defaultIfAbsent(payload, "type", "standard"), ["standard", "custom_python", "spark_udf", "feature"], "operator type"), function_hash: pythonString(defaultIfAbsent(payload, "function_hash", "demo")), entrypoint: optionalString(payload.entrypoint, "operator entrypoint"), code_hash: optionalString(payload.code_hash, "operator code_hash"), package_uri: optionalString(payload.package_uri, "operator package_uri"), code_artifact: artifact, input_schema: object(defaultIfAbsent(payload, "input_schema", {}), "operator input_schema"), output_schema: object(defaultIfAbsent(payload, "output_schema", {}), "operator output_schema"), config_schema: object(defaultIfAbsent(payload, "config_schema", {}), "operator config_schema"), runtime: object(defaultIfAbsent(payload, "runtime", {}), "operator runtime"), resources: object(defaultIfAbsent(payload, "resources", {}), "operator resources"), deterministic: bool(defaultIfAbsent(payload, "deterministic", true), "operator deterministic"), supports_batch: bool(defaultIfAbsent(payload, "supports_batch", true), "operator supports_batch"), supports_online: bool(defaultIfAbsent(payload, "supports_online", false), "operator supports_online"), owner: pythonString(defaultIfAbsent(payload, "owner", "demo")) }; if (artifact && normalized.code_hash !== artifact.sha256) throw new Error("operator code_hash must equal code_artifact.sha256"); if (artifact && normalized.package_uri !== artifact.uri) throw new Error("operator package_uri must equal code_artifact.uri"); return normalized; }
68
+ function normalizeOperator(payload) { object(payload, "operator"); const artifact = payload.code_artifact == null ? null : normalizeCodeArtifact(payload.code_artifact); const normalized = { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.operator/v1")), project: identifier(defaultIfAbsent(payload, "project", "default"), "operator project"), name: identifier(payload.name, "operator name"), version: identifier(payload.version, "operator version"), type: choice(defaultIfAbsent(payload, "type", "standard"), ["standard", "custom_python", "spark_udf", "feature"], "operator type"), function_hash: pythonString(defaultIfAbsent(payload, "function_hash", "demo")), entrypoint: optionalString(payload.entrypoint, "operator entrypoint"), code_hash: optionalString(payload.code_hash, "operator code_hash"), package_uri: optionalString(payload.package_uri, "operator package_uri"), code_artifact: artifact, input_schema: object(defaultIfAbsent(payload, "input_schema", {}), "operator input_schema"), output_schema: object(defaultIfAbsent(payload, "output_schema", {}), "operator output_schema"), config_schema: object(defaultIfAbsent(payload, "config_schema", {}), "operator config_schema"), runtime: object(defaultIfAbsent(payload, "runtime", {}), "operator runtime"), resources: object(defaultIfAbsent(payload, "resources", {}), "operator resources"), deterministic: bool(defaultIfAbsent(payload, "deterministic", true), "operator deterministic"), supports_batch: bool(defaultIfAbsent(payload, "supports_batch", true), "operator supports_batch"), supports_online: bool(defaultIfAbsent(payload, "supports_online", false), "operator supports_online"), owner: pythonString(defaultIfAbsent(payload, "owner", "demo")) }; if (artifact && normalized.code_hash !== artifact.sha256) throw new Error("operator code_hash must equal code_artifact.sha256"); if (artifact && normalized.package_uri !== artifact.uri) throw new Error("operator package_uri must equal code_artifact.uri"); return normalized; }
70
69
  function normalizeRegistryOperator(payload) { const value = object(payload, "operator"); if (!hasOwn(value, "deleted_at")) return normalizeOperator(value); const { deleted_at, ...contract } = value; void deleted_at; return normalizeOperator(contract); }
71
70
  function normalizeFeature(payload) { exactKeys(payload, ["schema_version", "project", "name", "version", "inputs", "operator", "operator_version", "config", "output_column", "output_dtype", "offline_online_supported", "owner", "description"], ["name", "version", "inputs", "operator", "operator_version", "output_column"], "feature"); const project = identifier(defaultIfAbsent(payload, "project", "default"), "feature project"); const inputs = objectList(payload.inputs, "feature inputs").map((item) => parameterRef(item, project)); if (!inputs.length) throw new Error("feature inputs must not be empty"); unique(inputs.map(parameterReferenceKey), "feature inputs"); if (typeof payload.output_column !== "string" || !outputColumnPattern.test(payload.output_column)) throw new Error("feature output_column is invalid"); return { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.feature/v1")), project, name: identifier(payload.name, "feature name"), version: identifier(payload.version, "feature version"), inputs, operator: identifier(payload.operator, "feature operator"), operator_version: identifier(payload.operator_version, "feature operator_version"), config: object(defaultIfAbsent(payload, "config", {}), "feature config"), output_column: payload.output_column, output_dtype: pythonString(defaultIfAbsent(payload, "output_dtype", "float64")), offline_online_supported: bool(defaultIfAbsent(payload, "offline_online_supported", true), "offline_online_supported"), owner: pythonString(defaultIfAbsent(payload, "owner", "demo")), description: pythonString(defaultIfAbsent(payload, "description", "")) }; }
72
71
  function normalizeFeatureSet(payload) { exactKeys(payload, ["schema_version", "project", "name", "version", "features", "owner", "description"], ["name", "version", "features"], "feature set"); const features = objectList(payload.features, "feature set features").map((item) => ref(item, "feature reference")); if (!features.length) throw new Error("feature_set features must not be empty"); unique(features.map(referenceKey), "feature_set features"); return { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.feature_set/v1")), project: identifier(defaultIfAbsent(payload, "project", "default"), "feature set project"), name: identifier(payload.name, "feature set name"), version: identifier(payload.version, "feature set version"), features, owner: pythonString(defaultIfAbsent(payload, "owner", "demo")), description: pythonString(defaultIfAbsent(payload, "description", "")) }; }
@@ -14,10 +14,22 @@ const BUSINESS_COMMANDS = new Set([
14
14
  "configure", "show-config", "health", "list-parameters", "list-operators", "list-features",
15
15
  "list-feature-sets", "list-datasets", "get-dataset", "list-jobs", "resolve-manifest",
16
16
  "resolve-dataset", "build-dataset", "build-registered-dataset",
17
+ "get-parameter", "get-feature", "get-feature-set", "get-operator",
18
+ "delete-parameter", "delete-feature", "delete-feature-set",
19
+ "restore-parameter", "restore-feature", "restore-feature-set",
20
+ "get-parameter-lifecycle", "get-feature-lifecycle", "get-feature-set-lifecycle",
21
+ "list-parameter-references", "list-feature-references", "list-feature-set-references",
22
+ "list-parameter-versions", "list-feature-versions", "list-feature-set-versions", "list-operator-versions",
17
23
  "add-parameter", "add-feature", "add-feature-set", "add-operator", "publish-operator",
18
24
  "add-dataset", "update-dataset", "delete-dataset", "list-dataset-artifacts",
19
25
  "register-operator", "show-operator-specs", "delete-operator", "run-operator",
20
- "get-job", "wait-job", "retry-job", "cancel-job", "get-dataset-artifact", "download-dataset-artifact", "fetch-inference-data", "apply",
26
+ "get-job", "wait-job", "retry-job", "cancel-job", "get-dataset-artifact", "download-dataset-artifact", "fetch-inference-data", "fetch-inference-context", "apply",
27
+ "register-trainer", "register-training-run", "validate-training-run", "submit-training-job", "get-training-job", "retry-training-job", "cancel-training-job",
28
+ "register-evaluation-config", "get-evaluation-config", "register-metric-definition", "get-metric-definition",
29
+ "register-executable-package", "verify-executable-package", "publish-executable-package",
30
+ "validate-evaluation-run", "submit-evaluation", "get-evaluation-run", "get-evaluation-job", "retry-evaluation-job", "cancel-evaluation-job", "list-evaluation-attempts",
31
+ "get-evaluation-result", "get-evaluation-result-metrics", "get-evaluation-result-artifacts", "summarize-evaluation-run", "get-evaluation-summary",
32
+ "create-model-package", "get-model-package", "cancel-model-package", "validate-model-artifact",
21
33
  ]);
22
34
 
23
35
  const COMMAND_USAGE = {
@@ -29,11 +41,31 @@ const COMMAND_USAGE = {
29
41
  "list-projects": "list-projects",
30
42
  "get-project": "get-project NAME",
31
43
  "delete-project": "delete-project NAME",
32
- "list-parameters": "list-parameters [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
33
- "list-operators": "list-operators [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
34
- "list-features": "list-features [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
35
- "list-feature-sets": "list-feature-sets [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
36
- "list-datasets": "list-datasets [--project PROJECT] [-q QUERY] [--limit N] [--offset N] [--include-deleted]",
44
+ "list-parameters": "list-parameters [--project PROJECT] [--name NAME] [--version VERSION] [-q QUERY] [--limit N] [--offset N] [--all]",
45
+ "list-operators": "list-operators [--project PROJECT] [--name NAME] [--version VERSION] [-q QUERY] [--limit N] [--offset N] [--all]",
46
+ "list-features": "list-features [--project PROJECT] [--name NAME] [--version VERSION] [-q QUERY] [--limit N] [--offset N] [--all]",
47
+ "list-feature-sets": "list-feature-sets [--project PROJECT] [--name NAME] [--version VERSION] [-q QUERY] [--limit N] [--offset N] [--all]",
48
+ "list-datasets": "list-datasets [--project PROJECT] [--dataset-id ID] [--dataset-version VERSION] [-q QUERY] [--limit N] [--offset N] [--include-deleted] [--all]",
49
+ "get-parameter": "get-parameter NAME VERSION [--project PROJECT]",
50
+ "get-feature": "get-feature NAME VERSION [--project PROJECT]",
51
+ "get-feature-set": "get-feature-set NAME VERSION [--project PROJECT]",
52
+ "get-operator": "get-operator NAME VERSION [--project PROJECT]",
53
+ "delete-parameter": "delete-parameter NAME VERSION [--project PROJECT] [--actor ACTOR] [--reason TEXT] [--idempotency-key KEY]",
54
+ "delete-feature": "delete-feature NAME VERSION [--project PROJECT] [--actor ACTOR] [--reason TEXT] [--idempotency-key KEY]",
55
+ "delete-feature-set": "delete-feature-set NAME VERSION [--project PROJECT] [--actor ACTOR] [--reason TEXT] [--idempotency-key KEY]",
56
+ "restore-parameter": "restore-parameter NAME VERSION [--project PROJECT] [--actor ACTOR] [--reason TEXT] [--idempotency-key KEY]",
57
+ "restore-feature": "restore-feature NAME VERSION [--project PROJECT] [--actor ACTOR] [--reason TEXT] [--idempotency-key KEY]",
58
+ "restore-feature-set": "restore-feature-set NAME VERSION [--project PROJECT] [--actor ACTOR] [--reason TEXT] [--idempotency-key KEY]",
59
+ "get-parameter-lifecycle": "get-parameter-lifecycle NAME VERSION [--project PROJECT]",
60
+ "get-feature-lifecycle": "get-feature-lifecycle NAME VERSION [--project PROJECT]",
61
+ "get-feature-set-lifecycle": "get-feature-set-lifecycle NAME VERSION [--project PROJECT]",
62
+ "list-parameter-references": "list-parameter-references NAME VERSION [--project PROJECT] [--direct]",
63
+ "list-feature-references": "list-feature-references NAME VERSION [--project PROJECT] [--direct]",
64
+ "list-feature-set-references": "list-feature-set-references NAME VERSION [--project PROJECT] [--direct]",
65
+ "list-parameter-versions": "list-parameter-versions NAME [--project PROJECT] [--limit N] [--offset N] [--all]",
66
+ "list-feature-versions": "list-feature-versions NAME [--project PROJECT] [--limit N] [--offset N] [--all]",
67
+ "list-feature-set-versions": "list-feature-set-versions NAME [--project PROJECT] [--limit N] [--offset N] [--all]",
68
+ "list-operator-versions": "list-operator-versions NAME [--project PROJECT] [--limit N] [--offset N] [--all]",
37
69
  "add-parameter": "add-parameter SPEC_JSON",
38
70
  "add-feature": "add-feature SPEC_JSON",
39
71
  "add-feature-set": "add-feature-set SPEC_JSON",
@@ -44,7 +76,7 @@ const COMMAND_USAGE = {
44
76
  "delete-dataset": "delete-dataset DATASET_ID DATASET_VERSION [--project PROJECT]",
45
77
  "list-dataset-artifacts": "list-dataset-artifacts [--project PROJECT] [--dataset-id ID] [--dataset-version VERSION] [--include-deleted] [--limit N] [--offset N]",
46
78
  "get-dataset": "get-dataset DATASET_ID DATASET_VERSION [--project PROJECT] [--include-deleted]",
47
- "list-jobs": "list-jobs [-q QUERY] [--limit N] [--offset N]",
79
+ "list-jobs": "list-jobs [filters] [-q QUERY] [--limit N] [--offset N] [--all]",
48
80
  "resolve-manifest": "resolve-manifest MANIFEST [--out PATH]",
49
81
  "resolve-dataset": "resolve-dataset DATASET_ID DATASET_VERSION [--project PROJECT] [--out PATH]",
50
82
  "build-dataset": "build-dataset MANIFEST [--engine chronon] [--source-mode direct] [--upload-chronon-metadata] [--partition-duration DURATION] [--max-parallelism N] [--no-resume] [--wait] [--poll-interval SECONDS] [--wait-timeout-seconds SECONDS]",
@@ -58,8 +90,39 @@ const COMMAND_USAGE = {
58
90
  "delete-operator": "delete-operator NAME VERSION [--project PROJECT]",
59
91
  "run-operator": "run-operator NAME VERSION [--project PROJECT] [--config JSON]",
60
92
  "get-dataset-artifact": "get-dataset-artifact DATASET_ID MANIFEST_HASH [--project PROJECT]",
61
- "download-dataset-artifact": "download-dataset-artifact DATASET_ID MANIFEST_HASH [--project PROJECT] --out-dir PATH [--force]",
93
+ "download-dataset-artifact": "download-dataset-artifact DATASET_ID MANIFEST_HASH [--project PROJECT] --out-dir PATH [--file PATH] [--force]",
62
94
  "fetch-inference-data": "fetch-inference-data MANIFEST --cutoff-time TIMESTAMP [--max-workers N] [--max-source-lag-hours HOURS] [--no-validate-freshness] [--allow-missing]",
95
+ "fetch-inference-context": "fetch-inference-context REQUEST_JSON",
96
+ "register-trainer": "register-trainer DEFINITION_JSON",
97
+ "register-training-run": "register-training-run RUN_JSON",
98
+ "validate-training-run": "validate-training-run RUN_JSON",
99
+ "submit-training-job": "submit-training-job REQUEST_JSON",
100
+ "get-training-job": "get-training-job JOB_ID",
101
+ "retry-training-job": "retry-training-job JOB_ID",
102
+ "cancel-training-job": "cancel-training-job JOB_ID",
103
+ "register-evaluation-config": "register-evaluation-config CONFIG_JSON",
104
+ "get-evaluation-config": "get-evaluation-config NAME VERSION",
105
+ "register-metric-definition": "register-metric-definition DEFINITION_JSON",
106
+ "get-metric-definition": "get-metric-definition NAME VERSION",
107
+ "register-executable-package": "register-executable-package SPEC_JSON",
108
+ "verify-executable-package": "verify-executable-package NAME VERSION",
109
+ "publish-executable-package": "publish-executable-package NAME VERSION",
110
+ "validate-evaluation-run": "validate-evaluation-run REQUEST_JSON",
111
+ "submit-evaluation": "submit-evaluation REQUEST_JSON",
112
+ "get-evaluation-run": "get-evaluation-run RUN_ID",
113
+ "get-evaluation-job": "get-evaluation-job JOB_ID",
114
+ "retry-evaluation-job": "retry-evaluation-job JOB_ID",
115
+ "cancel-evaluation-job": "cancel-evaluation-job JOB_ID",
116
+ "list-evaluation-attempts": "list-evaluation-attempts JOB_ID",
117
+ "get-evaluation-result": "get-evaluation-result RESULT_ID",
118
+ "get-evaluation-result-metrics": "get-evaluation-result-metrics RESULT_ID",
119
+ "get-evaluation-result-artifacts": "get-evaluation-result-artifacts RESULT_ID",
120
+ "summarize-evaluation-run": "summarize-evaluation-run RUN_ID",
121
+ "get-evaluation-summary": "get-evaluation-summary SUMMARY_ID",
122
+ "create-model-package": "create-model-package REQUEST_JSON",
123
+ "get-model-package": "get-model-package PACKAGE_ID [--package-version VERSION]",
124
+ "cancel-model-package": "cancel-model-package PACKAGE_ID [--package-version VERSION]",
125
+ "validate-model-artifact": "validate-model-artifact ARTIFACT_ID",
63
126
  apply: "apply CATALOG_DIRECTORY [--dry-run] [--project PROJECT]",
64
127
  };
65
128
 
@@ -143,6 +206,17 @@ function datasetEndpoint(project, datasetId, datasetVersion) {
143
206
  return `/datasets/${encodeURIComponent(project)}/${encodeURIComponent(datasetId)}/${encodeURIComponent(datasetVersion)}`;
144
207
  }
145
208
 
209
+ function lifecycleResource(command) {
210
+ if (command.includes("feature-set")) return "feature-sets";
211
+ if (command.includes("parameter")) return "parameters";
212
+ if (command.includes("feature")) return "features";
213
+ throw new Error(`unsupported lifecycle command: ${command}`);
214
+ }
215
+
216
+ function lifecycleEndpoint(resource, project, name, version, suffix = "") {
217
+ return `/registry/${resource}/${encodeURIComponent(project)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}${suffix}`;
218
+ }
219
+
146
220
  async function registeredDataset(api, project, datasetId, datasetVersion, includeDeleted = false) {
147
221
  const payload = await api.get(datasetEndpoint(project, datasetId, datasetVersion), { include_deleted: includeDeleted });
148
222
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("registered dataset response must be a JSON object");
@@ -245,17 +319,55 @@ async function runBusinessCli(argv) {
245
319
  } else if (options.command === "get-dataset") {
246
320
  const datasetId = positional(rest, "dataset id"); const datasetVersion = positional(rest, "dataset version"); const project = take(rest, "--project", "default");
247
321
  result = await registeredDataset(client(options), project, datasetId, datasetVersion, boolean(rest, "--include-deleted"));
248
- }
249
- else if (options.command.startsWith("list-") && !["list-jobs", "list-dataset-artifacts"].includes(options.command)) {
322
+ } else if (["get-parameter", "get-feature", "get-feature-set", "get-operator"].includes(options.command)) {
323
+ const resource = options.command.slice(4);
324
+ const endpoint = resource === "feature-set" ? "feature-sets" : `${resource}s`;
325
+ const name = positional(rest, `${resource} name`);
326
+ const version = positional(rest, `${resource} version`);
327
+ const project = take(rest, "--project", "default");
328
+ result = await client(options).get(`/${endpoint}/${encodeURIComponent(project)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}`);
329
+ } else if (/^delete-(parameter|feature|feature-set)$/.test(options.command)) {
330
+ const name = positional(rest, "resource name"); const version = positional(rest, "resource version"); const project = take(rest, "--project", "default");
331
+ result = await client(options).delete(lifecycleEndpoint(lifecycleResource(options.command), project, name, version), {
332
+ actor: take(rest, "--actor"), reason: take(rest, "--reason"), idempotency_key: take(rest, "--idempotency-key"),
333
+ });
334
+ } else if (/^restore-(parameter|feature|feature-set)$/.test(options.command)) {
335
+ const name = positional(rest, "resource name"); const version = positional(rest, "resource version"); const project = take(rest, "--project", "default");
336
+ result = await client(options).post(lifecycleEndpoint(lifecycleResource(options.command), project, name, version, "/restore"), {
337
+ actor: take(rest, "--actor") || "anonymous", reason: take(rest, "--reason", ""), idempotency_key: take(rest, "--idempotency-key"),
338
+ });
339
+ } else if (/^get-(parameter|feature|feature-set)-lifecycle$/.test(options.command)) {
340
+ const name = positional(rest, "resource name"); const version = positional(rest, "resource version"); const project = take(rest, "--project", "default");
341
+ result = await client(options).get(lifecycleEndpoint(lifecycleResource(options.command), project, name, version, "/lifecycle"));
342
+ } else if (/^list-(parameter|feature|feature-set)-references$/.test(options.command)) {
343
+ const name = positional(rest, "resource name"); const version = positional(rest, "resource version"); const project = take(rest, "--project", "default");
344
+ result = await client(options).get(lifecycleEndpoint(lifecycleResource(options.command), project, name, version, "/references"), { transitive: !boolean(rest, "--direct") });
345
+ } else if (["list-parameter-versions", "list-feature-versions", "list-feature-set-versions", "list-operator-versions"].includes(options.command)) {
346
+ const resource = options.command.replace(/^list-/, "").replace(/-versions$/, "");
347
+ const endpoint = resource === "feature-set" ? "feature-sets" : `${resource}s`;
348
+ const name = positional(rest, `${resource} name`);
349
+ const project = take(rest, "--project", "default");
350
+ const allPages = boolean(rest, "--all");
351
+ const rawLimit = take(rest, "--limit"); const rawOffset = take(rest, "--offset");
352
+ if (allPages && (rawLimit !== undefined || rawOffset !== undefined)) throw new Error("--all cannot be combined with --limit or --offset");
353
+ if (allPages) result = await fetchAll(client(options), `/${endpoint}/${encodeURIComponent(project)}/${encodeURIComponent(name)}`);
354
+ else { const limit = Number(rawLimit ?? 50); const offset = Number(rawOffset ?? 0); if (!Number.isInteger(limit) || limit < 1 || limit > 500) throw new Error("limit must be between 1 and 500"); if (!Number.isInteger(offset) || offset < 0) throw new Error("offset must be >= 0"); result = await client(options).get(`/${endpoint}/${encodeURIComponent(project)}/${encodeURIComponent(name)}`, { limit, offset }); }
355
+ } else if (options.command.startsWith("list-") && !["list-jobs", "list-dataset-artifacts"].includes(options.command)) {
250
356
  const endpoints = { "list-parameters": "/parameters", "list-operators": "/operators", "list-features": "/features", "list-feature-sets": "/feature-sets", "list-datasets": "/datasets" };
251
357
  const project = take(rest, "--project");
358
+ const name = take(rest, "--name"); const version = take(rest, "--version");
252
359
  const query = take(rest, "--q", take(rest, "-q")); const includeDeleted = options.command === "list-datasets" && boolean(rest, "--include-deleted"); const rawLimit = take(rest, "--limit"); const rawOffset = take(rest, "--offset");
253
- if (rawLimit !== undefined || rawOffset !== undefined) { const limit = Number(rawLimit ?? 50); const offset = Number(rawOffset ?? 0); if (!Number.isInteger(limit) || limit < 1 || limit > 500) throw new Error("limit must be between 1 and 500"); if (!Number.isInteger(offset) || offset < 0) throw new Error("offset must be >= 0"); result = await client(options).get(endpoints[options.command], { q: query, limit, offset, project, include_deleted: includeDeleted }); }
254
- else result = await fetchAll(client(options), endpoints[options.command], { q: query, project, include_deleted: includeDeleted });
360
+ const allPages = boolean(rest, "--all");
361
+ if (allPages && (rawLimit !== undefined || rawOffset !== undefined)) throw new Error("--all cannot be combined with --limit or --offset");
362
+ if (allPages) result = await fetchAll(client(options), endpoints[options.command], { q: query, project, name, version, include_deleted: includeDeleted });
363
+ else { const limit = Number(rawLimit ?? 50); const offset = Number(rawOffset ?? 0); if (!Number.isInteger(limit) || limit < 1 || limit > 500) throw new Error("limit must be between 1 and 500"); if (!Number.isInteger(offset) || offset < 0) throw new Error("offset must be >= 0"); result = await client(options).get(endpoints[options.command], { q: query, limit, offset, project, name, version, include_deleted: includeDeleted }); }
255
364
  } else if (options.command === "list-jobs") {
256
365
  const query = take(rest, "--q", take(rest, "-q")); const rawLimit = take(rest, "--limit"); const rawOffset = take(rest, "--offset");
257
- if (rawLimit !== undefined || rawOffset !== undefined) { const limit = Number(rawLimit ?? 50); const offset = Number(rawOffset ?? 0); if (!Number.isInteger(limit) || limit < 1 || limit > 500) throw new Error("limit must be between 1 and 500"); if (!Number.isInteger(offset) || offset < 0) throw new Error("offset must be >= 0"); result = await client(options).get("/jobs", { q: query, limit, offset }); }
258
- else result = await fetchAll(client(options), "/jobs", { q: query });
366
+ const filters = {}; for (const field of ["project", "dataset-id", "dataset-version", "manifest-hash", "status", "parent-job-id", "created-from", "created-to", "updated-from", "updated-to"]) filters[field.replaceAll("-", "_")] = take(rest, `--${field}`);
367
+ const allPages = boolean(rest, "--all");
368
+ if (allPages && (rawLimit !== undefined || rawOffset !== undefined)) throw new Error("--all cannot be combined with --limit or --offset");
369
+ if (allPages) result = await fetchAll(client(options), "/jobs", { q: query, ...filters });
370
+ else { const limit = Number(rawLimit ?? 50); const offset = Number(rawOffset ?? 0); if (!Number.isInteger(limit) || limit < 1 || limit > 500) throw new Error("limit must be between 1 and 500"); if (!Number.isInteger(offset) || offset < 0) throw new Error("offset must be >= 0"); result = await client(options).get("/jobs", { q: query, limit, offset, ...filters }); }
259
371
  }
260
372
  else if (options.command === "resolve-manifest") {
261
373
  const manifest = positional(rest, "manifest");
@@ -314,11 +426,49 @@ async function runBusinessCli(argv) {
314
426
  } else if (options.command === "download-dataset-artifact") {
315
427
  const dataset = positional(rest, "dataset id"); const hash = positional(rest, "manifest hash"); const project = take(rest, "--project", "default"); const outDir = take(rest, "--out-dir");
316
428
  if (!outDir) throw new Error("--out-dir is required");
317
- result = await client(options).downloadDatasetArtifact(project, dataset, hash, resolvedPath(outDir), boolean(rest, "--force"));
429
+ const files = [];
430
+ while (rest.includes("--file")) files.push(take(rest, "--file"));
431
+ result = files.length
432
+ ? await client(options).downloadDatasetArtifactFiles(project, dataset, hash, files, resolvedPath(outDir), boolean(rest, "--force"))
433
+ : await client(options).downloadDatasetArtifact(project, dataset, hash, resolvedPath(outDir), boolean(rest, "--force"));
318
434
  } else if (options.command === "fetch-inference-data") {
319
435
  const manifest = positional(rest, "manifest"); const cutoffTime = take(rest, "--cutoff-time"); if (!cutoffTime) throw new Error("--cutoff-time is required");
320
436
  const realtime = {}; const maxWorkers = take(rest, "--max-workers"); const maxLag = take(rest, "--max-source-lag-hours"); if (maxWorkers !== undefined) { realtime.max_workers = integer(maxWorkers, "--max-workers"); if (realtime.max_workers < 1) throw new Error("--max-workers must be at least 1"); } if (maxLag !== undefined) { realtime.max_source_lag_hours = finiteNumber(maxLag, "--max-source-lag-hours"); if (realtime.max_source_lag_hours < 0) throw new Error("--max-source-lag-hours must be non-negative"); } if (boolean(rest, "--no-validate-freshness")) realtime.validate_freshness = false; if (boolean(rest, "--allow-missing")) realtime.allow_missing = true;
321
437
  const request = { manifest: readDataset(manifest), cutoff_time: cutoffTime }; if (Object.keys(realtime).length) request.realtime_fetch = realtime; result = await client(options).post("/inference-data/fetch", request);
438
+ } else if (options.command === "fetch-inference-context") {
439
+ result = await client(options).post("/inference-data/context", readSpec(positional(rest, "request JSON")));
440
+ } else if ({
441
+ "register-trainer": "/trainer-definitions", "register-training-run": "/training-runs", "validate-training-run": "/training-runs/validate",
442
+ "submit-training-job": "/training-jobs", "register-evaluation-config": "/evaluation-configs", "register-metric-definition": "/metric-definitions",
443
+ "register-executable-package": "/executable-packages", "validate-evaluation-run": "/evaluation-runs/validate", "submit-evaluation": "/evaluation-runs",
444
+ "create-model-package": "/model-packages",
445
+ }[options.command]) {
446
+ const endpoint = { "register-trainer": "/trainer-definitions", "register-training-run": "/training-runs", "validate-training-run": "/training-runs/validate", "submit-training-job": "/training-jobs", "register-evaluation-config": "/evaluation-configs", "register-metric-definition": "/metric-definitions", "register-executable-package": "/executable-packages", "validate-evaluation-run": "/evaluation-runs/validate", "submit-evaluation": "/evaluation-runs", "create-model-package": "/model-packages" }[options.command];
447
+ result = await client(options).post(endpoint, readSpec(positional(rest, "request JSON")));
448
+ } else if (/^(get|retry|cancel)-training-job$/.test(options.command)) {
449
+ const jobId = positional(rest, "training job id"); const endpoint = `/training-jobs/${encodeURIComponent(jobId)}`;
450
+ result = options.command === "get-training-job" ? await client(options).get(endpoint) : await client(options).post(`${endpoint}/${options.command.split("-")[0]}`);
451
+ } else if (/^(get|retry|cancel)-evaluation-job$/.test(options.command) || options.command === "list-evaluation-attempts") {
452
+ const jobId = positional(rest, "evaluation job id"); const endpoint = `/evaluation-jobs/${encodeURIComponent(jobId)}`;
453
+ if (options.command === "get-evaluation-job") result = await client(options).get(endpoint);
454
+ else if (options.command === "list-evaluation-attempts") result = await client(options).get(`${endpoint}/attempts`);
455
+ else result = await client(options).post(`${endpoint}/${options.command.split("-")[0]}`);
456
+ } else if (options.command === "get-evaluation-run") result = await client(options).get(`/evaluation-runs/${encodeURIComponent(positional(rest, "evaluation run id"))}`);
457
+ else if (options.command === "summarize-evaluation-run") result = await client(options).post(`/evaluation-runs/${encodeURIComponent(positional(rest, "evaluation run id"))}/summarize`);
458
+ else if (options.command === "get-evaluation-summary") result = await client(options).get(`/evaluation-summaries/${encodeURIComponent(positional(rest, "evaluation summary id"))}`);
459
+ else if (/^get-evaluation-result(?:-metrics|-artifacts)?$/.test(options.command)) {
460
+ const resultId = positional(rest, "evaluation result id"); const suffix = options.command.endsWith("-metrics") ? "/metrics" : options.command.endsWith("-artifacts") ? "/artifacts" : "";
461
+ result = await client(options).get(`/evaluation-results/${encodeURIComponent(resultId)}${suffix}`);
462
+ } else if (/^(get-evaluation-config|get-metric-definition)$/.test(options.command)) {
463
+ const name = positional(rest, "resource name"); const version = positional(rest, "resource version"); const base = options.command === "get-evaluation-config" ? "evaluation-configs" : "metric-definitions";
464
+ result = await client(options).get(`/${base}/${encodeURIComponent(name)}/${encodeURIComponent(version)}`);
465
+ } else if (/^(verify|publish)-executable-package$/.test(options.command)) {
466
+ const name = positional(rest, "package name"); const version = positional(rest, "package version"); const action = options.command.split("-")[0];
467
+ result = await client(options).post(`/executable-packages/${encodeURIComponent(name)}/${encodeURIComponent(version)}/${action}`);
468
+ } else if (options.command === "validate-model-artifact") result = await client(options).get(`/model-artifacts/${encodeURIComponent(positional(rest, "model artifact id"))}/validate`);
469
+ else if (/^(get|cancel)-model-package$/.test(options.command)) {
470
+ const packageId = positional(rest, "model package id"); const params = { package_version: take(rest, "--package-version") }; const endpoint = `/model-packages/${encodeURIComponent(packageId)}`;
471
+ result = options.command === "get-model-package" ? await client(options).get(endpoint, params) : await client(options).post(`${endpoint}/cancel`, undefined, params);
322
472
  } else if (options.command === "apply") {
323
473
  const directory = positional(rest, "catalog directory"); const dryRun = boolean(rest, "--dry-run"); const targetProject = take(rest, "--project");
324
474
  result = await applyCatalog(loadCatalog(resolvedPath(directory)), client(options), dryRun, targetProject);
@@ -95,6 +95,52 @@ class PlatformApiClient {
95
95
  fs.rmSync(temporaryDir, { recursive: true, force: true });
96
96
  }
97
97
  }
98
+
99
+ async downloadDatasetArtifactFiles(project, datasetId, manifestHash, relativePaths, outputDir, force = false) {
100
+ const files = [...new Set((relativePaths || []).map((value) => String(value)))];
101
+ if (!files.length) throw new Error("at least one artifact file is required");
102
+ for (const relative of files) {
103
+ const normalized = relative.replace(/\\/g, "/");
104
+ const parts = normalized.split("/");
105
+ if (!normalized || normalized.startsWith("/") || normalized.endsWith("/") || parts.includes("..")) {
106
+ throw new Error(`artifact path escapes output directory: ${JSON.stringify(relative)}`);
107
+ }
108
+ }
109
+ const targetRoot = path.resolve(expandHome(outputDir));
110
+ if (fs.existsSync(targetRoot) && fs.readdirSync(targetRoot).length) {
111
+ if (!force) throw new Error(`download output directory is not empty: ${targetRoot}`);
112
+ fs.rmSync(targetRoot, { recursive: true, force: true });
113
+ }
114
+ fs.mkdirSync(targetRoot, { recursive: true });
115
+ try {
116
+ for (const relative of files) {
117
+ const normalized = relative.replace(/\\/g, "/");
118
+ const parts = normalized.split("/");
119
+ const destination = path.resolve(targetRoot, ...parts);
120
+ if (!normalized || normalized.startsWith("/") || normalized.endsWith("/")
121
+ || parts.includes("..") || destination !== targetRoot && !destination.startsWith(`${targetRoot}${path.sep}`)) {
122
+ throw new Error(`artifact path escapes output directory: ${JSON.stringify(relative)}`);
123
+ }
124
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
125
+ const endpoint = `/dataset-artifacts/${encodeURIComponent(project)}/${encodeURIComponent(datasetId)}/${encodeURIComponent(manifestHash)}/files/${parts.map(encodeURIComponent).join("/")}`;
126
+ const controller = new AbortController();
127
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
128
+ try {
129
+ const response = await fetch(this.url(endpoint), { headers: { Accept: "application/octet-stream" }, signal: controller.signal });
130
+ if (!response.ok) {
131
+ let detail = response.statusText;
132
+ try { detail = JSON.parse(await response.text())?.detail ?? detail; } catch (_) { /* status text */ }
133
+ throw new Error(`artifact file download failed: HTTP ${response.status}: ${detail}`);
134
+ }
135
+ fs.writeFileSync(destination, Buffer.from(await response.arrayBuffer()), { flag: "wx" });
136
+ } finally { clearTimeout(timer); }
137
+ }
138
+ } catch (error) {
139
+ fs.rmSync(targetRoot, { recursive: true, force: true });
140
+ throw error;
141
+ }
142
+ return { project, dataset_id: datasetId, manifest_hash: manifestHash, output_dir: targetRoot, file_count: files.length, files: files.sort() };
143
+ }
98
144
  }
99
145
 
100
146
  function contentLength(response) {
package/scripts/lib.js CHANGED
@@ -152,19 +152,21 @@ function loadRelease(packageRoot = PACKAGE_ROOT) {
152
152
  || requirements.os.join(",") !== "darwin,linux") {
153
153
  throw new Error("release manifest 缺少有效 runtime requirements");
154
154
  }
155
- const skill = release.skills && release.skills[SKILL_NAME];
156
- if (!skill || skill.path !== `skills/${SKILL_NAME}` || !isSha256(skill.sha256)
157
- || !isSemver(skill.revision) || typeof skill.requires_cli !== "string") {
155
+ const skills = release.skills;
156
+ const skill = skills && skills[SKILL_NAME];
157
+ if (!skill || Object.keys(skills).length < 1) {
158
158
  throw new Error("release manifest 缺少有效 Skill 信息");
159
159
  }
160
+ for (const [name, item] of Object.entries(skills)) {
161
+ if (item.path !== `skills/${name}` || !isSha256(item.sha256)
162
+ || !isSemver(item.revision) || typeof item.requires_cli !== "string") throw new Error("release manifest 缺少有效 Skill 信息");
163
+ }
160
164
  const cli = release.cli;
161
165
  if (!cli || cli.name !== CLI_NAME || cli.path !== "runtime/business-client"
162
166
  || !isSemver(cli.version) || !isSha256(cli.sha256) || cli.entrypoint !== "src/cli.js") {
163
167
  throw new Error("release manifest 缺少有效 JavaScript CLI 信息");
164
168
  }
165
- if (!versionSatisfies(cli.version, skill.requires_cli)) {
166
- throw new Error("Skill requires_cli 与 release CLI version 不兼容");
167
- }
169
+ for (const item of Object.values(skills)) if (!versionSatisfies(cli.version, item.requires_cli)) throw new Error("Skill requires_cli 与 release CLI version 不兼容");
168
170
  return release;
169
171
  }
170
172
 
@@ -201,7 +203,7 @@ function verifyPackage(packageRoot = PACKAGE_ROOT) {
201
203
  }
202
204
 
203
205
  assertTopLevel(path.join(packageRoot, "runtime"), new Set(["business-client"]), "runtime");
204
- assertTopLevel(path.join(packageRoot, "skills"), new Set([SKILL_NAME]), "skills");
206
+ assertTopLevel(path.join(packageRoot, "skills"), new Set(Object.keys(release.skills)), "skills");
205
207
  const skillRoot = resolveInside(packageRoot, release.skills[SKILL_NAME].path);
206
208
  const clientRoot = resolveInside(packageRoot, release.cli.path);
207
209
  assertTopLevel(skillRoot, SKILL_TOP_LEVEL, "Skill");
@@ -232,11 +234,18 @@ function verifyPackage(packageRoot = PACKAGE_ROOT) {
232
234
  if (treeDigest(skillRoot) !== release.skills[SKILL_NAME].sha256) {
233
235
  throw new Error("Skill 目录摘要与 release manifest 不一致");
234
236
  }
237
+ for (const [name, item] of Object.entries(release.skills)) {
238
+ const root = resolveInside(packageRoot, item.path);
239
+ const actual = new Set(fs.readdirSync(root));
240
+ if (!actual.has("SKILL.md") || !actual.has("agents") || [...actual].some((entry) => !SKILL_TOP_LEVEL.has(entry))) throw new Error(`Skill ${name} 目录结构无效`);
241
+ rejectForbiddenTree(root, `Skill ${name}`);
242
+ if (treeDigest(root) !== item.sha256) throw new Error(`Skill ${name} 目录摘要与 release manifest 不一致`);
243
+ }
235
244
  if (treeDigest(clientRoot) !== release.cli.sha256) {
236
245
  throw new Error("JavaScript CLI 摘要与 release manifest 不一致");
237
246
  }
238
247
  if (!fs.existsSync(expectedEntry)) throw new Error("JavaScript CLI 入口不存在");
239
- return { release, skillRoot, clientRoot };
248
+ return { release, skillRoot, skillRoots: Object.fromEntries(Object.entries(release.skills).map(([name, item]) => [name, resolveInside(packageRoot, item.path)])), clientRoot };
240
249
  }
241
250
 
242
251
  function status(options = {}, packageRoot = PACKAGE_ROOT) {
@@ -516,6 +525,9 @@ function installLocked(options, packageRoot, verified, paths) {
516
525
  let hadShim = false;
517
526
  let legacyBackupPath = null;
518
527
  let legacyMoved = false;
528
+ const additionalSkills = Object.keys(verified.release.skills).filter((name) => name !== SKILL_NAME);
529
+ const additionalBackups = [];
530
+ const installedAdditional = [];
519
531
  try {
520
532
  stagePersistentInstall(stagedInstall, packageRoot, verified);
521
533
  fs.cpSync(verified.skillRoot, stagedSkill, { recursive: true, errorOnExist: true });
@@ -540,6 +552,17 @@ function installLocked(options, packageRoot, verified, paths) {
540
552
  }
541
553
  fs.renameSync(stagedSkill, paths.target);
542
554
  skillMoved = true;
555
+ for (const name of additionalSkills) {
556
+ const target = path.join(paths.skillsDir, name);
557
+ const backup = path.join(paths.skillsDir, `.${name}.backup.${process.pid}.${Date.now()}`);
558
+ if (pathEntryExists(target)) {
559
+ if (!current?.skills?.[name] || current.skills[name].path !== target) throw new Error(`拒绝覆盖未托管 Skill: ${name}`);
560
+ fs.renameSync(target, backup);
561
+ additionalBackups.push({ target, backup });
562
+ }
563
+ fs.cpSync(verified.skillRoots[name], target, { recursive: true, errorOnExist: true });
564
+ installedAdditional.push(target);
565
+ }
543
566
  if (pathEntryExists(paths.shim)) {
544
567
  fs.renameSync(paths.shim, shimBackup);
545
568
  hadShim = true;
@@ -555,6 +578,7 @@ function installLocked(options, packageRoot, verified, paths) {
555
578
  removeOwnedDirectory(skillStaging, paths.skillsDir, `.${SKILL_NAME}.install.`);
556
579
  if (hadState) removeOwnedDirectory(stateBackup, paths.stateDir, ".backup.");
557
580
  if (hadSkill) removeOwnedDirectory(skillBackup, paths.skillsDir, `.${SKILL_NAME}.backup.`);
581
+ for (const { backup } of additionalBackups) fs.rmSync(backup, { recursive: true, force: true });
558
582
  if (hadShim) fs.rmSync(shimBackup, { force: true });
559
583
  const result = installationResult(
560
584
  options.migrateLegacy ? "migrated" : (current || (targetExists && !backingUpUnmanaged) ? "upgraded" : "installed"),
@@ -572,6 +596,8 @@ function installLocked(options, packageRoot, verified, paths) {
572
596
  if (hadShim && pathEntryExists(shimBackup)) fs.renameSync(shimBackup, paths.shim);
573
597
  if (skillMoved && pathEntryExists(paths.target)) removeOwnedDirectory(paths.target, paths.skillsDir, SKILL_NAME);
574
598
  if (hadSkill && pathEntryExists(skillBackup)) fs.renameSync(skillBackup, paths.target);
599
+ for (const target of installedAdditional.reverse()) fs.rmSync(target, { recursive: true, force: true });
600
+ for (const { target, backup } of additionalBackups.reverse()) if (pathEntryExists(backup)) fs.renameSync(backup, target);
575
601
  if (legacyMoved && legacyBackupPath && pathEntryExists(legacyBackupPath) && !pathEntryExists(paths.target)) {
576
602
  fs.renameSync(legacyBackupPath, paths.target);
577
603
  }
@@ -613,7 +639,7 @@ function stagePersistentInstall(target, packageRoot, verified) {
613
639
  }
614
640
 
615
641
  function buildInstallationRecord(paths, release) {
616
- return {
642
+ const record = {
617
643
  schema_version: RECORD_SCHEMA,
618
644
  owner: { package: PACKAGE_NAME, installation_id: installationId(paths) },
619
645
  release_version: release.release_version,
@@ -637,11 +663,17 @@ function buildInstallationRecord(paths, release) {
637
663
  release_sha256: sha256File(path.join(paths.installRoot, "release.json")),
638
664
  policy_sha256: sha256File(path.join(paths.installRoot, "release-policy.json")),
639
665
  };
666
+ record.skills = Object.fromEntries(Object.entries(release.skills).map(([name, item]) => [name, {
667
+ name, revision: item.revision, requires_cli: item.requires_cli,
668
+ path: path.join(paths.skillsDir, name), sha256: item.sha256,
669
+ }]));
670
+ return record;
640
671
  }
641
672
 
642
673
  function assertInstalledState(paths, record, prewarm) {
643
674
  if (!digestEquals(paths.clientRoot, record.cli.sha256)) throw new Error("安装后的 CLI 摘要不匹配");
644
675
  if (!digestEquals(paths.target, record.skill.sha256)) throw new Error("安装后的 Skill 摘要不匹配");
676
+ for (const skill of Object.values(record.skills || {})) if (!digestEquals(skill.path, skill.sha256)) throw new Error(`安装后的 Skill 摘要不匹配: ${skill.name}`);
645
677
  if (sha256File(paths.shim) !== record.shim.sha256) throw new Error("安装后的 CLI shim 摘要不匹配");
646
678
  if (prewarm) assertRuntime(paths.dispatcher);
647
679
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: feature-management
3
- description: Manage projects, versioned business feature assets, dataset rowsets and endpoint eligibility, and causal realtime feature reads through the deployed ML Platform API using the ml-platform CLI installed with the same release. Use when a business user asks an agent to manage an empty Project, define or change Parameter source contracts, author/package/register an Operator wheel, define single-column Features, order a FeatureSet, create a DatasetManifest or catalog, configure training/validation/test rowsets or abnormal-data endpoint exclusion, configure realtime freshness policy, run catalog dry-run/apply, build a dataset, fetch one inference row, or inspect an artifact without downloading the platform source repository. Do not use for model training, versioned Registry asset deletion, image builds, or Kubernetes deployment.
3
+ description: Manage projects, versioned business feature assets and their lifecycle, dataset rowsets and endpoint eligibility, and causal realtime row or sequence-context reads through the deployed ML Platform API using the ml-platform CLI installed with the same release. Use when a business user asks an agent to manage an empty Project, define or change Parameter source contracts, author/package/register an Operator wheel, define single-column Features, order a FeatureSet, create a DatasetManifest or catalog, inspect references, soft-delete or restore a Parameter/Feature/FeatureSet version, configure training/validation/test rowsets or abnormal-data endpoint exclusion, configure realtime freshness policy, run catalog dry-run/apply, build a dataset, fetch inference data, or inspect an artifact without downloading the platform source repository. Do not use for model training, Operator/Dataset deletion, image builds, or Kubernetes deployment.
4
4
  ---
5
5
 
6
6
  # Feature Management
@@ -14,6 +14,34 @@ Operator ------> Feature -> FeatureSet -> DatasetManifest -> DatasetArtifact
14
14
 
15
15
  Treat `Recipe` and public `Feature.compute` as removed. Treat `computation_hash` as internal execution metadata, never as a business-managed asset.
16
16
 
17
+ ## Platform Role And Evidence Boundaries
18
+
19
+ The ML Platform is a deterministic data-contract and data-delivery platform. It
20
+ registers versioned source Parameters, runs approved deterministic Operators,
21
+ orders single-column Features into FeatureSets, resolves DatasetManifests, and
22
+ materializes batch DatasetArtifacts or serves one causal realtime inference row.
23
+ It records versions, lineage, quality, missingness, freshness, and execution
24
+ metadata so an algorithm project can consume data without owning source routing
25
+ or cache details.
26
+
27
+ It does not train, evaluate, or serve models; own complete business feature
28
+ engineering; provision arbitrary source tables; or administer Kubernetes and
29
+ platform services. Read
30
+ [references/platform-capability-guide.md](references/platform-capability-guide.md)
31
+ when the user needs the detailed platform boundary or evidence-level explanation.
32
+
33
+ Keep these evidence rules active in every workflow:
34
+
35
+ - Platform defaults and capabilities are not business evidence.
36
+ - Dry-run, publish, resolve, build, metadata inspection, downloaded-file
37
+ inspection, and realtime fetch are separate evidence checkpoints; success at
38
+ one checkpoint does not imply success at the next.
39
+ - `validation.ok=true` does not by itself prove source semantics, causal formula
40
+ correctness, Parquet contents, or numeric parity.
41
+ - Dry-run, publish, and resolve may not detect a missing source relation. Do not
42
+ report source-backed success until build/fetch returns the required evidence,
43
+ and never rename a Parameter or remove fields merely to bypass a failed read.
44
+
17
45
  ## Load The Right Context
18
46
 
19
47
  1. Resolve the Skill root as the directory containing this `SKILL.md`.
@@ -89,6 +117,16 @@ Confirm:
89
117
 
90
118
  Separate preparation from mutation. Creating files and running local tests or `apply --dry-run` does not authorize publishing a wheel, changing the Registry, or submitting a build Job.
91
119
 
120
+ Soft-delete and restore are separate Registry mutations. Before soft-deleting a
121
+ Parameter, Feature, or FeatureSet version, inspect its lifecycle state and both
122
+ direct and transitive references, report the affected immutable consumers, and
123
+ obtain explicit authorization for the exact `project/name:version`. Supply an
124
+ actor, reason, and stable idempotency key. Soft deletion preserves historical
125
+ references and hashes but hides the version from normal discovery and prevents
126
+ new consumers from binding it. Restore only after checking that all of the
127
+ resource's own dependencies are active. Never describe these operations as
128
+ physical deletion.
129
+
92
130
  A label such as "10-minute mean" confirms neither its input Parameter nor a
93
131
  complete formula. Before drafting an Operator or Feature, ask for the exact
94
132
  versioned input Parameter(s), or for an approved source contract from which
@@ -10,8 +10,9 @@ Use the `ml-platform` executable installed on `PATH` by the same ML Platform rel
10
10
  4. Validate and publish a catalog
11
11
  5. Resolve and build a dataset
12
12
  6. Fetch realtime inference data
13
- 7. Inspect and download an artifact
14
- 8. Prohibited shortcuts
13
+ 7. Inspect and change Registry lifecycle state
14
+ 8. Inspect and download an artifact
15
+ 9. Prohibited shortcuts
15
16
 
16
17
  ## Select A Target
17
18
 
@@ -257,6 +258,57 @@ write parquet, or publish a DatasetArtifact. Treat HTTP `503` as retryable sourc
257
258
  freshness/missing-data failure and HTTP `400` as a contract error that requires a
258
259
  manifest or request change.
259
260
 
261
+ For a deployment-bound row or full sequence context, prepare one JSON object
262
+ containing `feature_retrieval`, `cutoff_time`, and `entity_keys`, then run:
263
+
264
+ ```bash
265
+ ml-platform --profile server fetch-inference-context /tmp/inference-context.json
266
+ ```
267
+
268
+ The `FeatureRetrievalSpec` must bind an existing inference-mode DatasetManifest
269
+ by project, dataset id, version, and manifest hash; its FeatureSet and contract
270
+ signature must match the resolved online contract. Context mode also fixes
271
+ `context_length`, `frequency`, `context_end`, `stride`, input-adapter hash, and
272
+ temporal-binding hash. Do not replace these immutable deployment bindings with
273
+ ad-hoc CLI flags. As with row fetch, this operation writes no artifact.
274
+
275
+ ## Inspect And Change Registry Lifecycle State
276
+
277
+ Lifecycle commands apply only to Parameter, Feature, and FeatureSet versions.
278
+ Before a mutation, inspect the exact resource and its references:
279
+
280
+ ```bash
281
+ ml-platform --profile server get-parameter-lifecycle <name> <version> --project <project>
282
+ ml-platform --profile server list-parameter-references <name> <version> --project <project> --direct
283
+ ml-platform --profile server list-parameter-references <name> <version> --project <project>
284
+ ```
285
+
286
+ Equivalent `get-*-lifecycle` and `list-*-references` commands exist for
287
+ `feature` and `feature-set`. The default reference query returns the transitive
288
+ closure; `--direct` returns only immediate dependents.
289
+
290
+ After explicit authorization for the exact resource, soft-delete it with
291
+ auditable metadata:
292
+
293
+ ```bash
294
+ ml-platform --profile server delete-parameter <name> <version> \
295
+ --project <project> --actor <actor> --reason '<reason>' \
296
+ --idempotency-key <stable-key>
297
+ ```
298
+
299
+ Restore uses the same audit fields and is rejected if the resource's own
300
+ dependencies are not active:
301
+
302
+ ```bash
303
+ ml-platform --profile server restore-parameter <name> <version> \
304
+ --project <project> --actor <actor> --reason '<reason>' \
305
+ --idempotency-key <stable-key>
306
+ ```
307
+
308
+ These operations never erase immutable specs, historical references, or
309
+ contract hashes. Do not use the Operator or Dataset deletion commands as a
310
+ substitute; they have separate lifecycle rules.
311
+
260
312
  ## Inspect And Download An Artifact
261
313
 
262
314
  ```bash
@@ -299,7 +351,7 @@ Use a structured Parquet reader for schema, row count, column order, and missing
299
351
 
300
352
  - Do not use `seed-demo v95` for business catalogs.
301
353
  - Do not call individual `add-*` commands when catalog `apply` is available for an end-to-end publication.
302
- - Do not use `delete-dataset`, `cancel-job`, or force overwrite operations unless separately requested.
354
+ - Do not use any `delete-*`, `restore-*`, `cancel-job`, or force overwrite operation unless separately requested.
303
355
  - Do not build images, deploy Kubernetes, or change service configuration in this workflow.
304
356
  - Do not claim a successful publication from `--dry-run` output.
305
357
  - Do not claim Chronon compile/backfill from a successful first-phase Operator build.
@@ -3,6 +3,35 @@
3
3
  Use this guide to explain what the platform can execute. It is not business
4
4
  evidence. A platform default never answers an unresolved business question.
5
5
 
6
+ ## Platform In One Paragraph
7
+
8
+ The platform is a versioned data-contract, registry, execution, and delivery
9
+ layer for algorithm projects. It turns confirmed source Parameters and approved
10
+ deterministic Operators into ordered Features and reproducible DatasetManifests,
11
+ then either materializes a batch DatasetArtifact or serves one causal realtime
12
+ read. It owns source adapters, quality/missingness/freshness evidence, lineage,
13
+ replay metadata, and Job execution; it does not own model training, model
14
+ evaluation, model serving, source-table creation, or Kubernetes administration.
15
+
16
+ The following distinctions are part of the platform contract:
17
+
18
+ - A `Parameter` is a public source-data contract, not an arbitrary raw table or
19
+ a model feature.
20
+ - An `Operator` is versioned executable code; a `Feature` is exactly one output
21
+ column; a `FeatureSet` is the consumer-visible column order.
22
+ - A `DatasetManifest` is a versioned request for time range, read policy,
23
+ rowsets, missing/endpoint rules, prediction, and one FeatureSet.
24
+ - A `DatasetArtifact` is an output of an authorized build, not evidence that the
25
+ source contract or business formula was semantically approved.
26
+ - Realtime inference is a causal cutoff read. It must not run a batch build,
27
+ write Parquet, or publish a DatasetArtifact.
28
+
29
+ Dry-run and registry resolution are structural evidence. They may not detect a
30
+ missing source relation; source-backed success must be established by the build
31
+ or realtime fetch response. Artifact metadata is separate from downloaded-file
32
+ evidence: schema, column order, file hashes, and numeric parity require the
33
+ complete files.
34
+
6
35
  ## Public Assets
7
36
 
8
37
  | Asset | Stores | Use when | Do not use for |
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: model-lifecycle-management
3
+ description: Manage ITSMP model training, evaluation, and immutable model packaging through the deployed platform API. Use when a user asks to register a trainer, validate or submit a training run, inspect/retry/cancel training jobs, configure or run governed evaluation, inspect evaluation evidence, validate a ModelArtifact, or create and inspect a ModelPackage. Do not use for feature catalog authoring, dataset builds, model deployment, Kubernetes administration, or image release engineering.
4
+ ---
5
+
6
+ # Model Lifecycle Management
7
+
8
+ Use the `ml-platform` executable installed with the same npm release as this Skill. Verify `ml-platform version`, `ml-platform show-config`, and server health before API operations. Never use a platform source checkout or a second client runtime.
9
+
10
+ ```text
11
+ DatasetArtifact -> TrainingRun -> TrainingJob -> ModelArtifact
12
+ -> EvaluationRun/Job -> EvaluationResult/Summary -> ModelPackage
13
+ ```
14
+
15
+ Read [references/training.md](references/training.md) for training work, [references/evaluation.md](references/evaluation.md) for governed evaluation, and [references/packaging.md](references/packaging.md) for ModelArtifact validation and packaging. Read only the references required by the request.
16
+
17
+ ## Shared Gates
18
+
19
+ - Establish exact immutable inputs, TrainerDefinition `v1`, runtime image digest, model inputs/targets, resources, evaluation policy, and package destination from authoritative contracts. Do not infer them from model names.
20
+ - Validate, register, submit, retry, cancel, and package creation are separate actions. Read-only discovery and validation do not authorize mutation or workload submission.
21
+ - Before submission, show the exact JSON path, immutable identities, expected workload, and target API. Obtain explicit authorization. Retry and cancel require separate authorization for the exact Job.
22
+ - Never use `latest` image tags. During development TrainerDefinition stays at `v1`; rebinding it follows the reviewed release procedure and requires confirming no active Job references the old definition.
23
+ - A succeeded Job is insufficient evidence. Training requires a valid ModelArtifact and signature; evaluation requires immutable Result/Summary evidence and coverage; packaging requires `READY`, an immutable image digest, manifest, and passing test report.
24
+ - Do not register caller-computed evaluation results. Workers produce predictions, metrics, decisions, and summaries from frozen DatasetArtifact rowsets.
25
+ - Keep credentials, registry secrets, Kubernetes details, and model binaries out of request JSON and reports.
26
+
27
+ Report immutable input identities, returned run/job/package IDs, status, validation or gate failures, and the next authorized action. Never claim deployment; it is outside this Skill.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "模型训练、评估与打包"
3
+ short_description: "管理 ITSMP 模型训练、评估与不可变模型打包流程"
4
+ default_prompt: "使用 $model-lifecycle-management 管理模型训练、评估或打包流程。"
@@ -0,0 +1,33 @@
1
+ # Evaluation Commands
2
+
3
+ ```bash
4
+ ml-platform --profile server register-evaluation-config evaluation-config.json
5
+ ml-platform --profile server register-metric-definition metric-definition.json
6
+ ml-platform --profile server register-executable-package executable-package.json
7
+ ml-platform --profile server verify-executable-package <name> <version>
8
+ ml-platform --profile server publish-executable-package <name> <version>
9
+ ```
10
+
11
+ Custom metric packages must be deterministic, digest-bound, network-restricted, verified, and separately authorized for publication.
12
+
13
+ ```bash
14
+ ml-platform --profile server validate-evaluation-run evaluation-request.json
15
+ ml-platform --profile server submit-evaluation evaluation-request.json
16
+ ml-platform --profile server get-evaluation-run <run-id>
17
+ ml-platform --profile server get-evaluation-job <job-id>
18
+ ml-platform --profile server list-evaluation-attempts <job-id>
19
+ ml-platform --profile server retry-evaluation-job <job-id>
20
+ ml-platform --profile server cancel-evaluation-job <job-id>
21
+ ```
22
+
23
+ An EvaluationRunRequest binds ModelArtifact(s), a frozen DatasetArtifact rowset, targets, EvaluationConfig, runtime identity, and optional baseline. Retry/cancel require separate authorization.
24
+
25
+ ```bash
26
+ ml-platform --profile server get-evaluation-result <result-id>
27
+ ml-platform --profile server get-evaluation-result-metrics <result-id>
28
+ ml-platform --profile server get-evaluation-result-artifacts <result-id>
29
+ ml-platform --profile server summarize-evaluation-run <run-id>
30
+ ml-platform --profile server get-evaluation-summary <summary-id>
31
+ ```
32
+
33
+ Report candidate, eligible, labeled and evaluable coverage; calculation space; slices; baseline identity; required decisions; runtime/package digests; and Result/Summary hashes. Exploratory or incomplete evidence is never a release gate pass.
@@ -0,0 +1,17 @@
1
+ # Model Packaging Commands
2
+
3
+ ```bash
4
+ ml-platform --profile server validate-model-artifact <artifact-id>
5
+ ml-platform --profile server create-model-package model-package-request.json
6
+ ml-platform --profile server get-model-package <package-id> --package-version <version>
7
+ ```
8
+
9
+ Confirm the ModelArtifact, signature, runtime, immutable base/builder/worker image digests, input modes, semantic package version, and destination repository. Creation submits a workload and needs separate authorization. Never put registry credentials in JSON.
10
+
11
+ A deliverable package is `READY` and includes its remote image digest, immutable reference, manifest, signature and adapter hashes, runtime capabilities, and a passing test report for the same digest. Other states are not deliverable.
12
+
13
+ ```bash
14
+ ml-platform --profile server cancel-model-package <package-id> --package-version <version>
15
+ ```
16
+
17
+ Cancellation requires exact-package authorization. Do not rebuild an immutable failed version; correct inputs and create a new semantic package version. This Skill does not deploy images or write model-center delivery status.
@@ -0,0 +1,19 @@
1
+ # Training Commands
2
+
3
+ ```bash
4
+ ml-platform --profile server register-trainer trainer-definition.json
5
+ ml-platform --profile server validate-training-run training-run.json
6
+ ml-platform --profile server register-training-run training-run.json
7
+ ml-platform --profile server submit-training-job training-job-request.json
8
+ ml-platform --profile server get-training-job <job-id>
9
+ ```
10
+
11
+ The TrainingRun binds immutable DatasetArtifact identities, DataSchema, ordered inputs, targets, InputAdapter, TrainerDefinition `v1`, reproducibility controls, and runtime identity. Validation proves compatibility only. Submit the run and Job separately after authorization and record spec hash and Job ID. Terminal success must expose a ModelArtifact ID; validate it before evaluation or packaging.
12
+
13
+ ```bash
14
+ ml-platform --profile server validate-model-artifact <artifact-id>
15
+ ml-platform --profile server retry-training-job <job-id>
16
+ ml-platform --profile server cancel-training-job <job-id>
17
+ ```
18
+
19
+ Retry and cancellation require exact-Job authorization. Never change a run contract while retrying; create a new immutable run when semantic inputs change.