@zhuoyuezs/ml-platform 0.1.3 → 0.1.5
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 +21 -0
- package/DEVELOPMENT.md +7 -5
- package/checksums.json +38 -33
- package/package.json +6 -3
- package/release.json +7 -7
- package/runtime/business-client/README.md +17 -0
- package/runtime/business-client/package-lock.json +2 -2
- package/runtime/business-client/package.json +4 -2
- package/runtime/business-client/src/catalog.js +3 -1
- package/runtime/business-client/src/cli.js +132 -24
- package/runtime/business-client/src/config.js +6 -2
- package/runtime/business-client/src/http.js +130 -13
- package/skills/feature-management/SKILL.md +178 -10
- package/skills/feature-management/assets/catalog-template/datasets/example_temperature_training.v1.json +16 -0
- package/skills/feature-management/assets/catalog-template/feature_sets/example_temperature_core.v1.json +1 -0
- package/skills/feature-management/assets/catalog-template/features/example_temperature_mean_5m.v1.json +9 -2
- package/skills/feature-management/assets/catalog-template/operator_package/tests/test_operator.py +51 -24
- package/skills/feature-management/assets/catalog-template/operators/example_temperature_features.v1.json +16 -1
- package/skills/feature-management/assets/catalog-template/parameters/example_temperature.v1.json +1 -0
- package/skills/feature-management/references/commands.md +29 -0
- package/skills/feature-management/references/contracts.md +43 -6
- package/skills/feature-management/references/operator-authoring.md +15 -7
- package/skills/feature-management/references/platform-capability-guide.md +73 -0
|
@@ -11,11 +11,15 @@ function expandHome(value) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function configPath() {
|
|
14
|
-
|
|
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
|
-
|
|
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,30 +64,138 @@ 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 });
|
|
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);
|
|
87
|
+
const files = await extractArchive(temporary, target);
|
|
88
|
+
return { dataset_id: datasetId, manifest_hash: manifestHash, output_dir: target, file_count: files.length, files };
|
|
69
89
|
} catch (error) {
|
|
90
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
91
|
+
if (error instanceof Error && error.message.startsWith("artifact download failed:")) throw error;
|
|
70
92
|
throw new Error(`artifact download failed: ${error.message}`);
|
|
71
93
|
} finally {
|
|
72
94
|
clearTimeout(timer);
|
|
95
|
+
fs.rmSync(temporaryDir, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async downloadDatasetArtifactFiles(project, datasetId, manifestHash, relativePaths, outputDir, force = false) {
|
|
100
|
+
const files = [...new Set((relativePaths || []).map((value) => String(value)))];
|
|
101
|
+
if (!files.length) throw new Error("at least one artifact file is required");
|
|
102
|
+
for (const relative of files) {
|
|
103
|
+
const normalized = relative.replace(/\\/g, "/");
|
|
104
|
+
const parts = normalized.split("/");
|
|
105
|
+
if (!normalized || normalized.startsWith("/") || normalized.endsWith("/") || parts.includes("..")) {
|
|
106
|
+
throw new Error(`artifact path escapes output directory: ${JSON.stringify(relative)}`);
|
|
107
|
+
}
|
|
73
108
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
109
|
+
const targetRoot = path.resolve(expandHome(outputDir));
|
|
110
|
+
if (fs.existsSync(targetRoot) && fs.readdirSync(targetRoot).length) {
|
|
111
|
+
if (!force) throw new Error(`download output directory is not empty: ${targetRoot}`);
|
|
112
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
78
113
|
}
|
|
79
|
-
|
|
114
|
+
fs.mkdirSync(targetRoot, { recursive: true });
|
|
80
115
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
116
|
+
for (const relative of files) {
|
|
117
|
+
const normalized = relative.replace(/\\/g, "/");
|
|
118
|
+
const parts = normalized.split("/");
|
|
119
|
+
const destination = path.resolve(targetRoot, ...parts);
|
|
120
|
+
if (!normalized || normalized.startsWith("/") || normalized.endsWith("/")
|
|
121
|
+
|| parts.includes("..") || destination !== targetRoot && !destination.startsWith(`${targetRoot}${path.sep}`)) {
|
|
122
|
+
throw new Error(`artifact path escapes output directory: ${JSON.stringify(relative)}`);
|
|
123
|
+
}
|
|
124
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
125
|
+
const endpoint = `/dataset-artifacts/${encodeURIComponent(project)}/${encodeURIComponent(datasetId)}/${encodeURIComponent(manifestHash)}/files/${parts.map(encodeURIComponent).join("/")}`;
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
128
|
+
try {
|
|
129
|
+
const response = await fetch(this.url(endpoint), { headers: { Accept: "application/octet-stream" }, signal: controller.signal });
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
let detail = response.statusText;
|
|
132
|
+
try { detail = JSON.parse(await response.text())?.detail ?? detail; } catch (_) { /* status text */ }
|
|
133
|
+
throw new Error(`artifact file download failed: HTTP ${response.status}: ${detail}`);
|
|
134
|
+
}
|
|
135
|
+
fs.writeFileSync(destination, Buffer.from(await response.arrayBuffer()), { flag: "wx" });
|
|
136
|
+
} finally { clearTimeout(timer); }
|
|
137
|
+
}
|
|
84
138
|
} catch (error) {
|
|
85
|
-
fs.rmSync(
|
|
139
|
+
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
86
140
|
throw error;
|
|
87
|
-
} finally {
|
|
88
|
-
fs.rmSync(path.dirname(temporary), { recursive: true, force: true });
|
|
89
141
|
}
|
|
142
|
+
return { project, dataset_id: datasetId, manifest_hash: manifestHash, output_dir: targetRoot, file_count: files.length, files: files.sort() };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function contentLength(response) {
|
|
147
|
+
const raw = response.headers?.get?.("content-length") ?? response.headers?.["content-length"];
|
|
148
|
+
const value = Number(raw);
|
|
149
|
+
return Number.isFinite(value) && value >= 0 ? value : null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function reportDownloadProgress(received, total) {
|
|
153
|
+
const suffix = total == null ? "" : `/${total}`;
|
|
154
|
+
process.stderr.write(`artifact download: received ${received}${suffix} bytes\n`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function abortable(value, signal) {
|
|
158
|
+
if (signal.aborted) return Promise.reject(new Error("This operation was aborted"));
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const onAbort = () => { cleanup(); reject(new Error("This operation was aborted")); };
|
|
161
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
162
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
163
|
+
Promise.resolve(value).then((result) => { cleanup(); resolve(result); }, (error) => { cleanup(); reject(error); });
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function writeResponseBody(response, outputPath, signal, onProgress) {
|
|
168
|
+
const handle = fs.openSync(outputPath, "w");
|
|
169
|
+
let received = 0;
|
|
170
|
+
try {
|
|
171
|
+
const write = (value) => {
|
|
172
|
+
const chunk = Buffer.from(value);
|
|
173
|
+
fs.writeSync(handle, chunk);
|
|
174
|
+
received += chunk.length;
|
|
175
|
+
onProgress(received);
|
|
176
|
+
};
|
|
177
|
+
if (response.body?.getReader) {
|
|
178
|
+
const reader = response.body.getReader();
|
|
179
|
+
while (true) {
|
|
180
|
+
const item = await abortable(reader.read(), signal);
|
|
181
|
+
if (item.done) break;
|
|
182
|
+
write(item.value);
|
|
183
|
+
}
|
|
184
|
+
} else if (response.body?.[Symbol.asyncIterator]) {
|
|
185
|
+
const iterator = response.body[Symbol.asyncIterator]();
|
|
186
|
+
while (true) {
|
|
187
|
+
const item = await abortable(iterator.next(), signal);
|
|
188
|
+
if (item.done) break;
|
|
189
|
+
write(item.value);
|
|
190
|
+
}
|
|
191
|
+
} else if (typeof response.arrayBuffer === "function") {
|
|
192
|
+
write(await abortable(response.arrayBuffer(), signal));
|
|
193
|
+
} else {
|
|
194
|
+
throw new Error("artifact download response has no readable body");
|
|
195
|
+
}
|
|
196
|
+
onProgress(received, true);
|
|
197
|
+
} finally {
|
|
198
|
+
fs.closeSync(handle);
|
|
90
199
|
}
|
|
91
200
|
}
|
|
92
201
|
|
|
@@ -111,8 +220,8 @@ async function extractArchive(archivePath, outputDir) {
|
|
|
111
220
|
if (next > archive.length || compressedSize === 0xffffffff || size === 0xffffffff || localOffset === 0xffffffff) throw new Error("artifact archive entry metadata is invalid");
|
|
112
221
|
if (flags & 1) throw new Error("artifact archive contains an encrypted entry");
|
|
113
222
|
if (![0, 8].includes(method)) throw new Error(`artifact archive compression method is unsupported: ${method}`);
|
|
114
|
-
|
|
115
|
-
const rawName =
|
|
223
|
+
const rawNameBytes = archive.subarray(cursor + 46, cursor + 46 + nameLength);
|
|
224
|
+
const rawName = decodeZipFilename(rawNameBytes, flags);
|
|
116
225
|
const name = rawName.replace(/\\/g, "/"); const parts = name.split("/"); const mode = (attributes >>> 16) & 0xffff;
|
|
117
226
|
if (!name || name.startsWith("/") || /^[A-Za-z]:/.test(name) || parts.includes("..")) throw new Error(`artifact archive contains unsafe path: ${JSON.stringify(rawName)}`);
|
|
118
227
|
if ((mode & 0o170000) === 0o120000) throw new Error(`artifact archive contains a symbolic link: ${JSON.stringify(rawName)}`);
|
|
@@ -131,6 +240,14 @@ async function extractArchive(archivePath, outputDir) {
|
|
|
131
240
|
return files.sort();
|
|
132
241
|
}
|
|
133
242
|
|
|
243
|
+
function decodeZipFilename(bytes, flags) {
|
|
244
|
+
if (flags & 0x800) return bytes.toString("utf8");
|
|
245
|
+
// ZIP producers commonly omit the UTF-8 flag for ASCII entry names. Keep
|
|
246
|
+
// those names interoperable while refusing ambiguous non-ASCII encodings.
|
|
247
|
+
if (bytes.some((value) => value > 0x7f)) throw new Error("artifact archive contains an unsupported non-UTF-8 filename");
|
|
248
|
+
return bytes.toString("ascii");
|
|
249
|
+
}
|
|
250
|
+
|
|
134
251
|
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
252
|
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
253
|
|
|
@@ -14,13 +14,42 @@ Operator ------> Feature -> FeatureSet -> DatasetManifest -> DatasetArtifact
|
|
|
14
14
|
|
|
15
15
|
Treat `Recipe` and public `Feature.compute` as removed. Treat `computation_hash` as internal execution metadata, never as a business-managed asset.
|
|
16
16
|
|
|
17
|
+
## Platform Role And Evidence Boundaries
|
|
18
|
+
|
|
19
|
+
The ML Platform is a deterministic data-contract and data-delivery platform. It
|
|
20
|
+
registers versioned source Parameters, runs approved deterministic Operators,
|
|
21
|
+
orders single-column Features into FeatureSets, resolves DatasetManifests, and
|
|
22
|
+
materializes batch DatasetArtifacts or serves one causal realtime inference row.
|
|
23
|
+
It records versions, lineage, quality, missingness, freshness, and execution
|
|
24
|
+
metadata so an algorithm project can consume data without owning source routing
|
|
25
|
+
or cache details.
|
|
26
|
+
|
|
27
|
+
It does not train, evaluate, or serve models; own complete business feature
|
|
28
|
+
engineering; provision arbitrary source tables; or administer Kubernetes and
|
|
29
|
+
platform services. Read
|
|
30
|
+
[references/platform-capability-guide.md](references/platform-capability-guide.md)
|
|
31
|
+
when the user needs the detailed platform boundary or evidence-level explanation.
|
|
32
|
+
|
|
33
|
+
Keep these evidence rules active in every workflow:
|
|
34
|
+
|
|
35
|
+
- Platform defaults and capabilities are not business evidence.
|
|
36
|
+
- Dry-run, publish, resolve, build, metadata inspection, downloaded-file
|
|
37
|
+
inspection, and realtime fetch are separate evidence checkpoints; success at
|
|
38
|
+
one checkpoint does not imply success at the next.
|
|
39
|
+
- `validation.ok=true` does not by itself prove source semantics, causal formula
|
|
40
|
+
correctness, Parquet contents, or numeric parity.
|
|
41
|
+
- Dry-run, publish, and resolve may not detect a missing source relation. Do not
|
|
42
|
+
report source-backed success until build/fetch returns the required evidence,
|
|
43
|
+
and never rename a Parameter or remove fields merely to bypass a failed read.
|
|
44
|
+
|
|
17
45
|
## Load The Right Context
|
|
18
46
|
|
|
19
47
|
1. Resolve the Skill root as the directory containing this `SKILL.md`.
|
|
20
48
|
2. Read [references/contracts.md](references/contracts.md) before creating or changing JSON assets.
|
|
21
49
|
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
|
|
50
|
+
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
51
|
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.
|
|
52
|
+
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
53
|
|
|
25
54
|
## Initialize The Client
|
|
26
55
|
|
|
@@ -46,6 +75,19 @@ ml-platform show-config
|
|
|
46
75
|
ml-platform --profile server health
|
|
47
76
|
```
|
|
48
77
|
|
|
78
|
+
Use the global `--profile server` form shown above, before the business
|
|
79
|
+
command. Do not append `--profile` after a business command. Before discovery,
|
|
80
|
+
check the installed release with `ml-platform --help` and the relevant
|
|
81
|
+
subcommand's `--help`; CLI releases may differ in supported flags. In the
|
|
82
|
+
deployed 0.6.x CLI, registry `list-*` commands emit JSON by default and do not
|
|
83
|
+
accept a `--json` flag.
|
|
84
|
+
|
|
85
|
+
If the installed release rejects the global `--profile server` form shown above,
|
|
86
|
+
record the CLI syntax error and inspect `show-config`/subcommand help before
|
|
87
|
+
retrying the same read-only command without that flag when supported. Do not
|
|
88
|
+
switch clients or classify a syntax error as an API outage; keep each command
|
|
89
|
+
separate so CLI compatibility remains distinguishable from network health.
|
|
90
|
+
|
|
49
91
|
The effective API target is resolved in this order: explicit `--api-url`,
|
|
50
92
|
`ML_PLATFORM_API_URL`, then the saved value written by `ml-platform configure`.
|
|
51
93
|
If no target is configured, ask the user for an approved API URL before making
|
|
@@ -61,7 +103,7 @@ Collect only missing information. Do not invent a source table, source field, fo
|
|
|
61
103
|
|
|
62
104
|
Confirm:
|
|
63
105
|
|
|
64
|
-
- the target project (namespace) for the assets; use the built-in `default` only when the user has no dedicated project.
|
|
106
|
+
- 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
107
|
- business meaning, stable asset names, owner, and intended consumers;
|
|
66
108
|
- Parameter source adapter and credential-free source mapping;
|
|
67
109
|
- feature formula, exact Parameter dependencies, windows, inclusion rules, rounding, null behavior, and output dtype;
|
|
@@ -75,9 +117,64 @@ Confirm:
|
|
|
75
117
|
|
|
76
118
|
Separate preparation from mutation. Creating files and running local tests or `apply --dry-run` does not authorize publishing a wheel, changing the Registry, or submitting a build Job.
|
|
77
119
|
|
|
120
|
+
A label such as "10-minute mean" confirms neither its input Parameter nor a
|
|
121
|
+
complete formula. Before drafting an Operator or Feature, ask for the exact
|
|
122
|
+
versioned input Parameter(s), or for an approved source contract from which
|
|
123
|
+
they will be created. Do not infer an input from the Feature name or a similar
|
|
124
|
+
existing asset.
|
|
125
|
+
|
|
126
|
+
### Semantic Asset Review Gate
|
|
127
|
+
|
|
128
|
+
Treat resource correctness as a separate gate from workflow correctness. Before
|
|
129
|
+
creating Catalog JSON, write a local semantic asset review with one row per
|
|
130
|
+
Parameter, Operator, Feature and Dataset field:
|
|
131
|
+
|
|
132
|
+
```text
|
|
133
|
+
asset_key | business_meaning | source/evidence | confirmed_by | unresolved | proposed_value
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The review must distinguish three sources: confirmed business facts, facts
|
|
137
|
+
observed from the selected Project Registry/CLI, and agent proposals. A Skill
|
|
138
|
+
reference, schema default, similarly named old asset, or platform capability is
|
|
139
|
+
not business evidence. Do not convert a proposal into `proposed_value` without
|
|
140
|
+
explicit user confirmation or an exact match in the selected Project.
|
|
141
|
+
|
|
142
|
+
Stop with `need_business_confirmation` when any required source mapping, field,
|
|
143
|
+
unit, formula, time conversion, missing/quality policy, FeatureSet order,
|
|
144
|
+
dataset read policy, rowset/endpoint policy, owner, or approval boundary is
|
|
145
|
+
unresolved. Do not create placeholder Catalog files to make the workflow look
|
|
146
|
+
complete.
|
|
147
|
+
|
|
148
|
+
For a requested Feature, `exact Parameter dependencies` is an independent
|
|
149
|
+
required contract item. If the user names a transform or window but not its
|
|
150
|
+
input Parameter(s), include `input_parameter` in the unresolved questions and
|
|
151
|
+
stop. Do this before treating an Operator formula as complete: a time window,
|
|
152
|
+
cutoff rule, or Feature name never identifies the input.
|
|
153
|
+
|
|
154
|
+
After confirmation, show the proposed dependency closure and obtain approval of
|
|
155
|
+
the semantic asset plan before generating Catalog files. A successful local
|
|
156
|
+
schema check or remote dry-run is not semantic approval.
|
|
157
|
+
|
|
158
|
+
Before emitting or writing a draft, mechanically check that every asset carries
|
|
159
|
+
the confirmed `project` field, that every Feature input carries that same
|
|
160
|
+
`project`, and that every Operator `input_schema.parameters` reference is
|
|
161
|
+
qualified as `project/parameter:version`. The schema's `default` values are not
|
|
162
|
+
valid substitutes for an explicit non-default target. When a confirmed Feature
|
|
163
|
+
config defines window closure, cutoff inclusion, duplicate handling, or empty
|
|
164
|
+
window behavior, copy all of those fields into the Feature config rather than
|
|
165
|
+
reducing it to the window string.
|
|
166
|
+
|
|
78
167
|
## Discover Existing Assets
|
|
79
168
|
|
|
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.
|
|
169
|
+
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.
|
|
170
|
+
|
|
171
|
+
CLI readiness is a hard gate for every Registry conclusion. If `command -v`,
|
|
172
|
+
`version`, `show-config`, or server `health` fails, returns a nonzero status, or
|
|
173
|
+
does not identify the intended target, stop with `platform_not_ready`. Do not
|
|
174
|
+
list assets, select reuse versus authoring, dry-run, publish, build, fetch, or
|
|
175
|
+
report a Registry fact from prompt text, a remembered response, or a similar
|
|
176
|
+
local asset. Report the failed command and its configured target; resume only
|
|
177
|
+
after the user/environment restores the same target.
|
|
81
178
|
|
|
82
179
|
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
180
|
|
|
@@ -140,8 +237,29 @@ Keep platform demos and V95 built-ins unchanged. Do not put unrelated business a
|
|
|
140
237
|
|
|
141
238
|
### Define Parameters
|
|
142
239
|
|
|
240
|
+
Use the platform-capability guide's asset table before choosing the resource
|
|
241
|
+
type. For each Parameter record the business signal, exact source evidence,
|
|
242
|
+
unit, time/availability semantics and missing policy in the semantic review.
|
|
243
|
+
Never infer a source table, column, measurement, tag, unit or timezone from a
|
|
244
|
+
Parameter name.
|
|
245
|
+
|
|
143
246
|
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
247
|
|
|
248
|
+
When the approved source contract supplies SQL, copy its query text and named
|
|
249
|
+
parameters exactly into `source.sql`/`source.params`. Do not retype, beautify,
|
|
250
|
+
rename an identifier, change a join key, or "simplify" an approved query while
|
|
251
|
+
drafting. SQL whitespace may be formatted only when a reviewed formatter proves
|
|
252
|
+
the token stream is unchanged; every source identifier and `<= %(end)s` boundary
|
|
253
|
+
remains part of the Parameter contract.
|
|
254
|
+
|
|
255
|
+
For SQL Parameters, the runtime contract is a single statement: use the exact
|
|
256
|
+
`source.sql` field and a mapping under `source.params` for every named
|
|
257
|
+
placeholder other than the reserved `start` and `end` window parameters. Do not
|
|
258
|
+
use `source.parameters` (it is not a supported alias and may be silently
|
|
259
|
+
dropped by older CLI/server releases). Do not leave a trailing semicolon; the
|
|
260
|
+
deployed Worker rejects SQL containing multiple statements. Verify the resolved
|
|
261
|
+
Parameter still contains the named params before authorizing a build.
|
|
262
|
+
|
|
145
263
|
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
264
|
|
|
147
265
|
Do not represent a rolling mean, lag, ratio, trend, or model input as a Parameter. Those are Features.
|
|
@@ -166,7 +284,11 @@ Use a meaningful immutable `function_hash`. Change the Operator version whenever
|
|
|
166
284
|
|
|
167
285
|
### Define Features
|
|
168
286
|
|
|
169
|
-
Create one JSON file per single output column.
|
|
287
|
+
Create one JSON file per single output column. Set the confirmed `project` on the
|
|
288
|
+
Feature and on every nested Feature input; a FeatureInput without `project`
|
|
289
|
+
silently targets `default`. Bind exactly one Operator version and physical
|
|
290
|
+
`output_column`. List only the Parameter versions actually required by that
|
|
291
|
+
column's formula.
|
|
170
292
|
|
|
171
293
|
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
294
|
|
|
@@ -176,8 +298,41 @@ Reference immutable Feature versions in the exact consumer column order. Keep th
|
|
|
176
298
|
|
|
177
299
|
### Define The DatasetManifest
|
|
178
300
|
|
|
301
|
+
Do not use DatasetManifest as a catch-all for unresolved semantics. Confirm the
|
|
302
|
+
dataset purpose and consumer, then separately confirm the target grid, horizon,
|
|
303
|
+
read policy, explicit Parameter outputs, rowset or rowset splits, abnormal windows and
|
|
304
|
+
endpoint eligibility. Keep the semantic asset review beside the draft Catalog
|
|
305
|
+
so a reviewer can compare business decisions with generated fields.
|
|
306
|
+
|
|
179
307
|
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
308
|
|
|
309
|
+
Map the current Manifest schema exactly rather than carrying descriptive fields
|
|
310
|
+
from Parameter, Feature, or FeatureSet into it. A target Parameter reference is
|
|
311
|
+
**nested**; never flatten its `version` or `project` onto `target`:
|
|
312
|
+
|
|
313
|
+
```json
|
|
314
|
+
"target": {
|
|
315
|
+
"parameter": {"parameter": "hot_metal_si", "version": "v1", "project": "<project>"},
|
|
316
|
+
"si_time_source": "dispatch_time",
|
|
317
|
+
"offset_minutes": 20,
|
|
318
|
+
"interpolation": {"method": "linear"}
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`target.interpolation` is `{"method": "linear"}`, `{"method": "none"}` or
|
|
323
|
+
`{"method": "forward_fill"}` -- neither it nor `target.parameter` accepts a
|
|
324
|
+
bare string. The current DatasetManifest has no top-level `owner`,
|
|
325
|
+
`description`, `si_null_policy`, or `preserve_all_candidate_rows`. Map a
|
|
326
|
+
business statement only to an existing schema field (for example `mark_only`,
|
|
327
|
+
`endpoint_policy`, a Feature config, or a Parameter missing policy); otherwise
|
|
328
|
+
keep it in the semantic review and ask for a platform/schema change. Validate
|
|
329
|
+
the generated object against the current schema before running `apply --dry-run`.
|
|
330
|
+
|
|
331
|
+
When a confirmed missing-data policy applies to a Feature-only Parameter, put it
|
|
332
|
+
in `parameter_missing_policies`, using `parameter`, `version`, `project`, and
|
|
333
|
+
`policy`. Do not copy the `ParameterRequest` names `name` and `missing_policy`
|
|
334
|
+
into that list. Omit the section when no such policy is confirmed.
|
|
335
|
+
|
|
181
336
|
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
337
|
|
|
183
338
|
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 +357,16 @@ freshness, tail-edge, gap-fill, and rowset contracts.
|
|
|
202
357
|
|
|
203
358
|
Run, in order:
|
|
204
359
|
|
|
205
|
-
1.
|
|
206
|
-
2.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
360
|
+
1. Semantic asset review: no required unresolved field and every proposed value has a source or user confirmation.
|
|
361
|
+
2. Operator unit tests. Before building, turn every applicable item in
|
|
362
|
+
`operator-authoring.md` into a separately named test; do not let one broad
|
|
363
|
+
happy-path assertion substitute for cutoff-before/at/after, duplicate and
|
|
364
|
+
missing input, empty history, requested-output, event-time order, or
|
|
365
|
+
dtype/rounding coverage. The template is only a starting point.
|
|
366
|
+
3. Wheel build and wheel filename verification.
|
|
367
|
+
4. Catalog path, schema, dependency, immutability, and package validation with `apply --dry-run` against the same target profile intended for publication.
|
|
368
|
+
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.
|
|
369
|
+
6. A human-readable summary of planned new, unchanged, retained, and conflicting assets.
|
|
210
370
|
|
|
211
371
|
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
372
|
|
|
@@ -214,7 +374,7 @@ Stop on any error. Do not weaken schema validation, fabricate a missing dependen
|
|
|
214
374
|
|
|
215
375
|
Publish only after the user explicitly approves Registry and wheel changes. Use catalog `apply` so publication follows Parameter -> Operator -> Feature -> FeatureSet -> Dataset order.
|
|
216
376
|
|
|
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`.
|
|
377
|
+
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
378
|
|
|
219
379
|
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
380
|
|
|
@@ -232,6 +392,14 @@ gap-filled inputs as degraded freshness, not as a normal read.
|
|
|
232
392
|
|
|
233
393
|
## Verify The Artifact
|
|
234
394
|
|
|
395
|
+
Treat Registry metadata inspection and downloaded-file inspection as separate
|
|
396
|
+
checkpoints. A successful `get-dataset-artifact` can establish the immutable
|
|
397
|
+
identity, object inventory, row count, validation summary, and lineage metadata;
|
|
398
|
+
it cannot establish Parquet schema, column order, or file-content hashes. If a
|
|
399
|
+
download times out, report whether no response/progress was observed or whether
|
|
400
|
+
some body bytes were received, preserve the metadata-only evidence, and leave
|
|
401
|
+
downloaded-file validation explicitly incomplete.
|
|
402
|
+
|
|
235
403
|
Require all of the following before reporting success:
|
|
236
404
|
|
|
237
405
|
- 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,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",
|
package/skills/feature-management/assets/catalog-template/operator_package/tests/test_operator.py
CHANGED
|
@@ -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(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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__":
|