@zhuoyuezs/ml-platform 0.2.8 → 0.2.9

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 CHANGED
@@ -17,5 +17,5 @@ npm install --global ./zhuoyuezs-ml-platform-*.tgz
17
17
  ml-platform version
18
18
  ```
19
19
 
20
- 运行已安装的 CLI 不需要 Python、`uv`、Git 仓库或平台源码。`uv` 只在 CI 构建阶段用于
21
- 执行现有的 release staging 脚本。
20
+ 运行已安装的 CLI 不需要 Python、`uv`、Git 仓库或平台源码。CI 构建阶段使用
21
+ `node:22-bookworm` 中的 Python 3.11 运行 release staging 脚本。
package/checksums.json CHANGED
@@ -2,23 +2,23 @@
2
2
  "files": [
3
3
  {
4
4
  "path": "runtime/business-client/README.md",
5
- "sha256": "sha256:ef7aabd0a1434dbf5d60018e357b018eeb08099de32a9b6a01a89d1f6764bc20",
6
- "size_bytes": 17026
5
+ "sha256": "sha256:9f81e4400f46c53952a0df6f7442d4c0b58cff4ca1be61f1daf785e111b3f8eb",
6
+ "size_bytes": 20557
7
7
  },
8
8
  {
9
9
  "path": "runtime/business-client/package-lock.json",
10
- "sha256": "sha256:706e7515b082fe20d4ad482347e222e41e34cd153c50bb947dc14f795cd5efb6",
10
+ "sha256": "sha256:9d19700ee4b348406bd0134f0539c37c2800e41a87dc130c1b0476eb2d23b931",
11
11
  "size_bytes": 1628
12
12
  },
13
13
  {
14
14
  "path": "runtime/business-client/package.json",
15
- "sha256": "sha256:6462d955b53f556056493bb6ee7acb506ced249e58d44532b9395cd8e1be684f",
15
+ "sha256": "sha256:d967d64289276cf1ce94f826a138f60fc93982b7478c1fb9f78f068f47f9d803",
16
16
  "size_bytes": 671
17
17
  },
18
18
  {
19
19
  "path": "runtime/business-client/src/catalog.js",
20
- "sha256": "sha256:369259ccfef1b079acf616eca1dc0040dda61cfb4b69941d0fcca30a658cd549",
21
- "size_bytes": 58749
20
+ "sha256": "sha256:94e4b43f248b752d9a9cec42e1f705849f01e4167bb570b9557b556c5be12c19",
21
+ "size_bytes": 59791
22
22
  },
23
23
  {
24
24
  "path": "runtime/business-client/src/cli-arguments.js",
@@ -27,8 +27,8 @@
27
27
  },
28
28
  {
29
29
  "path": "runtime/business-client/src/cli.js",
30
- "sha256": "sha256:cfabcb75a2e2b4e694c9c36390589299abcca3c0fd87e7171ea40b4a29d4e4eb",
31
- "size_bytes": 73361
30
+ "sha256": "sha256:c235053473fb55737dc2c6269a556ed9e4206b074327c0acaf8ac663f1ea00da",
31
+ "size_bytes": 79409
32
32
  },
33
33
  {
34
34
  "path": "runtime/business-client/src/config.js",
@@ -37,8 +37,8 @@
37
37
  },
38
38
  {
39
39
  "path": "runtime/business-client/src/http.js",
40
- "sha256": "sha256:088ef5fa067cc830a95db7d8ad3887ae58276179b524533f338daa781e243979",
41
- "size_bytes": 19452
40
+ "sha256": "sha256:91dc66d086deb5b5c0fd016560e6fddd369ccce9d430ce8f926bcae3f324cc86",
41
+ "size_bytes": 29035
42
42
  },
43
43
  {
44
44
  "path": "skills/data-source-exploration/SKILL.md",
@@ -52,8 +52,8 @@
52
52
  },
53
53
  {
54
54
  "path": "skills/feature-management/SKILL.md",
55
- "sha256": "sha256:6b16afd7fde7949879a9277e29268c203fd42ac1bd630108d3c0101a56db1558",
56
- "size_bytes": 34392
55
+ "sha256": "sha256:1435e380ede73fc9775bd0579ff9df08baf07b4eed8b1bbc174b5b394a72914f",
56
+ "size_bytes": 35359
57
57
  },
58
58
  {
59
59
  "path": "skills/feature-management/agents/openai.yaml",
@@ -107,18 +107,18 @@
107
107
  },
108
108
  {
109
109
  "path": "skills/feature-management/references/commands.md",
110
- "sha256": "sha256:bb6d8508bad62db6987df7d97802a679d5ccfc1fd7eb00f47b61b28396452be6",
111
- "size_bytes": 15872
110
+ "sha256": "sha256:375f953bac0263f9a40d217c99f029226d2a838c9510fce8d6e2324b8f548fb1",
111
+ "size_bytes": 18288
112
112
  },
113
113
  {
114
114
  "path": "skills/feature-management/references/contracts.md",
115
- "sha256": "sha256:a9f6c8d6c8b1220a26a282f3ff73db54c6359372df02c322159087507ca0318d",
116
- "size_bytes": 30894
115
+ "sha256": "sha256:717b81364f8508cd73d52f18d7b5dd012a3f3638921a8a5a93c41991a4f27998",
116
+ "size_bytes": 38182
117
117
  },
118
118
  {
119
119
  "path": "skills/feature-management/references/operator-authoring.md",
120
- "sha256": "sha256:2ecfccbc9fe771bc1103db679d67af78847307eeb440f680f4055c35405ac923",
121
- "size_bytes": 6089
120
+ "sha256": "sha256:68ec5861800a6623db8e3cfae8e3363493c54772c4719d60dc66e4f4f0d3a347",
121
+ "size_bytes": 12570
122
122
  },
123
123
  {
124
124
  "path": "skills/feature-management/references/platform-capability-guide.md",
@@ -132,8 +132,8 @@
132
132
  },
133
133
  {
134
134
  "path": "skills/model-deployment-management/SKILL.md",
135
- "sha256": "sha256:e4ec21765e23a02ad86bd6b0c92414eee182d74a9f7032e738711c823478ecfe",
136
- "size_bytes": 4960
135
+ "sha256": "sha256:84c58dbc71046ead45e3bd1aea55a5f98908e633314dbd088ce6d2c83987011c",
136
+ "size_bytes": 5195
137
137
  },
138
138
  {
139
139
  "path": "skills/model-deployment-management/agents/openai.yaml",
@@ -147,8 +147,8 @@
147
147
  },
148
148
  {
149
149
  "path": "skills/model-deployment-management/references/operations.md",
150
- "sha256": "sha256:2eb0fb62d971e21bfbdc1f398a5dcb91916dfccec35e7cbfa4a6404799bfdfb3",
151
- "size_bytes": 6165
150
+ "sha256": "sha256:262ac8d3ab7524a9b30a14a95550a28821626a8832fd967fe3e9acd015f8c7b2",
151
+ "size_bytes": 6987
152
152
  },
153
153
  {
154
154
  "path": "skills/model-deployment-management/references/troubleshooting.md",
@@ -157,8 +157,8 @@
157
157
  },
158
158
  {
159
159
  "path": "skills/model-lifecycle-management/SKILL.md",
160
- "sha256": "sha256:5880a2f7dc5077767c05f3c2e34be30699d63ea620a08b7ed6944c6b5b29f801",
161
- "size_bytes": 6482
160
+ "sha256": "sha256:55f0172c0b79a04b8ddfeb6bb9f617e7cb5da622b6bd43b3dbae2d3318f6f877",
161
+ "size_bytes": 7447
162
162
  },
163
163
  {
164
164
  "path": "skills/model-lifecycle-management/agents/openai.yaml",
@@ -167,13 +167,13 @@
167
167
  },
168
168
  {
169
169
  "path": "skills/model-lifecycle-management/references/discovery.md",
170
- "sha256": "sha256:ad4623e84b8d972357497a55df0aed84be5ada59ef197f6a217f19ce97821195",
171
- "size_bytes": 5789
170
+ "sha256": "sha256:6ae9afc7169f4e51eefa8ac124b2fa5d9d26bed13ebbc57ec9fbbbcdc6b1960c",
171
+ "size_bytes": 6643
172
172
  },
173
173
  {
174
174
  "path": "skills/model-lifecycle-management/references/evaluation.md",
175
- "sha256": "sha256:e5c82007aa19632565ac3b9862b2c12e896c08f21171985795d342992324a683",
176
- "size_bytes": 10310
175
+ "sha256": "sha256:ba16117c0bf37ac92e28dc9b5e3bda266ac6ad29993c3d4bfcc1c955bb0ab3fa",
176
+ "size_bytes": 16573
177
177
  },
178
178
  {
179
179
  "path": "skills/model-lifecycle-management/references/model-comparison.md",
@@ -182,18 +182,18 @@
182
182
  },
183
183
  {
184
184
  "path": "skills/model-lifecycle-management/references/packaging.md",
185
- "sha256": "sha256:10a30f3408c1c387c215603db4d4aa436ce3bdb925e060139c3283653dd18505",
186
- "size_bytes": 7949
185
+ "sha256": "sha256:da2bde7db390643efbf7a3c8dbb668b0ffe69a1d62ec2dd6436cec5b05cc14ec",
186
+ "size_bytes": 9921
187
187
  },
188
188
  {
189
189
  "path": "skills/model-lifecycle-management/references/training-contracts.md",
190
- "sha256": "sha256:eb774709bc94e07a51a539e1a871202bd7569ae2c4a3d0fa4ebd2b1784ee08a6",
191
- "size_bytes": 17803
190
+ "sha256": "sha256:45568961f933b134e912a20b4ad37d435616d44c3159f302fbccaff2ffe16389",
191
+ "size_bytes": 27143
192
192
  },
193
193
  {
194
194
  "path": "skills/model-lifecycle-management/references/training.md",
195
- "sha256": "sha256:6732128fd6e0ef1fd1d40340bccd4ef555936c9050a7ccbf5ccbfc6c5854a8c3",
196
- "size_bytes": 8369
195
+ "sha256": "sha256:654d3d45bfae4906f7f37898d09fa1fa84ecaab46346190c439f9f3fc0e17f95",
196
+ "size_bytes": 9691
197
197
  },
198
198
  {
199
199
  "path": "skills/model-lifecycle-management/references/zero-shot-inference.md",
@@ -202,8 +202,8 @@
202
202
  },
203
203
  {
204
204
  "path": "skills/prediction-report-management/SKILL.md",
205
- "sha256": "sha256:7c751b1c6176a807f287095ece6463747007f9277afa82abad0c6035f7cbdca5",
206
- "size_bytes": 6489
205
+ "sha256": "sha256:fb1f8c579d392cc4d030d41b5c3b06cd59ab0c488de343bae93cbc87a27dd650",
206
+ "size_bytes": 8707
207
207
  },
208
208
  {
209
209
  "path": "skills/prediction-report-management/agents/openai.yaml",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhuoyuezs/ml-platform",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "ML 数据平台 business CLI 与 Agent Skills 管理工具",
5
5
  "bin": {
6
6
  "ml-platform": "scripts/main.js"
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:eec6e0e65cf300d6fbfbddcc13bdee6af324409567222454c9ba7abc812a79f3",
17
- "version": "0.8.6"
16
+ "sha256": "sha256:6b8adc18d15ead31da78bd6e4274d17bb7277edcb0d76a9b476ae77a3be22812",
17
+ "version": "0.8.7"
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.2.8",
28
+ "release_version": "0.2.9",
29
29
  "runtime_requirements": {
30
30
  "node": ">=18",
31
31
  "os": [
@@ -37,34 +37,34 @@
37
37
  "skills": {
38
38
  "data-source-exploration": {
39
39
  "path": "skills/data-source-exploration",
40
- "requires_cli": ">=0.8.6 <0.9.0",
41
- "revision": "0.2.8",
40
+ "requires_cli": ">=0.8.7 <0.9.0",
41
+ "revision": "0.2.9",
42
42
  "sha256": "sha256:6cb1cad3e9531e5098582c532288edd9838c2acab2b324794ec99d2550418f32"
43
43
  },
44
44
  "feature-management": {
45
45
  "path": "skills/feature-management",
46
- "requires_cli": ">=0.8.6 <0.9.0",
47
- "revision": "0.2.8",
48
- "sha256": "sha256:91c59ad073fc6d13ae024ba3f2f5652742ebb7dbee8a0ad4573332e8f009a580"
46
+ "requires_cli": ">=0.8.7 <0.9.0",
47
+ "revision": "0.2.9",
48
+ "sha256": "sha256:2bd8e19ffe43e414fe74a269a75ccfdb88e66c7f2a52df6230a929321dbb54ea"
49
49
  },
50
50
  "model-deployment-management": {
51
51
  "path": "skills/model-deployment-management",
52
- "requires_cli": ">=0.8.6 <0.9.0",
53
- "revision": "0.2.8",
54
- "sha256": "sha256:a83471a2914210065b4354c50b9ddb8f6e4be582739636a116b9098524948a11"
52
+ "requires_cli": ">=0.8.7 <0.9.0",
53
+ "revision": "0.2.9",
54
+ "sha256": "sha256:26cbe95ca26371f3b34a4b83a1f185eeb879f64f099d9dbc30e4cc4ec81d9c64"
55
55
  },
56
56
  "model-lifecycle-management": {
57
57
  "path": "skills/model-lifecycle-management",
58
- "requires_cli": ">=0.8.6 <0.9.0",
59
- "revision": "0.2.8",
60
- "sha256": "sha256:113c673d383cdfb191d3c0efbc91aa0f950e2db0ca03a163209680c6dc1683a0"
58
+ "requires_cli": ">=0.8.7 <0.9.0",
59
+ "revision": "0.2.9",
60
+ "sha256": "sha256:40d8b5eef72a937c208c3607b41666015ac143f604fb0521e0953659306182b2"
61
61
  },
62
62
  "prediction-report-management": {
63
63
  "path": "skills/prediction-report-management",
64
- "requires_cli": ">=0.8.6 <0.9.0",
65
- "revision": "0.2.8",
66
- "sha256": "sha256:671f65e54bf464c7fa46398d4fae31a6317290cf0987fc5c886e56a89309fafc"
64
+ "requires_cli": ">=0.8.7 <0.9.0",
65
+ "revision": "0.2.9",
66
+ "sha256": "sha256:0aee744970236f0357aa9e264e31e144b3cb6fab4a3ea6a01f184dcdaef7e846"
67
67
  }
68
68
  },
69
- "source_commit": "f05d2c98eddd9a4d2cdf47a1d7a3133fc421ffc9"
69
+ "source_commit": "e0c33cf9f1d052d2a24a574393fdf804438a4543"
70
70
  }
@@ -81,8 +81,10 @@ ml-platform get-deployment <deployment-id>
81
81
  ml-platform get-deployment-events <deployment-id> --limit 100 --offset 0
82
82
  ```
83
83
 
84
- `list-deployments` 也支持按 `service`、`environment`、`state` 和自由文本 `q` 过滤,返回平台
85
- Deployment Controller 管理的部署,不把模型中心回写的 delivery 状态当作平台部署。
84
+ `list-deployments` 支持 `--service` 对 service 名称做不区分大小写的子串匹配(忽略首尾空白),
85
+ 也支持按 `environment`、`state` 和跨字段自由文本 `q` 过滤,返回平台
86
+ Deployment Controller 管理的部署,不把模型中心回写的 delivery 状态当作平台部署。列表支持
87
+ `--sort-by created_at|updated_at` 和 `--sort-order asc|desc`,默认按 `deployment_id` 升序。
86
88
 
87
89
  部署业务分类由平台控制面管理。领域固定为 `power`(电力)、`blast_furnace`(高炉)和
88
90
  `other`(其他),场景可按领域自定义:
@@ -126,8 +128,11 @@ ml-platform list-deployments --business-domain-id power --scenario-id energy_sto
126
128
  "values": {
127
129
  "source": "predictions", "type": "array",
128
130
  "items": {"fields": {
131
+ "date": {"source": "target_time", "format": "%Y-%m-%d", "timezone": "Asia/Shanghai",
132
+ "midnight_as_24": true, "type": "string"},
129
133
  "time": {"source": "target_time", "format": "%H:%M", "timezone": "Asia/Shanghai",
130
134
  "midnight_as_24": true, "type": "string"},
135
+ "timestamp": {"source": "target_time", "type": "string"},
131
136
  "value": {"source": "value", "type": "number", "scale": 1.0, "offset": 0.0}
132
137
  }}
133
138
  }
@@ -159,8 +164,11 @@ ml-platform list-deployments --business-domain-id power --scenario-id energy_sto
159
164
  `target_time`。`source` 引用平台标准预测结果中的字段;`fields` 生成嵌套对象,`items` 将数组逐项转换,
160
165
  `constant` 写入固定值,`template` 可拼接已存在的字符串字段。`format` 可格式化时间;需要按业务时区格式化时,
161
166
  同时设置 `timezone`(IANA 名称),例如 `2026-09-10T16:00:00Z` 配置 `Asia/Shanghai` 后得到 `2026-09-11`;
162
- 如果目标接口把本地日界的午夜表达为 `24:00`,在包含 `%H` 的时间格式上设置 `midnight_as_24: true`,例如
163
- `%H:%M` 会将本地 `00:00` 映射为 `24:00`,其他时刻保持不变;
167
+ 如果目标接口把本地日界的午夜表达为前一日的 `24:00`,在业务日期和时间两个字段上均设置
168
+ `midnight_as_24: true` 和 `timezone: "Asia/Shanghai"`:`%Y-%m-%d` 输出前一日,`%H:%M` 输出 `24:00`。
169
+ 例如 `2026-10-31T00:00:00+08:00` 对应 `date=2026-10-30`、`time=24:00`,其他时刻保持不变。
170
+ `timestamp` 直接引用 `target_time`,MW 数值直接引用 `value`,不添加格式化或数值换算即可保持原值。
171
+ 表示起报本地日期的 `origin_date` 不设置 `midnight_as_24`;
164
172
  `scale` 和 `offset` 可执行受控数值换算,`type` 校验最终类型。`token_path`、业务成功码和
165
173
  `/forecast/report` 只是示例,必须按目标接口的真实响应和路径配置。
166
174
  映射字段缺失或类型不匹配时不会发送请求,Run 会记录 `payload_mapping_failed`。若响应提供有效期,可设置 `expires_in_path` 与 `expires_in_unit`;否则服务端默认按 7200 秒缓存,
@@ -304,10 +312,62 @@ Training Run and Job validation also return `effective_trainer_parameters`;
304
312
  for Job preflight it must match `job_spec.trainer_parameters` and represents
305
313
  the complete mapping delivered to the Worker after defaults are resolved.
306
314
 
315
+ ## Artifact file downloads
316
+
317
+ ```bash
318
+ ml-platform download-model-artifact <artifact_id> --out-dir /tmp/model [--file runtime.json] [--project <project>]
319
+ ml-platform download-evaluation-result-artifact <result_id> --out-dir /tmp/eval [--file predictions.parquet]
320
+ ml-platform download-dataset-artifact <dataset_id> <artifact_manifest_hash> --project <project> --out-dir /tmp/ds
321
+ ```
322
+
323
+ The lifecycle commands read the registry file index (`get-model-artifact` /
324
+ `get-evaluation-result-artifacts`), download every indexed file or each
325
+ repeated `--file`, and keep a file only when its sha256 equals the registry
326
+ record; a mismatch removes the output directory. Names outside the index fail
327
+ before any file request. They call the read-only
328
+ `GET /model-artifacts/{id}/files/{path}` and
329
+ `GET /evaluation-results/{id}/files/{path}` routes, which verify the sha256
330
+ server-side, accept an optional `project` scope, and refuse files above
331
+ `DATA_PLATFORM_LIFECYCLE_ARTIFACT_FILE_MAX_BYTES` (default 2 GiB).
332
+
333
+ A DatasetArtifact is keyed by the artifact manifest hash, not the registered
334
+ manifest hash. `build-dataset --wait`, `build-registered-dataset --wait` and
335
+ `wait-job` add an `artifact` object (`project`, `dataset_id`,
336
+ `dataset_version`, `manifest_hash`, `artifact_uri`, `row_count`) to a succeeded
337
+ build Job; use `artifact.manifest_hash` for downloads and dataset bindings. The
338
+ Job's top-level `manifest_hash` is unchanged.
339
+
340
+ `--idempotency-key` on `create-project`, `register-training-run`,
341
+ `submit-training-job`, `register-evaluation-config`, `submit-evaluation` and
342
+ `create-model-package` is sent as the `Idempotency-Key` header, which the
343
+ current server does not enforce. Inspect platform state before retrying an
344
+ uncertain submission.
345
+
307
346
  Maintainers can export the current API, including uncommitted schema changes,
308
347
  with `uv run python scripts/generate_openapi_schema.py --working-tree` from the
309
348
  repository root; use `--check` with it to verify the committed snapshot.
310
349
 
350
+ ## Connection handling
351
+
352
+ Every request is sent with `Connection: close`, so the CLI never writes a new
353
+ request onto a pooled keep-alive socket that the API (uvicorn closes idle
354
+ sockets after 5 s) or a slow VPN path has already dropped. Connection errors
355
+ report the underlying cause, for example
356
+ `fetch failed (UND_ERR_SOCKET: other side closed)` or
357
+ `request timed out after 30s (...)`.
358
+
359
+ Automatic retry (at most 3 retries, 0.5 s / 1 s / 2 s backoff, each attempt
360
+ with its own `--timeout`) happens only before response headers arrive:
361
+
362
+ - `GET`/`HEAD` (including `wait-job` polling and downloads): connection
363
+ errors such as `ECONNREFUSED`, `ECONNRESET`, `UND_ERR_SOCKET`,
364
+ `UND_ERR_CONNECT_TIMEOUT`, and the request timeout.
365
+ - `POST`/`PUT`/`PATCH`/`DELETE`: only `ECONNREFUSED` and
366
+ `UND_ERR_CONNECT_TIMEOUT`, where no TCP connection existed and the request
367
+ cannot have reached the server. Any other failure is reported once with
368
+ `not retried because the server may have received it`; inspect platform
369
+ state before resubmitting.
370
+
311
371
  ## Agent 宿主接入
312
372
 
313
373
  Coding agent 加载已安装的 Skills 后,通过原生 shell/exec 直接运行普通 `ml-platform` 命令。
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@zhuoyuezs/ml-platform-business-client",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@zhuoyuezs/ml-platform-business-client",
9
- "version": "0.8.6",
9
+ "version": "0.8.7",
10
10
  "license": "UNLICENSED",
11
11
  "bin": {
12
12
  "ml-platform": "src/cli.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhuoyuezs/ml-platform-business-client",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "private": true,
5
5
  "description": "JavaScript implementation of the ML Platform business client",
6
6
  "bin": {
@@ -454,11 +454,27 @@ function canonical(value) { if (Array.isArray(value))
454
454
  return value.map(canonical); if (value && typeof value === "object")
455
455
  return Object.fromEntries(Object.keys(value).sort().map((name) => [name, canonical(value[name])])); return value; }
456
456
  function equal(left, right) { return JSON.stringify(canonical(left)) === JSON.stringify(canonical(right)); }
457
- function assertImmutable(label, assetKey, candidate, existing) { if (existing && !equal(candidate, existing)) {
458
- const error = new Error(`${label} version is immutable: ${assetKey}`);
459
- error.code = "EEXIST";
460
- throw error;
461
- } }
457
+ function immutableError(label, assetKey) { const error = new Error(`${label} version is immutable: ${assetKey}`); error.code = "EEXIST"; return error; }
458
+ function assertImmutable(label, assetKey, candidate, existing) { if (existing && !equal(candidate, existing))
459
+ throw immutableError(label, assetKey); }
460
+ // A registered DatasetManifest carries server-filled defaults, including nested ones, that a catalog may
461
+ // omit. A raw mismatch is therefore settled by the server's normalized contract hash; servers without
462
+ // /datasets/validate keep the raw-comparison conflict.
463
+ async function assertDatasetImmutable(client, assetKey, candidate, existing) {
464
+ if (!existing || equal(candidate, existing))
465
+ return;
466
+ let result;
467
+ try {
468
+ result = await client.post("/datasets/validate", candidate);
469
+ }
470
+ catch (error) {
471
+ if (/HTTP 40[45]\b/.test(String(error.message)))
472
+ throw immutableError("datasets", assetKey);
473
+ throw error;
474
+ }
475
+ if (!result?.registered_manifest_hash || result.manifest_hash !== result.registered_manifest_hash)
476
+ throw immutableError("datasets", assetKey);
477
+ }
462
478
  function sha256File(file) { return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex")}`; }
463
479
  async function applyCatalog(catalog, client, dryRun, targetProject) {
464
480
  validateCatalogProject(catalog, targetProject);
@@ -483,7 +499,10 @@ async function applyCatalog(catalog, client, dryRun, targetProject) {
483
499
  }
484
500
  const plain = async (assets, existing, label, endpoint, keyFn = key, params) => { for (const asset of assets) {
485
501
  const assetKey = keyFn(asset);
486
- assertImmutable(label, assetKey, asset, existing.get(assetKey));
502
+ if (label === "datasets")
503
+ await assertDatasetImmutable(client, assetKey, asset, existing.get(assetKey));
504
+ else
505
+ assertImmutable(label, assetKey, asset, existing.get(assetKey));
487
506
  if (existing.has(assetKey))
488
507
  summary[label].unchanged += 1;
489
508
  else if (dryRun)
@@ -25,7 +25,7 @@ const BUSINESS_COMMANDS = new Set([
25
25
  "get-parameter", "get-feature", "get-feature-set", "get-operator",
26
26
  "delete-parameter", "delete-feature", "delete-feature-set",
27
27
  "restore-parameter", "restore-feature", "restore-feature-set",
28
- "get-parameter-lifecycle", "get-feature-lifecycle", "get-feature-set-lifecycle",
28
+ "get-parameter-lifecycle", "get-feature-lifecycle", "get-feature-set-lifecycle", "get-parameter-freshness",
29
29
  "list-parameter-references", "list-feature-references", "list-feature-set-references",
30
30
  "list-parameter-versions", "list-feature-versions", "list-feature-set-versions", "list-operator-versions",
31
31
  "add-parameter", "add-feature", "add-feature-set", "add-operator", "publish-operator",
@@ -38,6 +38,7 @@ const BUSINESS_COMMANDS = new Set([
38
38
  "register-executable-package", "verify-executable-package", "publish-executable-package",
39
39
  "validate-evaluation-run", "submit-evaluation", "get-evaluation-run", "get-evaluation-job", "retry-evaluation-job", "cancel-evaluation-job", "list-evaluation-attempts",
40
40
  "get-evaluation-result", "get-evaluation-result-metrics", "get-evaluation-result-artifacts", "summarize-evaluation-run", "get-evaluation-summary",
41
+ "download-model-artifact", "download-evaluation-result-artifact",
41
42
  "create-model-package", "get-model-package", "cancel-model-package", "validate-model-artifact", "register-model-artifact",
42
43
  "list-business-domains", "list-scenarios", "create-scenario", "put-deployment-metadata",
43
44
  "list-deployment-targets", "validate-deployment", "create-deployment", "list-deployments",
@@ -91,6 +92,7 @@ const COMMAND_USAGE = {
91
92
  "restore-feature": "restore-feature NAME VERSION [--project PROJECT] [--actor ACTOR] [--reason TEXT] [--idempotency-key KEY]",
92
93
  "restore-feature-set": "restore-feature-set NAME VERSION [--project PROJECT] [--actor ACTOR] [--reason TEXT] [--idempotency-key KEY]",
93
94
  "get-parameter-lifecycle": "get-parameter-lifecycle NAME VERSION [--project PROJECT]",
95
+ "get-parameter-freshness": "get-parameter-freshness NAME VERSION [--project PROJECT] [--lookback DURATION] [--end TIMESTAMP] [--online]",
94
96
  "get-feature-lifecycle": "get-feature-lifecycle NAME VERSION [--project PROJECT]",
95
97
  "get-feature-set-lifecycle": "get-feature-set-lifecycle NAME VERSION [--project PROJECT]",
96
98
  "list-parameter-references": "list-parameter-references NAME VERSION [--project PROJECT] [--direct]",
@@ -159,6 +161,8 @@ const COMMAND_USAGE = {
159
161
  "get-evaluation-result": "get-evaluation-result RESULT_ID",
160
162
  "get-evaluation-result-metrics": "get-evaluation-result-metrics RESULT_ID",
161
163
  "get-evaluation-result-artifacts": "get-evaluation-result-artifacts RESULT_ID",
164
+ "download-model-artifact": "download-model-artifact ARTIFACT_ID --out-dir PATH [--file NAME] [--project PROJECT] [--force]",
165
+ "download-evaluation-result-artifact": "download-evaluation-result-artifact RESULT_ID --out-dir PATH [--file NAME] [--project PROJECT] [--force]",
162
166
  "summarize-evaluation-run": "summarize-evaluation-run RUN_ID",
163
167
  "get-evaluation-summary": "get-evaluation-summary SUMMARY_ID",
164
168
  "create-model-package": "create-model-package REQUEST_JSON [--idempotency-key KEY]",
@@ -173,7 +177,7 @@ const COMMAND_USAGE = {
173
177
  "list-deployment-targets": "list-deployment-targets",
174
178
  "validate-deployment": "validate-deployment SPEC_JSON --evaluation-id ID",
175
179
  "create-deployment": "create-deployment SPEC_JSON --evaluation-id ID",
176
- "list-deployments": "list-deployments [--service SERVICE] [--environment ENV] [--package-id ID] [--package-version VERSION] [--model-artifact-id ID] [--state STATE] [--business-domain-id ID] [--scenario-id ID] [-q QUERY] [--limit N] [--offset N]",
180
+ "list-deployments": "list-deployments [--service TEXT] [--environment ENV] [--package-id ID] [--package-version VERSION] [--model-artifact-id ID] [--state STATE] [--business-domain-id ID] [--scenario-id ID] [-q QUERY] [--sort-by deployment_id|created_at|updated_at] [--sort-order asc|desc] [--limit N] [--offset N]",
177
181
  "get-deployment": "get-deployment DEPLOYMENT_ID",
178
182
  "wait-deployment": "wait-deployment DEPLOYMENT_ID [--poll-interval SECONDS] [--wait-timeout-seconds SECONDS]",
179
183
  "reconcile-deployment": "reconcile-deployment DEPLOYMENT_ID",
@@ -200,10 +204,22 @@ const COMMAND_USAGE = {
200
204
  "get-prediction-report-run": "get-prediction-report-run RUN_ID",
201
205
  apply: "apply CATALOG_DIRECTORY [--dry-run] [--project PROJECT]",
202
206
  };
207
+ const IDEMPOTENCY_HEADER_NOTE = "--idempotency-key is sent as the Idempotency-Key header; the current server does not enforce it, so inspect platform state before retrying an uncertain submission.";
208
+ const BUILD_WAIT_NOTE = "With --wait, a succeeded result adds artifact.manifest_hash: the DatasetArtifact key for get/download-dataset-artifact and training/evaluation dataset bindings. The top-level manifest_hash identifies the submitted manifest and is not an artifact key.";
209
+ // Help-only notes. Keep them out of COMMAND_USAGE, which is also the argv grammar.
210
+ const COMMAND_NOTES = {
211
+ "download-dataset-artifact": "MANIFEST_HASH is the artifact manifest hash (list-dataset-artifacts, or artifact.manifest_hash from build-registered-dataset --wait), not the registered manifest hash.",
212
+ "build-dataset": BUILD_WAIT_NOTE,
213
+ "build-registered-dataset": BUILD_WAIT_NOTE,
214
+ "wait-job": "A succeeded build Job result adds artifact.manifest_hash, the DatasetArtifact key.",
215
+ "download-model-artifact": "Downloads every indexed ModelArtifact file, or only each --file NAME, and verifies each sha256 against the registry record. --project fails closed when the artifact belongs to another project.",
216
+ "download-evaluation-result-artifact": "Downloads every indexed EvaluationResult file (for example predictions.parquet, metrics.json, runtime.json), or only each --file NAME, and verifies each sha256 against the registry record.",
217
+ ...Object.fromEntries(["create-project", "register-training-run", "submit-training-job", "register-evaluation-config", "submit-evaluation", "create-model-package"].map((name) => [name, IDEMPOTENCY_HEADER_NOTE])),
218
+ };
203
219
  function usage(command) {
204
220
  const prefix = "ml-platform [--profile server] [--api-url URL] [--request-timeout-seconds SECONDS]";
205
221
  if (command)
206
- return `usage: ${prefix} ${COMMAND_USAGE[command]}\n`;
222
+ return `usage: ${prefix} ${COMMAND_USAGE[command]}\n${COMMAND_NOTES[command] ? `\n${COMMAND_NOTES[command]}\n` : ""}`;
207
223
  return `usage: ${prefix} COMMAND [ARGS]\n\ncommands:\n ${[...BUSINESS_COMMANDS].join("\n ")}\n`;
208
224
  }
209
225
  function parse(args) {
@@ -363,12 +379,33 @@ async function submitBuild(api, manifest, build) {
363
379
  if (build.wait) {
364
380
  if (!result || typeof result !== "object" || !result.job_id)
365
381
  throw new Error("server build did not return a job_id to wait for");
366
- result = await waitForJob(api, String(result.job_id), build.pollInterval, build.waitTimeout);
382
+ result = withBuildArtifact(await waitForJob(api, String(result.job_id), build.pollInterval, build.waitTimeout));
367
383
  if (["failed", "cancelled"].includes(result.status))
368
384
  exitCode = 1;
369
385
  }
370
386
  return { result, exitCode };
371
387
  }
388
+ // A build Job's top-level manifest_hash identifies the submitted (registered)
389
+ // manifest. The published DatasetArtifact is keyed by the resolved contract hash,
390
+ // so surface it additively as `artifact` for downloads and dataset bindings.
391
+ function withBuildArtifact(job) {
392
+ if (!job || typeof job !== "object" || Array.isArray(job) || job.status !== "succeeded")
393
+ return job;
394
+ const resolved = job.resolved_manifest;
395
+ if (!resolved || typeof resolved !== "object" || !resolved.manifest_hash)
396
+ return job;
397
+ return {
398
+ ...job,
399
+ artifact: {
400
+ project: resolved.project ?? job.manifest?.project ?? "default",
401
+ dataset_id: resolved.dataset_id ?? job.manifest?.dataset_id ?? null,
402
+ dataset_version: resolved.dataset_version ?? job.manifest?.dataset_version ?? null,
403
+ manifest_hash: resolved.manifest_hash,
404
+ artifact_uri: resolved.artifact_uri ?? null,
405
+ row_count: resolved.row_count ?? null,
406
+ },
407
+ };
408
+ }
372
409
  async function waitForJob(api, jobId, pollInterval, timeoutSeconds) {
373
410
  if (!(pollInterval > 0))
374
411
  throw new Error("job poll interval must be positive");
@@ -559,6 +596,21 @@ async function runBusinessCli(argv, output = { write: text => process.stdout.wri
559
596
  actor: take(rest, "--actor") || "anonymous", reason: take(rest, "--reason", ""), idempotency_key: take(rest, "--idempotency-key"),
560
597
  });
561
598
  }
599
+ else if (options.command === "get-parameter-freshness") {
600
+ const name = positional(rest, "parameter name");
601
+ const version = positional(rest, "parameter version");
602
+ const project = take(rest, "--project", "default");
603
+ const params = {};
604
+ const lookback = take(rest, "--lookback");
605
+ const end = take(rest, "--end");
606
+ if (lookback !== undefined)
607
+ params.lookback = lookback;
608
+ if (end !== undefined)
609
+ params.end = end;
610
+ if (boolean(rest, "--online"))
611
+ params.online = true;
612
+ result = await client(options).get(`/parameters/${encodeURIComponent(project)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}/freshness`, params);
613
+ }
562
614
  else if (/^get-(parameter|feature|feature-set)-lifecycle$/.test(options.command)) {
563
615
  const name = positional(rest, "resource name");
564
616
  const version = positional(rest, "resource version");
@@ -730,7 +782,7 @@ async function runBusinessCli(argv, output = { write: text => process.stdout.wri
730
782
  const jobId = positional(rest, "job id");
731
783
  const poll = finiteNumber(take(rest, "--poll-interval", 2), "--poll-interval");
732
784
  const rawTimeout = take(rest, "--wait-timeout-seconds");
733
- result = await waitForJob(client(options), jobId, poll, rawTimeout === undefined ? undefined : finiteNumber(rawTimeout, "--wait-timeout-seconds"));
785
+ result = withBuildArtifact(await waitForJob(client(options), jobId, poll, rawTimeout === undefined ? undefined : finiteNumber(rawTimeout, "--wait-timeout-seconds")));
734
786
  if (["failed", "cancelled"].includes(result.status))
735
787
  exitCode = 1;
736
788
  }
@@ -868,6 +920,8 @@ async function runBusinessCli(argv, output = { write: text => process.stdout.wri
868
920
  business_domain_id: take(rest, "--business-domain-id"),
869
921
  scenario_id: take(rest, "--scenario-id"),
870
922
  q: take(rest, "--q", take(rest, "-q")),
923
+ sort_by: take(rest, "--sort-by"),
924
+ sort_order: take(rest, "--sort-order"),
871
925
  };
872
926
  boolean(rest, "--all");
873
927
  result = await listCollection(client(options), "/deployments", params, rest);
@@ -1074,6 +1128,36 @@ async function runBusinessCli(argv, output = { write: text => process.stdout.wri
1074
1128
  const suffix = options.command.endsWith("-metrics") ? "/metrics" : options.command.endsWith("-artifacts") ? "/artifacts" : "";
1075
1129
  result = await client(options).get(`/evaluation-results/${encodeURIComponent(resultId)}${suffix}`);
1076
1130
  }
1131
+ else if (options.command === "download-model-artifact" || options.command === "download-evaluation-result-artifact") {
1132
+ const model = options.command === "download-model-artifact";
1133
+ const resourceId = positional(rest, model ? "model artifact id" : "evaluation result id");
1134
+ const outDir = take(rest, "--out-dir");
1135
+ const project = take(rest, "--project");
1136
+ const force = boolean(rest, "--force");
1137
+ if (!outDir)
1138
+ throw new Error("--out-dir is required");
1139
+ const files = [];
1140
+ while (rest.includes("--file"))
1141
+ files.push(take(rest, "--file"));
1142
+ const api = client(options);
1143
+ let index;
1144
+ let owner = project;
1145
+ if (model) {
1146
+ const record = await api.get(`/model-artifacts/${encodeURIComponent(resourceId)}`);
1147
+ if (project !== undefined && record.project !== project)
1148
+ throw new Error(`model artifact ${resourceId} belongs to project ${JSON.stringify(record.project)}, not ${JSON.stringify(project)}`);
1149
+ index = record.files;
1150
+ owner = record.project;
1151
+ }
1152
+ else {
1153
+ const listing = await api.get(`/evaluation-results/${encodeURIComponent(resourceId)}/artifacts`);
1154
+ if (!Array.isArray(listing.items))
1155
+ throw new Error("evaluation result artifact listing has no items");
1156
+ index = Object.fromEntries(listing.items.map((item) => [String(item.name), String(item.sha256)]));
1157
+ }
1158
+ const downloaded = await api.downloadLifecycleArtifactFiles(model ? "model-artifacts" : "evaluation-results", resourceId, index, files, resolvedPath(outDir), { force, project });
1159
+ result = { [model ? "model_artifact_id" : "result_id"]: resourceId, ...downloaded, project: owner ?? null };
1160
+ }
1077
1161
  else if (/^(get-evaluation-config|get-metric-definition)$/.test(options.command)) {
1078
1162
  const name = positional(rest, "resource name");
1079
1163
  const version = positional(rest, "resource version");
@@ -1137,7 +1221,7 @@ async function listCollection(api, endpoint, params, rest) {
1137
1221
  throw new Error("offset must be >= 0");
1138
1222
  return api.get(endpoint, { ...params, limit, offset });
1139
1223
  }
1140
- module.exports = { BUSINESS_COMMANDS, fetchAll, listCollection, parse, runBusinessCli, usage, waitForDeployment, waitForJob };
1224
+ module.exports = { BUSINESS_COMMANDS, COMMAND_NOTES, withBuildArtifact, fetchAll, listCollection, parse, runBusinessCli, usage, waitForDeployment, waitForJob };
1141
1225
  if (require.main === module) {
1142
1226
  runBusinessCli(process.argv.slice(2)).then((code) => { process.exitCode = code; }).catch((error) => {
1143
1227
  process.stderr.write(`error: ${error.message}\n`);