@zhuoyuezs/ml-platform 0.1.3 → 0.1.4

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/checksums.json CHANGED
@@ -17,28 +17,28 @@
17
17
  },
18
18
  {
19
19
  "path": "runtime/business-client/src/catalog.js",
20
- "sha256": "sha256:d37f8069027350a3b698af2e9121067e77feb995980bbb226e45e7cdf9130db1",
21
- "size_bytes": 53751
20
+ "sha256": "sha256:afc2ba28eee2d1dcd833a2e5c4ac923421dc60302b31bf985aa638849bbf3d36",
21
+ "size_bytes": 54246
22
22
  },
23
23
  {
24
24
  "path": "runtime/business-client/src/cli.js",
25
- "sha256": "sha256:51d2056d8f1c12c3e669b597415bd2c2b911c9a8d44ce4721ee9ff63c9922679",
26
- "size_bytes": 19305
25
+ "sha256": "sha256:db25cecb760a2b94ac1433c1d04ab70b429825e78dd1168e72688d8b9a8556be",
26
+ "size_bytes": 25911
27
27
  },
28
28
  {
29
29
  "path": "runtime/business-client/src/config.js",
30
- "sha256": "sha256:81022c2fc2a21cac8a12fd2c93b5defa18665a4d4a583a86e9eb54587650f47b",
31
- "size_bytes": 1960
30
+ "sha256": "sha256:4190e09c339a3f0b2861ca7dbc96fe2aa3bdd7927a5e1c4a4c00103a53236bd8",
31
+ "size_bytes": 2186
32
32
  },
33
33
  {
34
34
  "path": "runtime/business-client/src/http.js",
35
- "sha256": "sha256:0744596f8ae0beb60f317c06eccd87f3d5d0dc0ccba15668432a7a47c692aec9",
36
- "size_bytes": 9450
35
+ "sha256": "sha256:95ac9342c09605966f055dde2ac5cd67fdf28e8bc877e2233e3c48077ffdd729",
36
+ "size_bytes": 12429
37
37
  },
38
38
  {
39
39
  "path": "skills/feature-management/SKILL.md",
40
- "sha256": "sha256:ffd68632b98f51b15e18f225ee83452d6548969e799af727ea9f9efae7300cfb",
41
- "size_bytes": 19156
40
+ "sha256": "sha256:69842537b8df63e58630ca0581575f29f6ec9e97d22c9c2c30b1575270161f6e",
41
+ "size_bytes": 28100
42
42
  },
43
43
  {
44
44
  "path": "skills/feature-management/agents/openai.yaml",
@@ -52,18 +52,18 @@
52
52
  },
53
53
  {
54
54
  "path": "skills/feature-management/assets/catalog-template/datasets/example_temperature_training.v1.json",
55
- "sha256": "sha256:c4b4c811cd7dc90ba014e3be0b38fdc357bc83c224e5df614ab9168ce7fbcc0c",
56
- "size_bytes": 562
55
+ "sha256": "sha256:4dbe304631d6ecbf60b19d7c5a377178e0593525362f5fe509c60622b99e8a46",
56
+ "size_bytes": 900
57
57
  },
58
58
  {
59
59
  "path": "skills/feature-management/assets/catalog-template/feature_sets/example_temperature_core.v1.json",
60
- "sha256": "sha256:bca7e24927d8aef2fc7f48df327bf56a25c77591332b97b0d2a548708126ec88",
61
- "size_bytes": 315
60
+ "sha256": "sha256:22f37b954200864c19ed5593ab8455f562cc3dee7e05a0b01d5856a163dc0660",
61
+ "size_bytes": 352
62
62
  },
63
63
  {
64
64
  "path": "skills/feature-management/assets/catalog-template/features/example_temperature_mean_5m.v1.json",
65
- "sha256": "sha256:36eb3878125e337bf2afced7b363c56833266a529ce311fffd8f8b146f361739",
66
- "size_bytes": 572
65
+ "sha256": "sha256:7e58fa9c6a5ef581866d5cae598682de73e5ef3ce49fdd630432bada3a8a41a1",
66
+ "size_bytes": 807
67
67
  },
68
68
  {
69
69
  "path": "skills/feature-management/assets/catalog-template/operator_package/pyproject.toml",
@@ -77,33 +77,38 @@
77
77
  },
78
78
  {
79
79
  "path": "skills/feature-management/assets/catalog-template/operator_package/tests/test_operator.py",
80
- "sha256": "sha256:b758f343225365cf8361ae6c7a7e130502d339cc4ffe2be13d75c728ce9ec71a",
81
- "size_bytes": 1842
80
+ "sha256": "sha256:6fe073e07ed57d0fb4e782150153556b97c5b873b9eb2dad45df0d7f522d8650",
81
+ "size_bytes": 3678
82
82
  },
83
83
  {
84
84
  "path": "skills/feature-management/assets/catalog-template/operators/example_temperature_features.v1.json",
85
- "sha256": "sha256:5d188372b2c7721c95bd91bdaa871368fca1ce5d43965db22494ee8c895fa516",
86
- "size_bytes": 916
85
+ "sha256": "sha256:646965a05b982ae291960b23f359403c69bf8a173d6cc65d706fb2dc53ab030b",
86
+ "size_bytes": 1277
87
87
  },
88
88
  {
89
89
  "path": "skills/feature-management/assets/catalog-template/parameters/example_temperature.v1.json",
90
- "sha256": "sha256:df65f46f67b320d7edf940c8f11d153f0869ecfc52fc4336f31358f6ecc7783d",
91
- "size_bytes": 1451
90
+ "sha256": "sha256:28c7d3576c4b7e94c1e9db7f95f26d83ee2e10dc9f81ec6223f4fbd9b2f5eeda",
91
+ "size_bytes": 1488
92
92
  },
93
93
  {
94
94
  "path": "skills/feature-management/references/commands.md",
95
- "sha256": "sha256:1c1de0e9c5d40ab57374f59b32a04b92648d1e6abecd4962965ddd887a4e0242",
96
- "size_bytes": 9999
95
+ "sha256": "sha256:c20ccf9ad8d505a1880a7e35773edb5040bbbcfeba3a64316e3148a6e337bdc2",
96
+ "size_bytes": 11546
97
97
  },
98
98
  {
99
99
  "path": "skills/feature-management/references/contracts.md",
100
- "sha256": "sha256:b15cf8e0379c9ac2d0ecbc70a63635472da91956c3917318593cd5dc1f93695a",
101
- "size_bytes": 26166
100
+ "sha256": "sha256:913d5f0ad52b5183aa3a4bdeef2b6974f7757842300f547679fee38552792892",
101
+ "size_bytes": 27738
102
102
  },
103
103
  {
104
104
  "path": "skills/feature-management/references/operator-authoring.md",
105
- "sha256": "sha256:c2afb155a1c3379935f70fe0b68c1918a58a790ee470bea2b7516f54502c6ca0",
106
- "size_bytes": 5510
105
+ "sha256": "sha256:2ecfccbc9fe771bc1103db679d67af78847307eeb440f680f4055c35405ac923",
106
+ "size_bytes": 6089
107
+ },
108
+ {
109
+ "path": "skills/feature-management/references/platform-capability-guide.md",
110
+ "sha256": "sha256:fb606c3637891dab462d5b94004b60a3b8f02eb178aef589716f968c3b87cd5e",
111
+ "size_bytes": 2627
107
112
  }
108
113
  ],
109
114
  "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.3",
3
+ "version": "0.1.4",
4
4
  "description": "ML 数据平台 business CLI 与 Agent Skills 管理工具",
5
5
  "bin": {
6
6
  "ml-platform": "scripts/main.js"
package/release.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "entrypoint": "src/cli.js",
14
14
  "name": "ml-platform",
15
15
  "path": "runtime/business-client",
16
- "sha256": "sha256:f782ec0cda02a69cf3fcc9dfccd7f29793bdbd91a8092d56e4073f09547fbf78",
16
+ "sha256": "sha256:bf935c8c82aa508c26afa4f76dc94dd3d9e23c6e2bb33d797190d548a0c98249",
17
17
  "version": "0.6.0"
18
18
  },
19
19
  "policy_sha256": "sha256:02fbde0134696b78c0c365d6e0b76c9814227208da5b2b3d4e5dbd82dc4a3606",
@@ -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.3",
28
+ "release_version": "0.1.4",
29
29
  "runtime_requirements": {
30
30
  "node": ">=18",
31
31
  "os": [
@@ -38,9 +38,9 @@
38
38
  "feature-management": {
39
39
  "path": "skills/feature-management",
40
40
  "requires_cli": ">=0.6.0 <0.7.0",
41
- "revision": "0.1.3",
42
- "sha256": "sha256:e0ec0b304bb9f511eb5a5c53a32b76dcfede8bcdce70e3ff516eb4143a6c04f2"
41
+ "revision": "0.1.4",
42
+ "sha256": "sha256:0ae088bcfbb4b9a0fcfa22752bd978b2a34ab033d63af4ebb4f641d3c2ac65fd"
43
43
  }
44
44
  },
45
- "source_commit": "f63960b92fce9eb9360dd6ad64d10f26f5dd72db"
45
+ "source_commit": "2f92b97d658f16ebc6f2081b07a56365aeced649"
46
46
  }
@@ -60,6 +60,8 @@ function normalizeCodeArtifact(value) { object(value, "operator code_artifact");
60
60
  function normalizeParameter(payload) {
61
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
62
  const source = object(payload.source, "parameter source");
63
+ if (Object.prototype.hasOwnProperty.call(source, "parameters")) throw new Error("parameter source uses unsupported field 'parameters'; use 'params'");
64
+ 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");
63
65
  const time = object(defaultIfAbsent(payload, "time_semantics", {}), "parameter time_semantics");
64
66
  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")) };
65
67
  }
@@ -165,7 +167,7 @@ function requiresPackage(spec) { return ["custom_python", "feature", "spark_udf"
165
167
  function validateCatalog(catalog, current) {
166
168
  const parameters = new Set([...current.parameters.keys(), ...catalog.parameters.map(key)]); const operators = new Map([...current.operators, ...catalog.operators.map((x) => [key(x.spec), x.spec])]); const features = new Map([...current.features, ...catalog.features.map((x) => [key(x), x])]); const featureSets = new Map([...current.feature_sets, ...catalog.feature_sets.map((x) => [key(x), x])]);
167
169
  for (const item of catalog.operators) if (!item.package_path && requiresPackage(item.spec)) throw new Error(`catalog operator ${key(item.spec)} requires a wheel package path`);
168
- for (const feature of catalog.features) { for (const input of feature.inputs) if (!parameters.has(parameterReferenceKey(input))) throw new Error(`catalog feature ${key(feature)} references missing parameter ${parameterReferenceKey(input)}`); const operatorKey = `${feature.operator}:${feature.operator_version}`; const operator = operators.get(operatorKey); if (!operator) throw new Error(`catalog feature ${key(feature)} references missing operator ${operatorKey}`); if (operator.type !== "feature") throw new Error(`catalog feature ${key(feature)} requires OperatorSpec.type='feature'`); const declaredInputs = new Set(operator.input_schema.parameters ?? []); if (declaredInputs.size) for (const input of feature.inputs) if (!declaredInputs.has(parameterReferenceKey(input))) throw new Error(`catalog feature ${key(feature)} input is not declared by ${operatorKey}`); const columns = operator.output_schema.columns ?? []; if (columns.length && !columns.includes(feature.output_column)) throw new Error(`catalog feature ${key(feature)} output_column is not declared by ${operatorKey}`); }
170
+ for (const feature of catalog.features) { for (const input of feature.inputs) if (!parameters.has(parameterReferenceKey(input))) throw new Error(`catalog feature ${key(feature)} references missing parameter ${parameterReferenceKey(input)}`); const operatorKey = `${feature.operator}:${feature.operator_version}`; const operator = operators.get(operatorKey); if (!operator) throw new Error(`catalog feature ${key(feature)} references missing operator ${operatorKey}`); if (operator.type !== "feature") throw new Error(`catalog feature ${key(feature)} requires OperatorSpec.type='feature'`); const declaredInputs = new Set(operator.input_schema.parameters ?? []); if (declaredInputs.size) for (const input of feature.inputs) { const inputKey = parameterReferenceKey(input); const qualifiedInputKey = `${operator.project ?? feature.project ?? "default"}/${inputKey}`; if (!declaredInputs.has(inputKey) && !declaredInputs.has(qualifiedInputKey)) throw new Error(`catalog feature ${key(feature)} input is not declared by ${operatorKey}`); } const columns = operator.output_schema.columns ?? []; if (columns.length && !columns.includes(feature.output_column)) throw new Error(`catalog feature ${key(feature)} output_column is not declared by ${operatorKey}`); }
169
171
  for (const set of catalog.feature_sets) { const names = []; for (const item of set.features) { const feature = features.get(referenceKey(item)); if (!feature) throw new Error(`catalog feature set ${key(set)} references missing feature ${referenceKey(item)}`); names.push(feature.name); } unique(names, `catalog feature set ${key(set)} final output names`); }
170
172
  for (const dataset of catalog.datasets) {
171
173
  const datasetId = datasetKey(dataset); const declared = new Set();
@@ -14,7 +14,10 @@ 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-job", "wait-job", "get-dataset-artifact", "download-dataset-artifact", "fetch-inference-data", "apply",
17
+ "add-parameter", "add-feature", "add-feature-set", "add-operator", "publish-operator",
18
+ "add-dataset", "update-dataset", "delete-dataset", "list-dataset-artifacts",
19
+ "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",
18
21
  ]);
19
22
 
20
23
  const COMMAND_USAGE = {
@@ -30,15 +33,30 @@ const COMMAND_USAGE = {
30
33
  "list-operators": "list-operators [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
31
34
  "list-features": "list-features [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
32
35
  "list-feature-sets": "list-feature-sets [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
33
- "list-datasets": "list-datasets [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
36
+ "list-datasets": "list-datasets [--project PROJECT] [-q QUERY] [--limit N] [--offset N] [--include-deleted]",
37
+ "add-parameter": "add-parameter SPEC_JSON",
38
+ "add-feature": "add-feature SPEC_JSON",
39
+ "add-feature-set": "add-feature-set SPEC_JSON",
40
+ "add-operator": "add-operator SPEC_JSON",
41
+ "publish-operator": "publish-operator SPEC_JSON --package PATH",
42
+ "add-dataset": "add-dataset MANIFEST_JSON [--force]",
43
+ "update-dataset": "update-dataset MANIFEST_JSON [--force]",
44
+ "delete-dataset": "delete-dataset DATASET_ID DATASET_VERSION [--project PROJECT]",
45
+ "list-dataset-artifacts": "list-dataset-artifacts [--project PROJECT] [--dataset-id ID] [--dataset-version VERSION] [--include-deleted] [--limit N] [--offset N]",
34
46
  "get-dataset": "get-dataset DATASET_ID DATASET_VERSION [--project PROJECT] [--include-deleted]",
35
47
  "list-jobs": "list-jobs [-q QUERY] [--limit N] [--offset N]",
36
48
  "resolve-manifest": "resolve-manifest MANIFEST [--out PATH]",
37
49
  "resolve-dataset": "resolve-dataset DATASET_ID DATASET_VERSION [--project PROJECT] [--out PATH]",
38
- "build-dataset": "build-dataset MANIFEST [--source-mode direct] [--upload-chronon-metadata] [--partition-duration DURATION] [--max-parallelism N] [--no-resume] [--wait] [--poll-interval SECONDS] [--wait-timeout-seconds SECONDS]",
39
- "build-registered-dataset": "build-registered-dataset DATASET_ID DATASET_VERSION [--project PROJECT] [--source-mode direct] [--upload-chronon-metadata] [--partition-duration DURATION] [--max-parallelism N] [--no-resume] [--wait] [--poll-interval SECONDS] [--wait-timeout-seconds SECONDS]",
50
+ "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]",
51
+ "build-registered-dataset": "build-registered-dataset DATASET_ID DATASET_VERSION [--project PROJECT] [--engine chronon] [--source-mode direct] [--upload-chronon-metadata] [--partition-duration DURATION] [--max-parallelism N] [--no-resume] [--wait] [--poll-interval SECONDS] [--wait-timeout-seconds SECONDS]",
40
52
  "get-job": "get-job JOB_ID",
41
53
  "wait-job": "wait-job JOB_ID [--poll-interval SECONDS] [--wait-timeout-seconds SECONDS]",
54
+ "retry-job": "retry-job JOB_ID",
55
+ "cancel-job": "cancel-job JOB_ID",
56
+ "register-operator": "register-operator SPEC_JSON",
57
+ "show-operator-specs": "show-operator-specs [--project PROJECT] [-q QUERY] [--limit N] [--offset N]",
58
+ "delete-operator": "delete-operator NAME VERSION [--project PROJECT]",
59
+ "run-operator": "run-operator NAME VERSION [--project PROJECT] [--config JSON]",
42
60
  "get-dataset-artifact": "get-dataset-artifact DATASET_ID MANIFEST_HASH [--project PROJECT]",
43
61
  "download-dataset-artifact": "download-dataset-artifact DATASET_ID MANIFEST_HASH [--project PROJECT] --out-dir PATH [--force]",
44
62
  "fetch-inference-data": "fetch-inference-data MANIFEST --cutoff-time TIMESTAMP [--max-workers N] [--max-source-lag-hours HOURS] [--no-validate-freshness] [--allow-missing]",
@@ -52,7 +70,10 @@ function usage(command) {
52
70
  }
53
71
 
54
72
  function parse(args) {
55
- const options = { profile: "server", timeout: finiteNumber(process.env.ML_PLATFORM_API_TIMEOUT_SECONDS ?? 30, "--request-timeout-seconds") };
73
+ const timeoutValue = process.env.ML_PLATFORM_API_TIMEOUT_SECONDS
74
+ || process.env.DATA_PLATFORM_DEMO_API_TIMEOUT_SECONDS
75
+ || 30;
76
+ const options = { profile: "server", timeout: finiteNumber(timeoutValue, "--request-timeout-seconds") };
56
77
  let index = 0;
57
78
  while (index < args.length && args[index].startsWith("--")) {
58
79
  const flag = args[index++];
@@ -66,7 +87,7 @@ function parse(args) {
66
87
  if (!(options.timeout > 0)) throw new Error("--request-timeout-seconds must be positive");
67
88
  options.command = args[index++];
68
89
  options.rest = args.slice(index);
69
- options.apiUrl ??= process.env.ML_PLATFORM_API_URL || loadApiUrl();
90
+ options.apiUrl ??= process.env.ML_PLATFORM_API_URL || process.env.DATA_PLATFORM_DEMO_API_URL || loadApiUrl();
70
91
  return options;
71
92
  }
72
93
 
@@ -105,7 +126,7 @@ function positional(rest, label) { const value = rest.shift(); if (!value || val
105
126
 
106
127
  function client(options) {
107
128
  if (options.profile !== "server") throw new Error("the business client supports only --profile server");
108
- if (!options.apiUrl) throw new Error("server profile requires --api-url, ML_PLATFORM_API_URL, or a saved configure target");
129
+ if (!options.apiUrl) throw new Error("server profile requires --api-url, ML_PLATFORM_API_URL, DATA_PLATFORM_DEMO_API_URL, or a saved configure target");
109
130
  return new PlatformApiClient(options.apiUrl, options.timeout);
110
131
  }
111
132
 
@@ -116,6 +137,7 @@ function readObject(file) {
116
137
  }
117
138
 
118
139
  function readDataset(file) { return normalizeDataset(readObject(file)); }
140
+ function readSpec(file) { return readObject(file); }
119
141
 
120
142
  function datasetEndpoint(project, datasetId, datasetVersion) {
121
143
  return `/datasets/${encodeURIComponent(project)}/${encodeURIComponent(datasetId)}/${encodeURIComponent(datasetVersion)}`;
@@ -128,6 +150,8 @@ async function registeredDataset(api, project, datasetId, datasetVersion, includ
128
150
  }
129
151
 
130
152
  function buildOptions(rest) {
153
+ const engine = take(rest, "--engine", "chronon");
154
+ if (engine !== "chronon") throw new Error("--engine must be chronon");
131
155
  const sourceMode = take(rest, "--source-mode");
132
156
  if (sourceMode !== undefined && sourceMode !== "direct") throw new Error("--source-mode must be direct");
133
157
  const maxParallelism = integer(take(rest, "--max-parallelism", 1), "--max-parallelism");
@@ -139,6 +163,7 @@ function buildOptions(rest) {
139
163
  const waitTimeout = rawTimeout === undefined ? undefined : finiteNumber(rawTimeout, "--wait-timeout-seconds");
140
164
  if (waitTimeout !== undefined && !(waitTimeout > 0)) throw new Error("--wait-timeout-seconds must be positive");
141
165
  return {
166
+ engine,
142
167
  sourceMode,
143
168
  upload: boolean(rest, "--upload-chronon-metadata"),
144
169
  partitionDuration: take(rest, "--partition-duration"),
@@ -151,7 +176,7 @@ function buildOptions(rest) {
151
176
  }
152
177
 
153
178
  async function submitBuild(api, manifest, build) {
154
- let result = await api.post("/datasets/build", manifest, { engine: "chronon", source_mode: build.sourceMode, upload_chronon_metadata: build.upload, partition_duration: build.partitionDuration, max_parallelism: build.maxParallelism, resume: build.resume });
179
+ let result = await api.post("/datasets/build", manifest, { engine: build.engine, source_mode: build.sourceMode, upload_chronon_metadata: build.upload, partition_duration: build.partitionDuration, max_parallelism: build.maxParallelism, resume: build.resume });
155
180
  let exitCode = 0;
156
181
  if (build.wait) {
157
182
  if (!result || typeof result !== "object" || !result.job_id) throw new Error("server build did not return a job_id to wait for");
@@ -205,16 +230,28 @@ async function runBusinessCli(argv) {
205
230
  } else if (options.command === "list-projects") result = await client(options).get("/projects");
206
231
  else if (options.command === "get-project") result = await client(options).get(`/projects/${encodeURIComponent(positional(rest, "project name"))}`);
207
232
  else if (options.command === "delete-project") result = await client(options).delete(`/projects/${encodeURIComponent(positional(rest, "project name"))}`);
208
- else if (options.command === "get-dataset") {
233
+ else if (["add-parameter", "add-feature", "add-feature-set", "add-operator", "register-operator"].includes(options.command)) {
234
+ const spec = readSpec(positional(rest, "spec JSON"));
235
+ const endpoint = { "add-parameter": "/parameters", "add-feature": "/features", "add-feature-set": "/feature-sets", "add-operator": "/operators", "register-operator": "/operators" }[options.command];
236
+ result = await client(options).post(endpoint, spec);
237
+ } else if (options.command === "publish-operator") {
238
+ const specPath = positional(rest, "OperatorSpec JSON"); const packagePath = take(rest, "--package");
239
+ if (!packagePath) throw new Error("--package is required");
240
+ const spec = readSpec(specPath); const api = client(options); const project = spec.project || "default";
241
+ const upload = await api.uploadOperatorPackage(project, spec.name, spec.version, packagePath);
242
+ if (!upload || typeof upload.code_artifact !== "object") throw new Error("operator package upload response has no code_artifact");
243
+ const published = { ...spec, code_hash: upload.code_artifact.sha256, package_uri: upload.code_artifact.uri, code_artifact: upload.code_artifact };
244
+ result = { status: "published", profile: "server", path: null, operator: published, registration: await api.post("/operators", published) };
245
+ } else if (options.command === "get-dataset") {
209
246
  const datasetId = positional(rest, "dataset id"); const datasetVersion = positional(rest, "dataset version"); const project = take(rest, "--project", "default");
210
247
  result = await registeredDataset(client(options), project, datasetId, datasetVersion, boolean(rest, "--include-deleted"));
211
248
  }
212
- else if (options.command.startsWith("list-") && options.command !== "list-jobs") {
249
+ else if (options.command.startsWith("list-") && !["list-jobs", "list-dataset-artifacts"].includes(options.command)) {
213
250
  const endpoints = { "list-parameters": "/parameters", "list-operators": "/operators", "list-features": "/features", "list-feature-sets": "/feature-sets", "list-datasets": "/datasets" };
214
251
  const project = take(rest, "--project");
215
- const query = take(rest, "--q", take(rest, "-q")); const rawLimit = take(rest, "--limit"); const rawOffset = take(rest, "--offset");
216
- 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 }); }
217
- else result = await fetchAll(client(options), endpoints[options.command], { q: query, project });
252
+ 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 });
218
255
  } else if (options.command === "list-jobs") {
219
256
  const query = take(rest, "--q", take(rest, "-q")); const rawLimit = take(rest, "--limit"); const rawOffset = take(rest, "--offset");
220
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 }); }
@@ -239,12 +276,38 @@ async function runBusinessCli(argv) {
239
276
  const datasetId = positional(rest, "dataset id"); const datasetVersion = positional(rest, "dataset version"); const project = take(rest, "--project", "default"); const build = buildOptions(rest); const api = client(options);
240
277
  const manifest = normalizeRegistryDataset(await registeredDataset(api, project, datasetId, datasetVersion));
241
278
  ({ result, exitCode } = await submitBuild(api, manifest, build));
279
+ } else if (["add-dataset", "update-dataset"].includes(options.command)) {
280
+ const manifest = readDataset(positional(rest, "manifest JSON")); const force = boolean(rest, "--force"); const project = manifest.project || "default";
281
+ if (options.command === "add-dataset") result = await client(options).post("/datasets", manifest, { force });
282
+ else result = await client(options).put(`/datasets/${encodeURIComponent(project)}/${encodeURIComponent(manifest.dataset_id)}/${encodeURIComponent(manifest.dataset_version)}`, manifest, { force });
283
+ } else if (options.command === "delete-dataset") {
284
+ const id = positional(rest, "dataset id"); const version = positional(rest, "dataset version"); const project = take(rest, "--project", "default");
285
+ result = await client(options).delete(`/datasets/${encodeURIComponent(project)}/${encodeURIComponent(id)}/${encodeURIComponent(version)}`);
286
+ } else if (options.command === "list-dataset-artifacts") {
287
+ const project = take(rest, "--project"); const datasetId = take(rest, "--dataset-id"); const datasetVersion = take(rest, "--dataset-version"); const includeDeleted = boolean(rest, "--include-deleted");
288
+ result = await listCollection(client(options), "/dataset-artifacts", { project, dataset_id: datasetId, dataset_version: datasetVersion, include_deleted: includeDeleted }, rest);
289
+ } else if (options.command === "show-operator-specs") {
290
+ const project = take(rest, "--project"); const query = take(rest, "--q", take(rest, "-q"));
291
+ result = await listCollection(client(options), "/operators", { project, q: query }, rest);
292
+ } else if (options.command === "delete-operator") {
293
+ const name = positional(rest, "operator name"); const version = positional(rest, "operator version"); const project = take(rest, "--project", "default");
294
+ result = await client(options).delete(`/operators/${encodeURIComponent(project)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}`);
295
+ } else if (options.command === "run-operator") {
296
+ const name = positional(rest, "operator name"); const version = positional(rest, "operator version"); const project = take(rest, "--project", "default"); const rawConfig = take(rest, "--config", "{}");
297
+ let config; try { config = JSON.parse(rawConfig); } catch (_) { throw new Error("--config must be valid JSON"); }
298
+ result = await client(options).post(`/operators/${encodeURIComponent(project)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}/run`, config);
242
299
  } else if (options.command === "get-job") result = await client(options).get(`/jobs/${encodeURIComponent(positional(rest, "job id"))}`);
243
300
  else if (options.command === "wait-job") {
244
301
  const jobId = positional(rest, "job id"); const poll = finiteNumber(take(rest, "--poll-interval", 2), "--poll-interval"); const rawTimeout = take(rest, "--wait-timeout-seconds");
245
302
  result = await waitForJob(client(options), jobId, poll, rawTimeout === undefined ? undefined : finiteNumber(rawTimeout, "--wait-timeout-seconds"));
246
303
  if (["failed", "cancelled"].includes(result.status)) exitCode = 1;
247
304
  }
305
+ else if (options.command === "cancel-job") {
306
+ result = await client(options).post(`/jobs/${encodeURIComponent(positional(rest, "job id"))}/cancel`);
307
+ }
308
+ else if (options.command === "retry-job") {
309
+ result = await client(options).post(`/jobs/${encodeURIComponent(positional(rest, "job id"))}/retry`);
310
+ }
248
311
  else if (options.command === "get-dataset-artifact") {
249
312
  const dataset = positional(rest, "dataset id"); const hash = positional(rest, "manifest hash"); const project = take(rest, "--project", "default");
250
313
  result = await client(options).get(`/dataset-artifacts/${encodeURIComponent(project)}/${encodeURIComponent(dataset)}/${encodeURIComponent(hash)}`);
@@ -267,7 +330,16 @@ async function runBusinessCli(argv) {
267
330
 
268
331
  async function fetchAll(api, endpoint, params = {}) { const items = []; for (let offset = 0; ; offset += 500) { const page = await api.get(endpoint, { ...params, limit: 500, offset }); if (Array.isArray(page)) return page; if (!page || !Array.isArray(page.items)) throw new Error(`${endpoint} returned an invalid paginated response`); items.push(...page.items); if (page.total == null || items.length >= page.total || page.items.length < 500) return items; } }
269
332
 
270
- module.exports = { BUSINESS_COMMANDS, fetchAll, parse, runBusinessCli, usage, waitForJob };
333
+ async function listCollection(api, endpoint, params, rest) {
334
+ const rawLimit = take(rest, "--limit"); const rawOffset = take(rest, "--offset");
335
+ if (rawLimit === undefined && rawOffset === undefined) return fetchAll(api, endpoint, params);
336
+ const limit = Number(rawLimit ?? 50); const offset = Number(rawOffset ?? 0);
337
+ if (!Number.isInteger(limit) || limit < 1 || limit > 500) throw new Error("limit must be between 1 and 500");
338
+ if (!Number.isInteger(offset) || offset < 0) throw new Error("offset must be >= 0");
339
+ return api.get(endpoint, { ...params, limit, offset });
340
+ }
341
+
342
+ module.exports = { BUSINESS_COMMANDS, fetchAll, listCollection, parse, runBusinessCli, usage, waitForJob };
271
343
 
272
344
  if (require.main === module) {
273
345
  runBusinessCli(process.argv.slice(2)).then((code) => { process.exitCode = code; }).catch((error) => {
@@ -11,11 +11,15 @@ function expandHome(value) {
11
11
  }
12
12
 
13
13
  function configPath() {
14
- if (process.env.ML_PLATFORM_CONFIG_PATH?.trim()) return path.resolve(expandHome(process.env.ML_PLATFORM_CONFIG_PATH));
14
+ for (const variable of ["ML_PLATFORM_CONFIG_PATH", "DATA_PLATFORM_DEMO_CONFIG_PATH"]) {
15
+ if (process.env[variable]?.trim()) return path.resolve(expandHome(process.env[variable]));
16
+ }
15
17
  const base = process.env.XDG_CONFIG_HOME
16
18
  ? path.resolve(process.env.XDG_CONFIG_HOME)
17
19
  : path.join(os.homedir(), ".config");
18
- return path.join(base, "ml-platform", "config.json");
20
+ const primary = path.join(base, "ml-platform", "config.json");
21
+ const legacy = path.join(base, "data-platform-demo", "config.json");
22
+ return !fs.existsSync(primary) && fs.existsSync(legacy) ? legacy : primary;
19
23
  }
20
24
 
21
25
  function normalizeApiUrl(value) {
@@ -45,6 +45,7 @@ class PlatformApiClient {
45
45
 
46
46
  get(endpoint, params) { return this.request("GET", endpoint, { params }); }
47
47
  post(endpoint, payload, params) { return this.request("POST", endpoint, { payload, params }); }
48
+ put(endpoint, payload, params) { return this.request("PUT", endpoint, { payload, params }); }
48
49
  delete(endpoint, params) { return this.request("DELETE", endpoint, { params }); }
49
50
  uploadOperatorPackage(project, name, version, packagePath) {
50
51
  const resolved = path.resolve(expandHome(packagePath));
@@ -63,33 +64,95 @@ class PlatformApiClient {
63
64
  fs.mkdirSync(target, { recursive: true });
64
65
  const controller = new AbortController();
65
66
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
67
+ const temporaryDir = fs.mkdtempSync(path.join(os.tmpdir(), "data-platform-artifact-download-"));
68
+ const temporary = path.join(temporaryDir, "artifact.zip");
66
69
  let response;
67
70
  try {
68
71
  response = await fetch(this.url(`/dataset-artifacts/${encodeURIComponent(project)}/${encodeURIComponent(datasetId)}/${encodeURIComponent(manifestHash)}/download`), { headers: { Accept: "application/zip" }, signal: controller.signal });
69
- } catch (error) {
70
- throw new Error(`artifact download failed: ${error.message}`);
71
- } finally {
72
- clearTimeout(timer);
73
- }
74
- if (!response.ok) {
75
- let detail = response.statusText;
76
- try { detail = JSON.parse(await response.text())?.detail ?? detail; } catch (_) { /* use status text */ }
77
- throw new Error(`artifact download failed: HTTP ${response.status}: ${detail}`);
78
- }
79
- const temporary = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "data-platform-artifact-download-")), "artifact.zip");
80
- try {
81
- fs.writeFileSync(temporary, Buffer.from(await response.arrayBuffer()));
72
+ if (!response.ok) {
73
+ let detail = response.statusText;
74
+ try { detail = JSON.parse(await abortable(response.text(), controller.signal))?.detail ?? detail; } catch (_) { /* use status text */ }
75
+ throw new Error(`artifact download failed: HTTP ${response.status}: ${detail}`);
76
+ }
77
+ const total = contentLength(response);
78
+ let lastReported = -1;
79
+ const report = (received, complete = false) => {
80
+ if ((complete && received !== lastReported) || received === 0 || received - lastReported >= 1024 * 1024) {
81
+ reportDownloadProgress(received, total);
82
+ lastReported = received;
83
+ }
84
+ };
85
+ report(0);
86
+ await writeResponseBody(response, temporary, controller.signal, report);
82
87
  const files = await extractArchive(temporary, target);
83
88
  return { dataset_id: datasetId, manifest_hash: manifestHash, output_dir: target, file_count: files.length, files };
84
89
  } catch (error) {
85
90
  fs.rmSync(target, { recursive: true, force: true });
86
- throw error;
91
+ if (error instanceof Error && error.message.startsWith("artifact download failed:")) throw error;
92
+ throw new Error(`artifact download failed: ${error.message}`);
87
93
  } finally {
88
- fs.rmSync(path.dirname(temporary), { recursive: true, force: true });
94
+ clearTimeout(timer);
95
+ fs.rmSync(temporaryDir, { recursive: true, force: true });
89
96
  }
90
97
  }
91
98
  }
92
99
 
100
+ function contentLength(response) {
101
+ const raw = response.headers?.get?.("content-length") ?? response.headers?.["content-length"];
102
+ const value = Number(raw);
103
+ return Number.isFinite(value) && value >= 0 ? value : null;
104
+ }
105
+
106
+ function reportDownloadProgress(received, total) {
107
+ const suffix = total == null ? "" : `/${total}`;
108
+ process.stderr.write(`artifact download: received ${received}${suffix} bytes\n`);
109
+ }
110
+
111
+ function abortable(value, signal) {
112
+ if (signal.aborted) return Promise.reject(new Error("This operation was aborted"));
113
+ return new Promise((resolve, reject) => {
114
+ const onAbort = () => { cleanup(); reject(new Error("This operation was aborted")); };
115
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
116
+ signal.addEventListener("abort", onAbort, { once: true });
117
+ Promise.resolve(value).then((result) => { cleanup(); resolve(result); }, (error) => { cleanup(); reject(error); });
118
+ });
119
+ }
120
+
121
+ async function writeResponseBody(response, outputPath, signal, onProgress) {
122
+ const handle = fs.openSync(outputPath, "w");
123
+ let received = 0;
124
+ try {
125
+ const write = (value) => {
126
+ const chunk = Buffer.from(value);
127
+ fs.writeSync(handle, chunk);
128
+ received += chunk.length;
129
+ onProgress(received);
130
+ };
131
+ if (response.body?.getReader) {
132
+ const reader = response.body.getReader();
133
+ while (true) {
134
+ const item = await abortable(reader.read(), signal);
135
+ if (item.done) break;
136
+ write(item.value);
137
+ }
138
+ } else if (response.body?.[Symbol.asyncIterator]) {
139
+ const iterator = response.body[Symbol.asyncIterator]();
140
+ while (true) {
141
+ const item = await abortable(iterator.next(), signal);
142
+ if (item.done) break;
143
+ write(item.value);
144
+ }
145
+ } else if (typeof response.arrayBuffer === "function") {
146
+ write(await abortable(response.arrayBuffer(), signal));
147
+ } else {
148
+ throw new Error("artifact download response has no readable body");
149
+ }
150
+ onProgress(received, true);
151
+ } finally {
152
+ fs.closeSync(handle);
153
+ }
154
+ }
155
+
93
156
  async function extractArchive(archivePath, outputDir) {
94
157
  const archive = fs.readFileSync(archivePath);
95
158
  const end = findEndOfCentralDirectory(archive);
@@ -111,8 +174,8 @@ async function extractArchive(archivePath, outputDir) {
111
174
  if (next > archive.length || compressedSize === 0xffffffff || size === 0xffffffff || localOffset === 0xffffffff) throw new Error("artifact archive entry metadata is invalid");
112
175
  if (flags & 1) throw new Error("artifact archive contains an encrypted entry");
113
176
  if (![0, 8].includes(method)) throw new Error(`artifact archive compression method is unsupported: ${method}`);
114
- if (!(flags & 0x800)) throw new Error("artifact archive contains a non-UTF-8 filename");
115
- const rawName = archive.subarray(cursor + 46, cursor + 46 + nameLength).toString("utf8");
177
+ const rawNameBytes = archive.subarray(cursor + 46, cursor + 46 + nameLength);
178
+ const rawName = decodeZipFilename(rawNameBytes, flags);
116
179
  const name = rawName.replace(/\\/g, "/"); const parts = name.split("/"); const mode = (attributes >>> 16) & 0xffff;
117
180
  if (!name || name.startsWith("/") || /^[A-Za-z]:/.test(name) || parts.includes("..")) throw new Error(`artifact archive contains unsafe path: ${JSON.stringify(rawName)}`);
118
181
  if ((mode & 0o170000) === 0o120000) throw new Error(`artifact archive contains a symbolic link: ${JSON.stringify(rawName)}`);
@@ -131,6 +194,14 @@ async function extractArchive(archivePath, outputDir) {
131
194
  return files.sort();
132
195
  }
133
196
 
197
+ function decodeZipFilename(bytes, flags) {
198
+ if (flags & 0x800) return bytes.toString("utf8");
199
+ // ZIP producers commonly omit the UTF-8 flag for ASCII entry names. Keep
200
+ // those names interoperable while refusing ambiguous non-ASCII encodings.
201
+ if (bytes.some((value) => value > 0x7f)) throw new Error("artifact archive contains an unsupported non-UTF-8 filename");
202
+ return bytes.toString("ascii");
203
+ }
204
+
134
205
  function findEndOfCentralDirectory(archive) { const minimum = Math.max(0, archive.length - 65557); for (let offset = archive.length - 22; offset >= minimum; offset -= 1) if (archive.readUInt32LE(offset) === 0x06054b50 && offset + 22 + archive.readUInt16LE(offset + 20) === archive.length) return offset; throw new Error("artifact download is not a valid zip archive"); }
135
206
  function crc32(buffer) { let crc = 0xffffffff; for (const byte of buffer) { crc ^= byte; for (let bit = 0; bit < 8; bit += 1) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1)); } return (crc ^ 0xffffffff) >>> 0; }
136
207
 
@@ -19,8 +19,9 @@ Treat `Recipe` and public `Feature.compute` as removed. Treat `computation_hash`
19
19
  1. Resolve the Skill root as the directory containing this `SKILL.md`.
20
20
  2. Read [references/contracts.md](references/contracts.md) before creating or changing JSON assets.
21
21
  3. Read [references/operator-authoring.md](references/operator-authoring.md) whenever creating or changing Operator code or a wheel.
22
- 4. Read [references/commands.md](references/commands.md) before discovering assets, validating, publishing, building, fetching realtime data, or downloading.
22
+ 4. Read [references/commands.md](references/commands.md) immediately before a platform CLI action: discovery, validation, publication, build, realtime fetch, or artifact download. A local-only draft that is forbidden to call the CLI does not need this reference.
23
23
  5. Use [assets/catalog-template](assets/catalog-template) as a copyable starting point for a new end-to-end catalog. Rename every `example_*` identifier and update every referenced path before validation.
24
+ 6. Read [references/platform-capability-guide.md](references/platform-capability-guide.md) only when selecting an asset type or explaining a platform capability. Do not load it merely to write a confirmed local draft; it is never evidence for unresolved business semantics.
24
25
 
25
26
  ## Initialize The Client
26
27
 
@@ -46,6 +47,19 @@ ml-platform show-config
46
47
  ml-platform --profile server health
47
48
  ```
48
49
 
50
+ Use the global `--profile server` form shown above, before the business
51
+ command. Do not append `--profile` after a business command. Before discovery,
52
+ check the installed release with `ml-platform --help` and the relevant
53
+ subcommand's `--help`; CLI releases may differ in supported flags. In the
54
+ deployed 0.6.x CLI, registry `list-*` commands emit JSON by default and do not
55
+ accept a `--json` flag.
56
+
57
+ If the installed release rejects the global `--profile server` form shown above,
58
+ record the CLI syntax error and inspect `show-config`/subcommand help before
59
+ retrying the same read-only command without that flag when supported. Do not
60
+ switch clients or classify a syntax error as an API outage; keep each command
61
+ separate so CLI compatibility remains distinguishable from network health.
62
+
49
63
  The effective API target is resolved in this order: explicit `--api-url`,
50
64
  `ML_PLATFORM_API_URL`, then the saved value written by `ml-platform configure`.
51
65
  If no target is configured, ask the user for an approved API URL before making
@@ -61,7 +75,7 @@ Collect only missing information. Do not invent a source table, source field, fo
61
75
 
62
76
  Confirm:
63
77
 
64
- - the target project (namespace) for the assets; use the built-in `default` only when the user has no dedicated project. On creation the project comes from the optional `project` field in each asset spec (defaults to `default`); reads and filters use the `--project` command option;
78
+ - the target project (namespace) for the assets; use the built-in `default` only when the user has no dedicated project. After the target is confirmed, write its explicit `project` field in every Parameter, Operator, Feature, FeatureSet and DatasetManifest draft; never rely on the schema default. Reads and filters use the `--project` command option;
65
79
  - business meaning, stable asset names, owner, and intended consumers;
66
80
  - Parameter source adapter and credential-free source mapping;
67
81
  - feature formula, exact Parameter dependencies, windows, inclusion rules, rounding, null behavior, and output dtype;
@@ -75,9 +89,64 @@ Confirm:
75
89
 
76
90
  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.
77
91
 
92
+ A label such as "10-minute mean" confirms neither its input Parameter nor a
93
+ complete formula. Before drafting an Operator or Feature, ask for the exact
94
+ versioned input Parameter(s), or for an approved source contract from which
95
+ they will be created. Do not infer an input from the Feature name or a similar
96
+ existing asset.
97
+
98
+ ### Semantic Asset Review Gate
99
+
100
+ Treat resource correctness as a separate gate from workflow correctness. Before
101
+ creating Catalog JSON, write a local semantic asset review with one row per
102
+ Parameter, Operator, Feature and Dataset field:
103
+
104
+ ```text
105
+ asset_key | business_meaning | source/evidence | confirmed_by | unresolved | proposed_value
106
+ ```
107
+
108
+ The review must distinguish three sources: confirmed business facts, facts
109
+ observed from the selected Project Registry/CLI, and agent proposals. A Skill
110
+ reference, schema default, similarly named old asset, or platform capability is
111
+ not business evidence. Do not convert a proposal into `proposed_value` without
112
+ explicit user confirmation or an exact match in the selected Project.
113
+
114
+ Stop with `need_business_confirmation` when any required source mapping, field,
115
+ unit, formula, time conversion, missing/quality policy, FeatureSet order,
116
+ dataset read policy, rowset/endpoint policy, owner, or approval boundary is
117
+ unresolved. Do not create placeholder Catalog files to make the workflow look
118
+ complete.
119
+
120
+ For a requested Feature, `exact Parameter dependencies` is an independent
121
+ required contract item. If the user names a transform or window but not its
122
+ input Parameter(s), include `input_parameter` in the unresolved questions and
123
+ stop. Do this before treating an Operator formula as complete: a time window,
124
+ cutoff rule, or Feature name never identifies the input.
125
+
126
+ After confirmation, show the proposed dependency closure and obtain approval of
127
+ the semantic asset plan before generating Catalog files. A successful local
128
+ schema check or remote dry-run is not semantic approval.
129
+
130
+ Before emitting or writing a draft, mechanically check that every asset carries
131
+ the confirmed `project` field, that every Feature input carries that same
132
+ `project`, and that every Operator `input_schema.parameters` reference is
133
+ qualified as `project/parameter:version`. The schema's `default` values are not
134
+ valid substitutes for an explicit non-default target. When a confirmed Feature
135
+ config defines window closure, cutoff inclusion, duplicate handling, or empty
136
+ window behavior, copy all of those fields into the Feature config rather than
137
+ reducing it to the window string.
138
+
78
139
  ## Discover Existing Assets
79
140
 
80
- Check the selected profile and list existing Parameter, Operator, Feature, FeatureSet, and Dataset versions before choosing names. Scope discovery to the target project with `--project`, because the true registry key is `project/name:version` and the same `name:version` may exist independently under another project. Registry list commands are paginated; search by stable identifier or follow every page until `offset + len(items) >= total`. Reuse an exact immutable version only when its full content matches. Never reference an asset in another project; cross-project references are rejected.
141
+ Check the selected profile and list existing Parameter, Operator, Feature, FeatureSet, and Dataset versions before choosing names. Scope discovery to the target project with `--project`, because the true registry key is `project/name:version` and the same `name:version` may exist independently under another project. Run each `list-*` command as a separate command so one failed check cannot hide the status of the others. Do not add `--json` unless the installed subcommand help explicitly advertises it; supported releases emit machine-readable JSON by default. Registry list commands are paginated; search by stable identifier or follow every page until `offset + len(items) >= total`. Reuse an exact immutable version only when its full content matches. Never reference an asset in another project; cross-project references are rejected.
142
+
143
+ CLI readiness is a hard gate for every Registry conclusion. If `command -v`,
144
+ `version`, `show-config`, or server `health` fails, returns a nonzero status, or
145
+ does not identify the intended target, stop with `platform_not_ready`. Do not
146
+ list assets, select reuse versus authoring, dry-run, publish, build, fetch, or
147
+ report a Registry fact from prompt text, a remembered response, or a similar
148
+ local asset. Report the failed command and its configured target; resume only
149
+ after the user/environment restores the same target.
81
150
 
82
151
  Create a new version when source semantics, formula code, config meaning, inputs, output dtype, time behavior, quality rules, or column order change. Never overwrite an immutable version or use suffixes such as `new`, `final`, or `test2`.
83
152
 
@@ -140,8 +209,29 @@ Keep platform demos and V95 built-ins unchanged. Do not put unrelated business a
140
209
 
141
210
  ### Define Parameters
142
211
 
212
+ Use the platform-capability guide's asset table before choosing the resource
213
+ type. For each Parameter record the business signal, exact source evidence,
214
+ unit, time/availability semantics and missing policy in the semantic review.
215
+ Never infer a source table, column, measurement, tag, unit or timezone from a
216
+ Parameter name.
217
+
143
218
  Create one stable, versioned source contract for each independently readable value. Keep credentials out of JSON. Use only supported direct adapters and explicit source identity. Put source data validity in `quality_rules`; put dataset-specific transformations in preprocess Operators.
144
219
 
220
+ When the approved source contract supplies SQL, copy its query text and named
221
+ parameters exactly into `source.sql`/`source.params`. Do not retype, beautify,
222
+ rename an identifier, change a join key, or "simplify" an approved query while
223
+ drafting. SQL whitespace may be formatted only when a reviewed formatter proves
224
+ the token stream is unchanged; every source identifier and `<= %(end)s` boundary
225
+ remains part of the Parameter contract.
226
+
227
+ For SQL Parameters, the runtime contract is a single statement: use the exact
228
+ `source.sql` field and a mapping under `source.params` for every named
229
+ placeholder other than the reserved `start` and `end` window parameters. Do not
230
+ use `source.parameters` (it is not a supported alias and may be silently
231
+ dropped by older CLI/server releases). Do not leave a trailing semicolon; the
232
+ deployed Worker rejects SQL containing multiple statements. Verify the resolved
233
+ Parameter still contains the named params before authorizing a build.
234
+
145
235
  Use Parameter `rounding` when every consumer must receive the same fixed-point value. The platform applies `mode` (`half_up` or `half_even`) and `decimals` after source normalization and before quality checks, source caching, replay, and Operator execution. Prefer an unrounded source expression, do not repeat the same rounding in an Operator, and publish a new Parameter plus its affected reverse-dependency closure when the rule changes.
146
236
 
147
237
  Do not represent a rolling mean, lag, ratio, trend, or model input as a Parameter. Those are Features.
@@ -166,7 +256,11 @@ Use a meaningful immutable `function_hash`. Change the Operator version whenever
166
256
 
167
257
  ### Define Features
168
258
 
169
- Create one JSON file per single output column. Bind exactly one Operator version and physical `output_column`. List only the Parameter versions actually required by that column's formula.
259
+ Create one JSON file per single output column. Set the confirmed `project` on the
260
+ Feature and on every nested Feature input; a FeatureInput without `project`
261
+ silently targets `default`. Bind exactly one Operator version and physical
262
+ `output_column`. List only the Parameter versions actually required by that
263
+ column's formula.
170
264
 
171
265
  Features may share the same Operator and config. The planner will merge their exact inputs and call the Operator once per computation group. Do not duplicate every Operator input into every Feature merely to make schemas look uniform.
172
266
 
@@ -176,8 +270,41 @@ Reference immutable Feature versions in the exact consumer column order. Keep th
176
270
 
177
271
  ### Define The DatasetManifest
178
272
 
273
+ Do not use DatasetManifest as a catch-all for unresolved semantics. Confirm the
274
+ dataset purpose and consumer, then separately confirm the target grid, horizon,
275
+ read policy, explicit Parameter outputs, rowset or rowset splits, abnormal windows and
276
+ endpoint eligibility. Keep the semantic asset review beside the draft Catalog
277
+ so a reviewer can compare business decisions with generated fields.
278
+
179
279
  Reference exactly one FeatureSet. Use `parameters` only for Parameter columns that must also appear explicitly in `parameter_dataset.parquet`; Feature dependencies are resolved automatically.
180
280
 
281
+ Map the current Manifest schema exactly rather than carrying descriptive fields
282
+ from Parameter, Feature, or FeatureSet into it. A target Parameter reference is
283
+ **nested**; never flatten its `version` or `project` onto `target`:
284
+
285
+ ```json
286
+ "target": {
287
+ "parameter": {"parameter": "hot_metal_si", "version": "v1", "project": "<project>"},
288
+ "si_time_source": "dispatch_time",
289
+ "offset_minutes": 20,
290
+ "interpolation": {"method": "linear"}
291
+ }
292
+ ```
293
+
294
+ `target.interpolation` is `{"method": "linear"}`, `{"method": "none"}` or
295
+ `{"method": "forward_fill"}` -- neither it nor `target.parameter` accepts a
296
+ bare string. The current DatasetManifest has no top-level `owner`,
297
+ `description`, `si_null_policy`, or `preserve_all_candidate_rows`. Map a
298
+ business statement only to an existing schema field (for example `mark_only`,
299
+ `endpoint_policy`, a Feature config, or a Parameter missing policy); otherwise
300
+ keep it in the semantic review and ask for a platform/schema change. Validate
301
+ the generated object against the current schema before running `apply --dry-run`.
302
+
303
+ When a confirmed missing-data policy applies to a Feature-only Parameter, put it
304
+ in `parameter_missing_policies`, using `parameter`, `version`, `project`, and
305
+ `policy`. Do not copy the `ParameterRequest` names `name` and `missing_policy`
306
+ into that list. Omit the section when no such policy is confirmed.
307
+
181
308
  Declare one dataset-wide `prediction` contract when Features use a forecast cutoff. Operators consume `context.cutoff_times`; do not copy the same prediction horizon into every Feature config. Check the selected Operator's `input_schema.prediction` limits and satisfy an explicit-horizon requirement before publishing or building.
182
309
 
183
310
  Use `snapshot` for reproducible training data, `as_of` for historical visibility replay, and `latest` for current inference-style reads. Use a fresh `dataset_version` when validating a new release or intentionally requesting a new immutable dataset contract.
@@ -202,11 +329,16 @@ freshness, tail-edge, gap-fill, and rowset contracts.
202
329
 
203
330
  Run, in order:
204
331
 
205
- 1. Operator unit tests.
206
- 2. Wheel build and wheel filename verification.
207
- 3. Catalog path, schema, dependency, immutability, and package validation with `apply --dry-run` against the same target profile intended for publication.
208
- 4. Compare the catalog's exact dependency closure with the intended old-to-new version mapping. Reject any affected downstream reference that still points to an old version unless its retention is explicit and justified.
209
- 5. A human-readable summary of planned new, unchanged, retained, and conflicting assets.
332
+ 1. Semantic asset review: no required unresolved field and every proposed value has a source or user confirmation.
333
+ 2. Operator unit tests. Before building, turn every applicable item in
334
+ `operator-authoring.md` into a separately named test; do not let one broad
335
+ happy-path assertion substitute for cutoff-before/at/after, duplicate and
336
+ missing input, empty history, requested-output, event-time order, or
337
+ dtype/rounding coverage. The template is only a starting point.
338
+ 3. Wheel build and wheel filename verification.
339
+ 4. Catalog path, schema, dependency, immutability, and package validation with `apply --dry-run` against the same target profile intended for publication.
340
+ 5. Compare the catalog's exact dependency closure with the intended old-to-new version mapping. Reject any affected downstream reference that still points to an old version unless its retention is explicit and justified.
341
+ 6. A human-readable summary of planned new, unchanged, retained, and conflicting assets.
210
342
 
211
343
  Stop on any error. Do not weaken schema validation, fabricate a missing dependency, change an existing version in place, or switch profiles to make validation pass.
212
344
 
@@ -214,7 +346,7 @@ Stop on any error. Do not weaken schema validation, fabricate a missing dependen
214
346
 
215
347
  Publish only after the user explicitly approves Registry and wheel changes. Use catalog `apply` so publication follows Parameter -> Operator -> Feature -> FeatureSet -> Dataset order.
216
348
 
217
- After publication, resolve every new DatasetManifest and compare its exact Parameter, Operator, Feature, and FeatureSet versions with the pre-publication mapping. Stop if an affected old key or any unexpected version remains. Resolve the manifest before building. Submit a build only when requested. For server builds, wait for the terminal Job state and download the exact artifact by `dataset_id + manifest_hash`.
349
+ After publication, resolve every new DatasetManifest and compare its exact Parameter, Operator, Feature, and FeatureSet versions with the pre-publication mapping. Stop if an affected old key or any unexpected version remains. Resolve the manifest before building. Submit a build only when requested. For the current partitioned materialization runtime, use `--max-parallelism 1` unless a separately verified platform capability says otherwise; do not retry or resubmit a failed Job until its error and already-completed partitions are understood. For server builds, wait for the terminal Job state and download the exact artifact by `dataset_id + manifest_hash`.
218
350
 
219
351
  Do not delete versioned Registry assets, cancel Jobs, rebuild images, modify Kubernetes, or change service configuration as part of this workflow unless the user separately and explicitly requests that action. An empty Project may be soft-deleted only on an explicit request; rely on the server to reject deletion when resources still exist.
220
352
 
@@ -232,6 +364,14 @@ gap-filled inputs as degraded freshness, not as a normal read.
232
364
 
233
365
  ## Verify The Artifact
234
366
 
367
+ Treat Registry metadata inspection and downloaded-file inspection as separate
368
+ checkpoints. A successful `get-dataset-artifact` can establish the immutable
369
+ identity, object inventory, row count, validation summary, and lineage metadata;
370
+ it cannot establish Parquet schema, column order, or file-content hashes. If a
371
+ download times out, report whether no response/progress was observed or whether
372
+ some body bytes were received, preserve the metadata-only evidence, and leave
373
+ downloaded-file validation explicitly incomplete.
374
+
235
375
  Require all of the following before reporting success:
236
376
 
237
377
  - Job status is `succeeded`;
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "schema_version": "ml_data_platform.dataset_manifest/v1",
3
+ "project": "replace_with_project",
3
4
  "dataset_id": "example_temperature_training",
4
5
  "dataset_version": "v1",
5
6
  "mode": "training",
@@ -9,7 +10,22 @@
9
10
  "end": "2026-07-02T00:00:00+08:00",
10
11
  "grid": "10min"
11
12
  },
13
+ "prediction": {
14
+ "horizon": "0min"
15
+ },
16
+ "rowset": {
17
+ "strategy": "fixed_grid",
18
+ "grid": "10min"
19
+ },
12
20
  "parameters": [],
21
+ "parameter_missing_policies": [
22
+ {
23
+ "parameter": "example_temperature",
24
+ "version": "v1",
25
+ "project": "replace_with_project",
26
+ "policy": "report_only"
27
+ }
28
+ ],
13
29
  "feature_set": {
14
30
  "name": "example_temperature_core",
15
31
  "version": "v1"
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "schema_version": "ml_data_platform.feature_set/v1",
3
+ "project": "replace_with_project",
3
4
  "name": "example_temperature_core",
4
5
  "version": "v1",
5
6
  "features": [
@@ -1,17 +1,24 @@
1
1
  {
2
2
  "schema_version": "ml_data_platform.feature/v1",
3
+ "project": "replace_with_project",
3
4
  "name": "example_temperature_mean_5m",
4
5
  "version": "v1",
5
6
  "inputs": [
6
7
  {
7
8
  "parameter": "example_temperature",
8
- "version": "v1"
9
+ "version": "v1",
10
+ "project": "replace_with_project"
9
11
  }
10
12
  ],
11
13
  "operator": "example_temperature_features",
12
14
  "operator_version": "v1",
13
15
  "config": {
14
- "window": "5min"
16
+ "window": "5min",
17
+ "window_closed": "right",
18
+ "cutoff_included": true,
19
+ "post_cutoff_allowed": false,
20
+ "duplicate_event_time": "last",
21
+ "empty_window": "null"
15
22
  },
16
23
  "output_column": "example_temperature_mean_5m",
17
24
  "output_dtype": "float64",
@@ -9,20 +9,21 @@ from business_feature_operator_template import compute_features
9
9
 
10
10
 
11
11
  class ComputeFeaturesTest(unittest.TestCase):
12
- def context(self, requested: list[str] | None = None) -> SimpleNamespace:
13
- source = pd.DataFrame(
14
- {
15
- "timestamp": pd.DatetimeIndex(
16
- [
17
- "2026-07-01 09:54:00",
18
- "2026-07-01 09:56:00",
19
- "2026-07-01 10:00:00",
20
- "2026-07-01 10:01:00",
21
- ]
22
- ),
23
- "value": [10.0, 20.0, 30.0, 40.0],
24
- }
25
- )
12
+ def context(
13
+ self,
14
+ timestamps: list[str] | None = None,
15
+ values: list[float | None] | None = None,
16
+ targets: list[str] | None = None,
17
+ requested: list[str] | None = None,
18
+ ) -> SimpleNamespace:
19
+ timestamps = timestamps or [
20
+ "2026-07-01 09:54:00",
21
+ "2026-07-01 09:56:00",
22
+ "2026-07-01 10:00:00",
23
+ "2026-07-01 10:01:00",
24
+ ]
25
+ values = values or [10.0, 20.0, 30.0, 40.0]
26
+ source = pd.DataFrame({"timestamp": pd.DatetimeIndex(timestamps), "value": values})
26
27
  return SimpleNamespace(
27
28
  requested_output_columns=requested or ["example_temperature_mean_5m"],
28
29
  inputs=[
@@ -32,24 +33,50 @@ class ComputeFeaturesTest(unittest.TestCase):
32
33
  )
33
34
  ],
34
35
  metric_frames={"example_temperature:v1": source},
35
- target_times=pd.DatetimeIndex(
36
- ["2026-07-01 10:00:00", "2026-07-01 10:02:00"]
37
- ),
36
+ target_times=pd.DatetimeIndex(targets or ["2026-07-01 10:00:00", "2026-07-01 10:02:00"]),
38
37
  config={"window": "5min"},
39
38
  )
40
39
 
41
- def test_uses_open_left_and_closed_right_window(self) -> None:
40
+ def test_formula_uses_the_confirmed_mean(self) -> None:
42
41
  result = compute_features(self.context())
43
-
44
- self.assertEqual(
45
- list(result.columns),
46
- ["event_time", "example_temperature_mean_5m"],
47
- )
48
42
  self.assertEqual(result["example_temperature_mean_5m"].tolist(), [25.0, 35.0])
49
43
 
44
+ def test_excludes_event_at_open_left_window_boundary(self) -> None:
45
+ result = compute_features(self.context(["2026-07-01 09:55:00", "2026-07-01 09:56:00"], [10.0, 20.0]))
46
+ self.assertEqual(result["example_temperature_mean_5m"].iloc[0], 20.0)
47
+
48
+ def test_includes_event_exactly_at_cutoff(self) -> None:
49
+ result = compute_features(self.context(["2026-07-01 10:00:00"], [30.0]))
50
+ self.assertEqual(result["example_temperature_mean_5m"].iloc[0], 30.0)
51
+
52
+ def test_excludes_event_after_cutoff(self) -> None:
53
+ result = compute_features(self.context(["2026-07-01 10:00:01"], [40.0]))
54
+ self.assertTrue(pd.isna(result["example_temperature_mean_5m"].iloc[0]))
55
+
56
+ def test_uses_last_value_for_duplicate_event_time(self) -> None:
57
+ result = compute_features(self.context(["2026-07-01 10:00:00", "2026-07-01 10:00:00"], [10.0, 30.0]))
58
+ self.assertEqual(result["example_temperature_mean_5m"].iloc[0], 30.0)
59
+
60
+ def test_empty_history_returns_null(self) -> None:
61
+ result = compute_features(self.context(["2026-07-01 09:00:00"], [10.0]))
62
+ self.assertTrue(pd.isna(result["example_temperature_mean_5m"].iloc[0]))
63
+
64
+ def test_requested_output_is_exactly_the_supported_subset(self) -> None:
65
+ result = compute_features(self.context(requested=["example_temperature_mean_5m"]))
66
+ self.assertEqual(list(result.columns), ["event_time", "example_temperature_mean_5m"])
67
+
68
+ def test_preserves_requested_event_time_order(self) -> None:
69
+ targets = ["2026-07-01 10:02:00", "2026-07-01 10:00:00"]
70
+ result = compute_features(self.context(targets=targets))
71
+ self.assertEqual(list(result["event_time"]), list(pd.DatetimeIndex(targets)))
72
+
73
+ def test_output_dtype_is_float64(self) -> None:
74
+ result = compute_features(self.context())
75
+ self.assertEqual(str(result["example_temperature_mean_5m"].dtype), "float64")
76
+
50
77
  def test_rejects_unknown_requested_output(self) -> None:
51
78
  with self.assertRaisesRegex(ValueError, "unsupported output columns"):
52
- compute_features(self.context(["unknown_feature"]))
79
+ compute_features(self.context(requested=["unknown_feature"]))
53
80
 
54
81
 
55
82
  if __name__ == "__main__":
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "schema_version": "ml_data_platform.operator/v1",
3
+ "project": "replace_with_project",
3
4
  "name": "example_temperature_features",
4
5
  "version": "v1",
5
6
  "type": "feature",
@@ -10,7 +11,21 @@
10
11
  "code_artifact": null,
11
12
  "input_schema": {
12
13
  "parameters": [
13
- "example_temperature:v1"
14
+ "replace_with_project/example_temperature:v1"
15
+ ],
16
+ "prediction": {
17
+ "required": true,
18
+ "minimum_horizon": "0min",
19
+ "maximum_horizon": "0min"
20
+ },
21
+ "history_requirements": [
22
+ {
23
+ "anchor": "cutoff",
24
+ "lookback": "5min",
25
+ "output_columns": [
26
+ "example_temperature_mean_5m"
27
+ ]
28
+ }
14
29
  ]
15
30
  },
16
31
  "output_schema": {
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "schema_version": "ml_data_platform.parameter/v1",
3
+ "project": "replace_with_project",
3
4
  "name": "example_temperature",
4
5
  "display_name": "Example temperature",
5
6
  "version": "v1",
@@ -49,6 +49,15 @@ ml-platform --profile server list-feature-sets -q <stable-name>
49
49
  ml-platform --profile server list-datasets -q <stable-dataset-id>
50
50
  ```
51
51
 
52
+ Run each command separately. Check `ml-platform --help` and the specific
53
+ subcommand help before adding flags. In the deployed 0.6.x CLI, `list-*`
54
+ commands print JSON by default and do not accept a `--json` flag.
55
+ If a deployed release rejects the global `--profile server` prefix, treat that
56
+ as CLI compatibility drift: record the syntax error, inspect `show-config` and
57
+ the subcommand help, then retry the same read-only command without the rejected
58
+ flag only if the installed help permits it. Do not report this as API/network
59
+ unavailability or switch to a direct HTTP client.
60
+
52
61
  By default each `list-*` command auto-pages through the whole result set and
53
62
  prints a **bare JSON array** of every matching item, so it never silently stops
54
63
  at the server's default page size. Use `-q` for a case-insensitive identifier
@@ -217,6 +226,17 @@ ml-platform --profile server wait-job <job_id>
217
226
  ml-platform --profile server get-job <job_id>
218
227
  ```
219
228
 
229
+ With explicit user authorization, stop a running build through the server CLI:
230
+
231
+ ```bash
232
+ ml-platform --profile server cancel-job <job_id>
233
+ ```
234
+
235
+ Cancellation is terminal for that Job; it does not change the immutable
236
+ DatasetManifest and it does not automatically resubmit with different
237
+ partition or parallelism settings. Check `get-job` after the request and
238
+ record the returned terminal status.
239
+
220
240
  ## Fetch Realtime Inference Data
221
241
 
222
242
  Fetch one causal-cutoff row in memory:
@@ -252,6 +272,15 @@ ml-platform --profile server \
252
272
  `--project` defaults to `default`; pass the owning project when the dataset lives
253
273
  elsewhere, or the lookup returns `artifact_not_found`.
254
274
 
275
+ The download command writes progress to stderr after response headers arrive.
276
+ Interpret a timeout with no progress line as a request/header or first-byte
277
+ failure. Interpret a timeout after one or more `artifact download: received ...`
278
+ lines as an incomplete response body. In either case, report the timeout and
279
+ keep downloaded-file validation incomplete; do not replace the supported CLI
280
+ with direct HTTP or object-store access. A successful metadata query remains
281
+ valid metadata-only evidence, but it does not prove Parquet schema, column
282
+ order, or file-content hashes.
283
+
255
284
  Inspect at least:
256
285
 
257
286
  ```text
@@ -35,6 +35,9 @@ Use this reference for the implemented V1 schemas. Prefer the running CLI and `s
35
35
  `project` field in its own JSON spec (it defaults to `default` when omitted).
36
36
  So to place an asset in a non-default project, set `"project": "<name>"` in the
37
37
  spec before `apply`.
38
+ - For a confirmed non-default target, include that field in every draft
39
+ Parameter, Operator, Feature, Feature input, FeatureSet, and DatasetManifest;
40
+ do not rely on Pydantic's `default` fallback during authoring.
38
41
  - Reads and filters, by contrast, take the project out-of-band: `list-* --project`
39
42
  and the leading project argument of the artifact commands. `apply --project`
40
43
  scopes the comparison snapshot that publication is validated against.
@@ -91,6 +94,11 @@ Required shape:
91
94
 
92
95
  Supported source modes are `direct_column`, `sql`, and `derived`. Prefer `direct_column` for ordinary business parameters.
93
96
 
97
+ For `sql`, `source.sql` must contain one statement without a trailing semicolon,
98
+ and named placeholders must be supplied through `source.params`; `start` and
99
+ `end` are reserved runtime window parameters. `source.parameters` is not a
100
+ supported field.
101
+
94
102
  `availability.strategy` 支持 `source_field`、`fixed_delay` 和 `unsupported`。只有一个
95
103
  事件时间列时,可以使用经过数据 owner 确认的固定延迟上界,例如
96
104
  `{"strategy":"fixed_delay","delay":"PT10M","accuracy":"upper_bound"}`。
@@ -282,6 +290,14 @@ The list must be nonempty, unique, and ordered exactly as consumers expect.
282
290
  "horizon": "10min"
283
291
  },
284
292
  "parameters": [],
293
+ "parameter_missing_policies": [
294
+ {
295
+ "parameter": "hot_blast_pressure",
296
+ "version": "v1",
297
+ "project": "default",
298
+ "policy": "report_only"
299
+ }
300
+ ],
285
301
  "feature_set": {"name": "pressure_core", "version": "v1"},
286
302
  "preprocess": [],
287
303
  "output": {
@@ -321,9 +337,13 @@ declared `realtime_fetch` policy contains meaningful defaults and changes the
321
337
  manifest hash. Declaring `endpoint_policy` also changes the manifest hash.
322
338
 
323
339
  Each explicit `parameters` request may set `missing_policy`. A
324
- `parameter_missing_policies` entry uses the same Parameter `name` and `version`
325
- but is reserved for Parameters used only through the selected FeatureSet's
326
- input closure; it must not also appear in `parameters`. The policies are:
340
+ `parameter_missing_policies` entry is reserved for a Parameter used only through
341
+ the selected FeatureSet's input closure; it must not also appear in
342
+ `parameters`. Its exact V1 shape is `{"parameter": "<name>", "version":
343
+ "<version>", "project": "<project>", "policy": "<policy>"}`. Do not use
344
+ the `ParameterRequest` field names `name` or `missing_policy` in this list. For
345
+ a confirmed non-default target, set `project` explicitly instead of relying on
346
+ its `default` fallback. The policies are:
327
347
 
328
348
  - `report_only`: retain all target rows and report source gaps.
329
349
  - `fail_on_requested_range_gap`: fail when a detected gap affects the requested
@@ -424,13 +444,30 @@ near the end of the target range can have its lookback clipped by the range end,
424
444
  and a window close to the end can remove most remaining rows.
425
445
 
426
446
  `target` states which instant the label is aligned to, so reviewing a manifest
427
- shows the target definition rather than only a Parameter version string.
428
- `si_time_source` is `weight_time` or `dispatch_time`.
447
+ shows the target definition rather than only a Parameter version string. When
448
+ the target is a Parameter, keep its complete versioned reference **inside**
449
+ `target.parameter`; do not flatten `version` or `project` onto `target`.
450
+ `si_time_source` is `weight_time` or `dispatch_time`. `interpolation` is an
451
+ object with a `method`, never a bare string.
429
452
 
430
453
  ```json
431
- "target": {"si_time_source": "weight_time", "offset_minutes": 0}
454
+ "target": {
455
+ "parameter": {
456
+ "parameter": "hot_metal_si",
457
+ "version": "v1",
458
+ "project": "replace_with_project"
459
+ },
460
+ "si_time_source": "weight_time",
461
+ "offset_minutes": 0,
462
+ "interpolation": {"method": "linear"}
463
+ }
432
464
  ```
433
465
 
466
+ Allowed interpolation methods are `none`, `linear`, and `forward_fill`. A
467
+ business phrase such as "allow and report target nulls" belongs in an existing
468
+ Parameter missing policy, Feature configuration, or the semantic review; it is
469
+ not a free-form `target` or DatasetManifest field.
470
+
434
471
  The declaration is checked against the selected Si Parameter, but only for
435
472
  `direct_column` reads, which name their time column in the contract. A `sql` or
436
473
  `derived` Parameter can apply an offset inside its query text, so those are
@@ -67,12 +67,17 @@ Use `context.cutoff_times` for causal feature windows. It is derived once from
67
67
  `context.target_times - context.prediction_horizon` using the DatasetManifest
68
68
  prediction contract; do not parse a duplicated Feature-level horizon.
69
69
 
70
- If an Operator requires a forecast contract, declare `input_schema.prediction`
71
- with `required`, `minimum_horizon`, and `maximum_horizon`. Declare source history
72
- as `input_schema.history_requirements`, using `anchor: cutoff` for causal windows
73
- and `anchor: target_time` only for intentional target-aligned formulas. Scope a
74
- requirement with `output_columns` when only some outputs need it. Include alignment
75
- slack such as hourly floor boundaries in the declared lookback.
70
+ An Operator requires an explicit forecast contract whenever its formula or code
71
+ uses `context.cutoff_times`, or any `input_schema.history_requirements` entry uses
72
+ `anchor: cutoff`. In that case always declare `input_schema.prediction` with
73
+ `required`, `minimum_horizon`, and `maximum_horizon`. This includes a strict
74
+ nowcast: declare `required=true` and both bounds as `0min`; do not omit the
75
+ section and silently accept the runtime's `default_nowcast` fallback. Declare
76
+ source history as `input_schema.history_requirements`, using `anchor: cutoff`
77
+ for causal windows and `anchor: target_time` only for intentional target-aligned
78
+ formulas. Scope a requirement with `output_columns` when only some outputs need
79
+ it. Include alignment slack such as hourly floor boundaries in the declared
80
+ lookback.
76
81
 
77
82
  For realtime reads, `history_requirements` is also the per-Parameter fetch
78
83
  contract. Do not rely on a package-local lookback map or a single fixed window
@@ -110,7 +115,10 @@ Never infer a rule from a Feature name alone. Require a business decision when a
110
115
 
111
116
  ## Required Tests
112
117
 
113
- Use deterministic fixtures and cover:
118
+ Use deterministic fixtures and cover each applicable item with a separately
119
+ named test. A broad formula or happy-path test does not substitute for the
120
+ cutoff-before, cutoff-at, cutoff-after, duplicate/missing, empty-history,
121
+ requested-output, event-time-order, or dtype/rounding cases below:
114
122
 
115
123
  1. expected formula values;
116
124
  2. the event immediately before cutoff;
@@ -0,0 +1,44 @@
1
+ # Platform Capability Guide
2
+
3
+ Use this guide to explain what the platform can execute. It is not business
4
+ evidence. A platform default never answers an unresolved business question.
5
+
6
+ ## Public Assets
7
+
8
+ | Asset | Stores | Use when | Do not use for |
9
+ |---|---|---|---|
10
+ | Project | Namespace and isolation boundary | Every independent business release | A dataset version or a source table |
11
+ | Parameter | One readable source contract and its quality/time semantics | Raw or independently readable source values/events | Rolling means, ratios, trends, model columns |
12
+ | Operator | Versioned deterministic executable code | A formula needs computation over declared inputs | A business definition without confirmed formula |
13
+ | Feature | One immutable output-column contract | One model/input column produced by an Operator | A group of columns or an unnamed formula |
14
+ | FeatureSet | Ordered Feature references | Consumer column order is part of the contract | Copying formulas or creating multiple datasets |
15
+ | DatasetManifest | Dataset mode, time grid, rowsets, policy and FeatureSet reference | A reproducible dataset contract is ready | Filling unknown source or business semantics |
16
+ | DatasetArtifact | Output of an authorized build | Build was explicitly approved and completed | Proving a Catalog is semantically correct |
17
+
18
+ ## Decision Rules
19
+
20
+ - Parameter answers **what source value is exposed**; Operator/Feature answers
21
+ **how a confirmed business formula is computed**.
22
+ - FeatureSet order is consumer-facing and must be confirmed; it is not inferred
23
+ from filesystem order or JSON discovery order.
24
+ - Dataset `time_range`, `prediction`, `rowset_splits`, `abnormal_windows` and
25
+ `endpoint_policy` are separate contracts. A platform default does not choose
26
+ a business policy.
27
+ - `apply --dry-run` checks structure, references, package and immutability. It
28
+ does not prove source correctness, formula correctness or artifact parity.
29
+ - `publish` creates immutable Registry resources. `build` creates an execution
30
+ Job and must have a separate explicit approval.
31
+
32
+ ## Required Semantic Asset Review
33
+
34
+ Before writing Catalog JSON, produce a local review table with one row per
35
+ Parameter, Operator, Feature and Dataset field:
36
+
37
+ ```text
38
+ asset_key | business_meaning | source/evidence | confirmed_by | unresolved | proposed_value
39
+ ```
40
+
41
+ Stop before Catalog generation if any required `source/evidence`, formula,
42
+ time boundary, unit, null policy, output dtype, FeatureSet order, read policy,
43
+ or approval field is unresolved. The review table is a proposal for the user;
44
+ it is not a Registry asset and must not contain guessed values.