@zhuoyuezs/ml-platform 0.1.0

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.
Files changed (29) hide show
  1. package/DEVELOPMENT.md +189 -0
  2. package/README.md +103 -0
  3. package/checksums.json +110 -0
  4. package/package.json +29 -0
  5. package/release-policy.json +31 -0
  6. package/release.json +42 -0
  7. package/runtime/business-client/README.md +14 -0
  8. package/runtime/business-client/package-lock.json +19 -0
  9. package/runtime/business-client/package.json +21 -0
  10. package/runtime/business-client/src/catalog.js +184 -0
  11. package/runtime/business-client/src/cli.js +225 -0
  12. package/runtime/business-client/src/config.js +52 -0
  13. package/runtime/business-client/src/http.js +137 -0
  14. package/scripts/lib.js +819 -0
  15. package/scripts/main.js +92 -0
  16. package/skills/feature-management/SKILL.md +265 -0
  17. package/skills/feature-management/agents/openai.yaml +4 -0
  18. package/skills/feature-management/assets/catalog-template/catalog.json +23 -0
  19. package/skills/feature-management/assets/catalog-template/datasets/example_temperature_training.v1.json +24 -0
  20. package/skills/feature-management/assets/catalog-template/feature_sets/example_temperature_core.v1.json +13 -0
  21. package/skills/feature-management/assets/catalog-template/features/example_temperature_mean_5m.v1.json +21 -0
  22. package/skills/feature-management/assets/catalog-template/operator_package/pyproject.toml +12 -0
  23. package/skills/feature-management/assets/catalog-template/operator_package/src/business_feature_operator_template/__init__.py +39 -0
  24. package/skills/feature-management/assets/catalog-template/operator_package/tests/test_operator.py +56 -0
  25. package/skills/feature-management/assets/catalog-template/operators/example_temperature_features.v1.json +43 -0
  26. package/skills/feature-management/assets/catalog-template/parameters/example_temperature.v1.json +57 -0
  27. package/skills/feature-management/references/commands.md +244 -0
  28. package/skills/feature-management/references/contracts.md +682 -0
  29. package/skills/feature-management/references/operator-authoring.md +167 -0
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const path = require("path");
6
+ const zlib = require("zlib");
7
+ const { expandHome } = require("./config");
8
+
9
+ class PlatformApiClient {
10
+ constructor(baseUrl, timeoutSeconds = 30) {
11
+ this.baseUrl = String(baseUrl).trim().replace(/\/+$/, "");
12
+ if (!/^https?:\/\//.test(this.baseUrl)) throw new Error("server profile api_url must start with http:// or https://");
13
+ if (!(timeoutSeconds > 0)) throw new Error("request timeout must be positive");
14
+ this.timeoutMs = timeoutSeconds * 1000;
15
+ }
16
+
17
+ url(endpoint, params = {}) {
18
+ const url = new URL(endpoint.replace(/^\//, ""), `${this.baseUrl}/`);
19
+ for (const [key, value] of Object.entries(params)) {
20
+ if (value !== null && value !== undefined) url.searchParams.set(key, typeof value === "boolean" ? String(value) : value);
21
+ }
22
+ return url;
23
+ }
24
+
25
+ async request(method, endpoint, { params, payload, body, contentType } = {}) {
26
+ const controller = new AbortController();
27
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
28
+ const headers = { Accept: "application/json" };
29
+ let requestBody = body;
30
+ if (payload !== undefined) {
31
+ headers["Content-Type"] = "application/json";
32
+ requestBody = JSON.stringify(payload);
33
+ } else if (contentType) headers["Content-Type"] = contentType;
34
+ let response;
35
+ try { response = await fetch(this.url(endpoint, params), { method, headers, body: requestBody, signal: controller.signal }); }
36
+ catch (error) { throw new Error(`platform API request failed: ${method} ${this.url(endpoint, params)}: ${error.message}`); }
37
+ finally { clearTimeout(timer); }
38
+ const text = await response.text();
39
+ let result;
40
+ try { result = JSON.parse(text); } catch (_) { throw new Error("platform API returned invalid JSON"); }
41
+ if (!response.ok) throw new Error(`platform API request failed: ${method} ${response.url}: HTTP ${response.status}: ${result?.detail ?? response.statusText}`);
42
+ if (!result || typeof result !== "object") throw new Error("platform API returned an unsupported JSON response");
43
+ return result;
44
+ }
45
+
46
+ get(endpoint, params) { return this.request("GET", endpoint, { params }); }
47
+ post(endpoint, payload, params) { return this.request("POST", endpoint, { payload, params }); }
48
+ delete(endpoint, params) { return this.request("DELETE", endpoint, { params }); }
49
+ uploadOperatorPackage(project, name, version, packagePath) {
50
+ const resolved = path.resolve(expandHome(packagePath));
51
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) throw new Error(`operator package not found: ${resolved}`);
52
+ return this.request("POST", `/operator-packages/${encodeURIComponent(project)}/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, {
53
+ params: { filename: path.basename(resolved) }, body: fs.readFileSync(resolved), contentType: "application/octet-stream",
54
+ });
55
+ }
56
+
57
+ async downloadDatasetArtifact(project, datasetId, manifestHash, outputDir, force = false) {
58
+ const target = path.resolve(expandHome(outputDir));
59
+ if (fs.existsSync(target) && fs.readdirSync(target).length) {
60
+ if (!force) throw new Error(`download output directory is not empty: ${target}`);
61
+ fs.rmSync(target, { recursive: true, force: true });
62
+ }
63
+ fs.mkdirSync(target, { recursive: true });
64
+ const controller = new AbortController();
65
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
66
+ let response;
67
+ try {
68
+ 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()));
82
+ const files = await extractArchive(temporary, target);
83
+ return { dataset_id: datasetId, manifest_hash: manifestHash, output_dir: target, file_count: files.length, files };
84
+ } catch (error) {
85
+ fs.rmSync(target, { recursive: true, force: true });
86
+ throw error;
87
+ } finally {
88
+ fs.rmSync(path.dirname(temporary), { recursive: true, force: true });
89
+ }
90
+ }
91
+ }
92
+
93
+ async function extractArchive(archivePath, outputDir) {
94
+ const archive = fs.readFileSync(archivePath);
95
+ const end = findEndOfCentralDirectory(archive);
96
+ const entryCount = archive.readUInt16LE(end + 10);
97
+ const centralSize = archive.readUInt32LE(end + 12);
98
+ const centralOffset = archive.readUInt32LE(end + 16);
99
+ if (entryCount === 0xffff || centralSize === 0xffffffff || centralOffset === 0xffffffff) throw new Error("artifact archive ZIP64 format is unsupported");
100
+ if (centralOffset + centralSize > end) throw new Error("artifact archive central directory is invalid");
101
+ const files = [];
102
+ const names = new Set();
103
+ let cursor = centralOffset;
104
+ for (let index = 0; index < entryCount; index += 1) {
105
+ if (cursor + 46 > archive.length || archive.readUInt32LE(cursor) !== 0x02014b50) throw new Error("artifact archive central directory is invalid");
106
+ const flags = archive.readUInt16LE(cursor + 8); const method = archive.readUInt16LE(cursor + 10);
107
+ const crc = archive.readUInt32LE(cursor + 16); const compressedSize = archive.readUInt32LE(cursor + 20); const size = archive.readUInt32LE(cursor + 24);
108
+ const nameLength = archive.readUInt16LE(cursor + 28); const extraLength = archive.readUInt16LE(cursor + 30); const commentLength = archive.readUInt16LE(cursor + 32);
109
+ const attributes = archive.readUInt32LE(cursor + 38); const localOffset = archive.readUInt32LE(cursor + 42);
110
+ const next = cursor + 46 + nameLength + extraLength + commentLength;
111
+ if (next > archive.length || compressedSize === 0xffffffff || size === 0xffffffff || localOffset === 0xffffffff) throw new Error("artifact archive entry metadata is invalid");
112
+ if (flags & 1) throw new Error("artifact archive contains an encrypted entry");
113
+ 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");
116
+ const name = rawName.replace(/\\/g, "/"); const parts = name.split("/"); const mode = (attributes >>> 16) & 0xffff;
117
+ if (!name || name.startsWith("/") || /^[A-Za-z]:/.test(name) || parts.includes("..")) throw new Error(`artifact archive contains unsafe path: ${JSON.stringify(rawName)}`);
118
+ if ((mode & 0o170000) === 0o120000) throw new Error(`artifact archive contains a symbolic link: ${JSON.stringify(rawName)}`);
119
+ if (names.has(name)) throw new Error(`artifact archive contains duplicate path: ${JSON.stringify(rawName)}`); names.add(name);
120
+ const targetRoot = path.resolve(outputDir); const target = path.resolve(targetRoot, ...parts);
121
+ if (target !== targetRoot && !target.startsWith(`${targetRoot}${path.sep}`)) throw new Error(`artifact archive path escapes output directory: ${JSON.stringify(rawName)}`);
122
+ if (name.endsWith("/")) { fs.mkdirSync(target, { recursive: true }); cursor = next; continue; }
123
+ if (localOffset + 30 > archive.length || archive.readUInt32LE(localOffset) !== 0x04034b50) throw new Error("artifact archive local entry is invalid");
124
+ const localNameLength = archive.readUInt16LE(localOffset + 26); const localExtraLength = archive.readUInt16LE(localOffset + 28); const dataOffset = localOffset + 30 + localNameLength + localExtraLength;
125
+ if (dataOffset + compressedSize > archive.length) throw new Error("artifact archive entry data is truncated");
126
+ const compressed = archive.subarray(dataOffset, dataOffset + compressedSize); const content = method === 0 ? compressed : zlib.inflateRawSync(compressed);
127
+ if (content.length !== size || crc32(content) !== crc) throw new Error(`artifact archive entry integrity check failed: ${JSON.stringify(rawName)}`);
128
+ fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, content, { flag: "wx" }); files.push(name); cursor = next;
129
+ }
130
+ if (cursor !== centralOffset + centralSize) throw new Error("artifact archive central directory size is invalid");
131
+ return files.sort();
132
+ }
133
+
134
+ 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
+ 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
+
137
+ module.exports = { PlatformApiClient, extractArchive };