@zhuoyuezs/ml-platform 0.1.10 → 0.1.12

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 (34) hide show
  1. package/checksums.json +155 -0
  2. package/package.json +3 -2
  3. package/release.json +52 -0
  4. package/runtime/business-client/README.md +110 -0
  5. package/runtime/business-client/package-lock.json +19 -0
  6. package/runtime/business-client/package.json +23 -0
  7. package/runtime/business-client/src/catalog.js +214 -0
  8. package/runtime/business-client/src/cli.js +538 -0
  9. package/runtime/business-client/src/config.js +56 -0
  10. package/runtime/business-client/src/http.js +254 -0
  11. package/scripts/verify-release-package.js +28 -0
  12. package/skills/feature-management/SKILL.md +491 -0
  13. package/skills/feature-management/agents/openai.yaml +4 -0
  14. package/skills/feature-management/assets/catalog-template/catalog.json +23 -0
  15. package/skills/feature-management/assets/catalog-template/datasets/example_temperature_training.v1.json +40 -0
  16. package/skills/feature-management/assets/catalog-template/feature_sets/example_temperature_core.v1.json +14 -0
  17. package/skills/feature-management/assets/catalog-template/features/example_temperature_mean_5m.v1.json +28 -0
  18. package/skills/feature-management/assets/catalog-template/operator_package/pyproject.toml +12 -0
  19. package/skills/feature-management/assets/catalog-template/operator_package/src/business_feature_operator_template/__init__.py +39 -0
  20. package/skills/feature-management/assets/catalog-template/operator_package/tests/test_operator.py +83 -0
  21. package/skills/feature-management/assets/catalog-template/operators/example_temperature_features.v1.json +58 -0
  22. package/skills/feature-management/assets/catalog-template/parameters/example_temperature.v1.json +58 -0
  23. package/skills/feature-management/references/commands.md +358 -0
  24. package/skills/feature-management/references/contracts.md +766 -0
  25. package/skills/feature-management/references/operator-authoring.md +175 -0
  26. package/skills/feature-management/references/platform-capability-guide.md +75 -0
  27. package/skills/feature-management/references/supervised-datasets.md +101 -0
  28. package/skills/model-lifecycle-management/SKILL.md +38 -0
  29. package/skills/model-lifecycle-management/agents/openai.yaml +4 -0
  30. package/skills/model-lifecycle-management/references/discovery.md +89 -0
  31. package/skills/model-lifecycle-management/references/evaluation.md +172 -0
  32. package/skills/model-lifecycle-management/references/packaging.md +58 -0
  33. package/skills/model-lifecycle-management/references/training-contracts.md +259 -0
  34. package/skills/model-lifecycle-management/references/training.md +107 -0
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const idPattern = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
8
+ const outputColumnPattern = /^[^\s/\\\x00-\x1f\x7f][^/\\\x00-\x1f\x7f]{0,127}$/;
9
+ const wheelDigestPattern = /^sha256:[0-9a-f]{64}$/;
10
+ const wheelFilenamePattern = /^[A-Za-z0-9][A-Za-z0-9_.-]*\.whl$/;
11
+ const timestampPattern = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,6}))?)?(?:Z|([+-])(\d{2}):?(\d{2}))?)?$/;
12
+
13
+ function object(value, label) { if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`); return value; }
14
+ function hasOwn(value, key) { return Object.prototype.hasOwnProperty.call(value, key); }
15
+ function defaultIfAbsent(value, key, fallback) { return hasOwn(value, key) ? value[key] : fallback; }
16
+ function aliasIfAbsent(value, key, alias) { return hasOwn(value, key) ? value[key] : value[alias]; }
17
+ function pythonRepr(value) {
18
+ if (value === null) return "None";
19
+ if (value === true) return "True";
20
+ if (value === false) return "False";
21
+ if (typeof value === "string") return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
22
+ if (Array.isArray(value)) return `[${value.map(pythonRepr).join(", ")}]`;
23
+ if (value && typeof value === "object") return `{${Object.entries(value).map(([key, item]) => `${pythonRepr(key)}: ${pythonRepr(item)}`).join(", ")}}`;
24
+ return String(value);
25
+ }
26
+ function pythonString(value) { return typeof value === "string" ? value : pythonRepr(value); }
27
+ function identifier(value, label) { if (typeof value !== "string" || !idPattern.test(value)) throw new Error(`${label} is invalid`); return value; }
28
+ function exactKeys(value, allowed, required, label) { const keys = Object.keys(object(value, label)); const missing = required.filter((item) => !keys.includes(item)); const extra = keys.filter((item) => !allowed.includes(item)); if (missing.length) throw new Error(`${label} is missing required fields: ${missing.sort().join(", ")}`); if (extra.length) throw new Error(`${label} has unsupported fields: ${extra.sort().join(", ")}`); }
29
+ function requiredString(value, label) { if (typeof value !== "string" || !value) throw new Error(`${label} must be a non-empty string`); return value; }
30
+ function optionalString(value, label) { if (value == null) return null; if (typeof value !== "string") throw new Error(`${label} must be a string or null`); return value; }
31
+ function choice(value, choices, label) { if (!choices.includes(value)) throw new Error(`${label} must be one of ${choices.join(", ")}`); return value; }
32
+ function bool(value, label) { if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`); return value; }
33
+ function objectList(value, label) { if (!Array.isArray(value) || value.some((item) => !item || typeof item !== "object" || Array.isArray(item))) throw new Error(`${label} must be a list of objects`); return value; }
34
+ function stringMap(value, label) { object(value, label); if (Object.entries(value).some(([name, item]) => typeof name !== "string" || typeof item !== "string")) throw new Error(`${label} must contain only string keys and values`); return value; }
35
+ function unique(values, label) { if (new Set(values).size !== values.length) throw new Error(`${label} must be unique`); }
36
+ function ref(value, label) { exactKeys(value, ["name", "version", "project"], ["name", "version"], label); return { name: identifier(value.name, `${label} name`), version: identifier(value.version, `${label} version`) }; }
37
+ function parameterRef(value, parentProject) { exactKeys(value, ["parameter", "version", "project"], ["parameter", "version"], "feature input"); const project = identifier(defaultIfAbsent(value, "project", "default"), "feature input project"); if (parentProject && project !== parentProject) throw new Error("feature input cross-project reference is not allowed"); return { parameter: identifier(value.parameter, "feature input parameter"), version: identifier(value.version, "feature input version"), project }; }
38
+ function key(value) { return `${value.name}:${value.version}`; }
39
+ function datasetKey(value) { return `${value.dataset_id}:${value.dataset_version}`; }
40
+ function referenceKey(value) { return `${value.name}:${value.version}`; }
41
+ function parameterReferenceKey(value) { return `${value.parameter}:${value.version}`; }
42
+
43
+ function normalizeRounding(value) { if (value == null) return null; exactKeys(value, ["mode", "decimals"], [], "parameter rounding"); const decimals = defaultIfAbsent(value, "decimals", 2); if (!Number.isInteger(decimals) || decimals < 0 || decimals > 12) throw new Error("parameter rounding decimals must be an integer between 0 and 12"); return { mode: choice(defaultIfAbsent(value, "mode", "half_up"), ["half_up", "half_even"], "parameter rounding mode"), decimals }; }
44
+ function durationSeconds(value) { if (typeof value !== "string") return null; const match = /^P(?:(?<days>\d+(?:\.\d+)?)D)?(?:T(?:(?<hours>\d+(?:\.\d+)?)H)?(?:(?<minutes>\d+(?:\.\d+)?)M)?(?:(?<seconds>\d+(?:\.\d+)?)S)?)?$/.exec(value); if (!match || !Object.values(match.groups).some(Boolean)) return null; return Number(match.groups.days || 0) * 86400 + Number(match.groups.hours || 0) * 3600 + Number(match.groups.minutes || 0) * 60 + Number(match.groups.seconds || 0); }
45
+ function optionalDuration(value, label) { if (value == null) return null; if (durationSeconds(value) == null) throw new Error(`${label} must be a non-negative ISO-8601 duration`); return value; }
46
+ function normalizeAvailability(value) { if (value == null) return null; exactKeys(value, ["strategy", "field", "delay", "accuracy", "revision_id_field"], ["strategy"], "parameter availability"); const strategy = choice(value.strategy, ["source_field", "fixed_delay", "unsupported"], "parameter availability strategy"); const field = optionalString(value.field, "parameter availability field"); const delay = optionalDuration(value.delay, "parameter availability delay"); const accuracy = value.accuracy == null ? null : choice(value.accuracy, ["exact", "upper_bound", "estimated"], "parameter availability accuracy"); const revision_id_field = optionalString(value.revision_id_field, "parameter availability revision_id_field"); if (strategy === "source_field" && (!field || delay != null || accuracy == null)) throw new Error("parameter availability source_field requires field and accuracy, without delay"); if (strategy === "fixed_delay" && (field != null || delay == null || accuracy == null)) throw new Error("parameter availability fixed_delay requires delay and accuracy, without field"); if (strategy === "unsupported" && [field, delay, accuracy, revision_id_field].some((item) => item != null)) throw new Error("parameter availability unsupported strategy accepts no other fields"); return { strategy, field, delay, accuracy, revision_id_field }; }
47
+ function normalizeAvailabilitySla(value) { if (value == null) return null; exactKeys(value, ["expected_delay", "max_delay"], [], "parameter availability_sla"); const expected_delay = optionalDuration(value.expected_delay, "parameter availability_sla expected_delay"); const max_delay = optionalDuration(value.max_delay, "parameter availability_sla max_delay"); if (expected_delay && max_delay && durationSeconds(expected_delay) > durationSeconds(max_delay)) throw new Error("parameter availability_sla expected_delay must not exceed max_delay"); return { expected_delay, max_delay }; }
48
+ function validRange(value) { if (value == null) return null; if (!Array.isArray(value) || value.length !== 2 || value.some((item) => item != null && (typeof item !== "number" || !Number.isFinite(item)))) throw new Error("quality_rules.valid_range must contain exactly two numbers or null values"); if (value[0] == null && value[1] == null) throw new Error("quality_rules.valid_range requires at least one of min/max"); if (value[0] != null && value[1] != null && value[0] > value[1]) throw new Error("quality_rules.valid_range min must not exceed max"); return value.map((item) => item == null ? null : Number(item)); }
49
+ function number(value, label) { if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${label} must be numeric`); return value; }
50
+ function boundedRate(value, label) { const result = number(value, label); if (result < 0 || result > 1) throw new Error(`${label} must be between 0 and 1`); return result; }
51
+ function nonNegativeNumber(value, label) { const result = number(value, label); if (result < 0) throw new Error(`${label} must be a non-negative number`); return result; }
52
+ function canonicalShortDuration(value, label) { if (typeof value !== "string") throw new Error(`${label} must use a positive short duration`); const match = /^(\d+)(s|min|m|h|d)$/.exec(value.trim()); if (!match || Number(match[1]) <= 0) throw new Error(`${label} must use a positive short duration`); const seconds = Number(match[1]) * { s: 1, min: 60, m: 60, h: 3600, d: 86400 }[match[2]]; if (seconds % 86400 === 0) return `${seconds / 86400}d`; if (seconds % 3600 === 0) return `${seconds / 3600}h`; if (seconds % 60 === 0) return `${seconds / 60}min`; return `${seconds}s`; }
53
+ function normalizeMissingDetection(value) { if (value == null) return null; exactKeys(value, ["mode", "tolerance_ratio", "max_gap", "multiplier", "baseline", "min_observations"], [], "parameter quality_rules missing_detection"); const mode = choice(defaultIfAbsent(value, "mode", "disabled"), ["disabled", "fixed_frequency", "max_gap", "interval_multiplier"], "parameter quality_rules missing_detection mode"); const tolerance_ratio = number(defaultIfAbsent(value, "tolerance_ratio", 0.25), "parameter quality_rules missing_detection tolerance_ratio"); if (tolerance_ratio < 0 || tolerance_ratio >= 1) throw new Error("parameter quality_rules missing_detection tolerance_ratio must be between 0 and 1"); const max_gap = value.max_gap == null ? null : canonicalShortDuration(value.max_gap, "parameter quality_rules missing_detection max_gap"); if (mode === "max_gap" && max_gap == null) throw new Error("parameter quality_rules missing_detection mode=max_gap requires max_gap"); if (mode !== "max_gap" && max_gap != null) throw new Error("parameter quality_rules missing_detection max_gap is only valid for mode=max_gap"); const multiplier = number(defaultIfAbsent(value, "multiplier", 10), "parameter quality_rules missing_detection multiplier"); if (multiplier <= 1) throw new Error("parameter quality_rules missing_detection multiplier must be greater than 1"); const baseline = choice(defaultIfAbsent(value, "baseline", "median"), ["mean", "median"], "parameter quality_rules missing_detection baseline"); const min_observations = defaultIfAbsent(value, "min_observations", 2); if (!Number.isInteger(min_observations) || min_observations < 2) throw new Error("parameter quality_rules missing_detection min_observations must be an integer >= 2"); return { mode, tolerance_ratio, max_gap, multiplier, baseline, min_observations }; }
54
+ function normalizeAcceptance(value, label) { exactKeys(value, ["max_violation_rate", "min_evaluated_rows", "min_comparable_rate"], [], `${label} acceptance`); const min_evaluated_rows = defaultIfAbsent(value, "min_evaluated_rows", 1); if (!Number.isInteger(min_evaluated_rows) || min_evaluated_rows < 0) throw new Error(`${label} acceptance min_evaluated_rows must be a non-negative integer`); return { max_violation_rate: boundedRate(defaultIfAbsent(value, "max_violation_rate", 0), `${label} acceptance max_violation_rate`), min_evaluated_rows, min_comparable_rate: boundedRate(defaultIfAbsent(value, "min_comparable_rate", 0), `${label} acceptance min_comparable_rate`) }; }
55
+ function normalizeSingleCheck(value, label) { object(value, `${label} check`); const type = choice(value.type, ["not_null", "finite", "range", "max_gap"], `${label} check type`); if (type === "not_null" || type === "finite") { exactKeys(value, ["type"], ["type"], `${label} check`); return { type }; } if (type === "max_gap") { exactKeys(value, ["type", "max_gap"], ["type", "max_gap"], `${label} check`); const max_gap = optionalDuration(value.max_gap, `${label} check max_gap`); if (durationSeconds(max_gap) === 0) throw new Error(`${label} check max_gap must be positive`); return { type, max_gap }; } exactKeys(value, ["type", "min", "max", "inclusive_min", "inclusive_max"], ["type"], `${label} check`); const range = validRange([defaultIfAbsent(value, "min", null), defaultIfAbsent(value, "max", null)]); return { type, min: range[0], max: range[1], inclusive_min: bool(defaultIfAbsent(value, "inclusive_min", true), `${label} check inclusive_min`), inclusive_max: bool(defaultIfAbsent(value, "inclusive_max", true), `${label} check inclusive_max`) }; }
56
+ function normalizeConstraintRule(value) { const label = "parameter quality rule"; exactKeys(value, ["id", "stage", "check", "acceptance", "enforcement", "description"], ["id", "check"], label); return { id: identifier(value.id, `${label} id`), stage: choice(defaultIfAbsent(value, "stage", "normalized_source"), ["normalized_source", "post_preprocess", "aligned_grid"], `${label} stage`), check: normalizeSingleCheck(value.check, label), acceptance: normalizeAcceptance(object(defaultIfAbsent(value, "acceptance", {}), `${label} acceptance`), label), enforcement: choice(defaultIfAbsent(value, "enforcement", "warn"), ["observe", "warn", "fail"], `${label} enforcement`), description: pythonString(defaultIfAbsent(value, "description", "")) }; }
57
+ function normalizeQualityRules(value) { exactKeys(value, ["valid_range", "allow_missing", "missing_detection", "rules"], [], "parameter quality_rules"); const normalizedRange = validRange(value.valid_range); const allowMissing = bool(defaultIfAbsent(value, "allow_missing", true), "allow_missing"); const rules = objectList(defaultIfAbsent(value, "rules", []), "parameter quality rules").map(normalizeConstraintRule); unique(rules.map((item) => item.id), "parameter quality_rules rule ids"); const reserved = new Set(); if (normalizedRange != null) reserved.add("contract_valid_range"); if (!allowMissing) reserved.add("contract_allow_missing"); if (rules.some((item) => reserved.has(item.id))) throw new Error("parameter quality_rules uses reserved compatibility rule ids"); const result = { valid_range: normalizedRange, allow_missing: allowMissing, rules }; const missing = normalizeMissingDetection(value.missing_detection); if (missing != null) result.missing_detection = missing; return result; }
58
+ function normalizeCodeArtifact(value) { object(value, "operator code_artifact"); const sha256 = requiredString(value.sha256, "code_artifact sha256"); if (!wheelDigestPattern.test(sha256)) throw new Error("code_artifact sha256 is invalid"); const filename = requiredString(value.filename, "code_artifact filename"); if (path.basename(filename) !== filename || !wheelFilenamePattern.test(filename)) throw new Error("code_artifact filename is invalid"); if (!Number.isInteger(value.size_bytes) || value.size_bytes <= 0) throw new Error("code_artifact size_bytes must be a positive integer"); if (defaultIfAbsent(value, "format", "wheel") !== "wheel") throw new Error("code_artifact format must be wheel"); return { uri: requiredString(value.uri, "code_artifact uri"), sha256, format: "wheel", filename, size_bytes: value.size_bytes, python_version: optionalString(value.python_version, "code_artifact python_version"), dependency_lock_hash: optionalString(value.dependency_lock_hash, "code_artifact dependency_lock_hash") }; }
59
+
60
+ function normalizeSource(value, label) {
61
+ const source = object(value, label);
62
+ if (Object.prototype.hasOwnProperty.call(source, "parameters")) throw new Error(`${label} uses unsupported field 'parameters'; use 'params'`);
63
+ if (source.mode === "sql" && typeof source.sql === "string" && source.sql.trimEnd().endsWith(";")) throw new Error(`SQL ${label}.sql must not end with a semicolon`);
64
+ return { adapter: pythonString(defaultIfAbsent(source, "adapter", "synthetic")), mode: choice(defaultIfAbsent(source, "mode", "direct_column"), ["direct_column", "sql", "derived"], `${label} 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", {}), `${label} 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", {}), `${label} filters`), sql: optionalString(source.sql, "source sql"), params: object(defaultIfAbsent(source, "params", {}), `${label} params`) };
65
+ }
66
+
67
+ function normalizeParameter(payload) {
68
+ const source = normalizeSource(payload.source, "parameter source");
69
+ const online_source = payload.online_source == null ? null : normalizeSource(payload.online_source, "parameter online_source");
70
+ const time = object(defaultIfAbsent(payload, "time_semantics", {}), "parameter time_semantics");
71
+ 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, online_source, 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")) };
72
+ }
73
+
74
+ function normalizeOperator(payload) { object(payload, "operator"); const artifact = payload.code_artifact == null ? null : normalizeCodeArtifact(payload.code_artifact); const normalized = { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.operator/v1")), project: identifier(defaultIfAbsent(payload, "project", "default"), "operator project"), name: identifier(payload.name, "operator name"), version: identifier(payload.version, "operator version"), type: choice(defaultIfAbsent(payload, "type", "standard"), ["standard", "custom_python", "spark_udf", "feature"], "operator type"), function_hash: pythonString(defaultIfAbsent(payload, "function_hash", "demo")), entrypoint: optionalString(payload.entrypoint, "operator entrypoint"), code_hash: optionalString(payload.code_hash, "operator code_hash"), package_uri: optionalString(payload.package_uri, "operator package_uri"), code_artifact: artifact, input_schema: object(defaultIfAbsent(payload, "input_schema", {}), "operator input_schema"), output_schema: object(defaultIfAbsent(payload, "output_schema", {}), "operator output_schema"), config_schema: object(defaultIfAbsent(payload, "config_schema", {}), "operator config_schema"), runtime: object(defaultIfAbsent(payload, "runtime", {}), "operator runtime"), resources: object(defaultIfAbsent(payload, "resources", {}), "operator resources"), deterministic: bool(defaultIfAbsent(payload, "deterministic", true), "operator deterministic"), supports_batch: bool(defaultIfAbsent(payload, "supports_batch", true), "operator supports_batch"), supports_online: bool(defaultIfAbsent(payload, "supports_online", false), "operator supports_online"), owner: pythonString(defaultIfAbsent(payload, "owner", "demo")) }; if (artifact && normalized.code_hash !== artifact.sha256) throw new Error("operator code_hash must equal code_artifact.sha256"); if (artifact && normalized.package_uri !== artifact.uri) throw new Error("operator package_uri must equal code_artifact.uri"); return normalized; }
75
+ function normalizeRegistryOperator(payload) { const value = object(payload, "operator"); if (!hasOwn(value, "deleted_at")) return normalizeOperator(value); const { deleted_at, ...contract } = value; void deleted_at; return normalizeOperator(contract); }
76
+ function normalizeFeature(payload) { exactKeys(payload, ["schema_version", "project", "name", "version", "inputs", "operator", "operator_version", "config", "output_column", "output_dtype", "offline_online_supported", "owner", "description"], ["name", "version", "inputs", "operator", "operator_version", "output_column"], "feature"); const project = identifier(defaultIfAbsent(payload, "project", "default"), "feature project"); const inputs = objectList(payload.inputs, "feature inputs").map((item) => parameterRef(item, project)); if (!inputs.length) throw new Error("feature inputs must not be empty"); unique(inputs.map(parameterReferenceKey), "feature inputs"); if (typeof payload.output_column !== "string" || !outputColumnPattern.test(payload.output_column)) throw new Error("feature output_column is invalid"); return { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.feature/v1")), project, name: identifier(payload.name, "feature name"), version: identifier(payload.version, "feature version"), inputs, operator: identifier(payload.operator, "feature operator"), operator_version: identifier(payload.operator_version, "feature operator_version"), config: object(defaultIfAbsent(payload, "config", {}), "feature config"), output_column: payload.output_column, output_dtype: pythonString(defaultIfAbsent(payload, "output_dtype", "float64")), offline_online_supported: bool(defaultIfAbsent(payload, "offline_online_supported", true), "offline_online_supported"), owner: pythonString(defaultIfAbsent(payload, "owner", "demo")), description: pythonString(defaultIfAbsent(payload, "description", "")) }; }
77
+ function normalizeFeatureSet(payload) { exactKeys(payload, ["schema_version", "project", "name", "version", "features", "owner", "description"], ["name", "version", "features"], "feature set"); const features = objectList(payload.features, "feature set features").map((item) => ref(item, "feature reference")); if (!features.length) throw new Error("feature_set features must not be empty"); unique(features.map(referenceKey), "feature_set features"); return { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.feature_set/v1")), project: identifier(defaultIfAbsent(payload, "project", "default"), "feature set project"), name: identifier(payload.name, "feature set name"), version: identifier(payload.version, "feature set version"), features, owner: pythonString(defaultIfAbsent(payload, "owner", "demo")), description: pythonString(defaultIfAbsent(payload, "description", "")) }; }
78
+ function timestamp(value, label) {
79
+ requiredString(value, label);
80
+ const match = timestampPattern.exec(value);
81
+ if (!match) throw new Error(`${label} must be an ISO-8601 timestamp`);
82
+
83
+ const [, yearText, monthText, dayText, hourText, minuteText, secondText, , , offsetHourText, offsetMinuteText] = match;
84
+ const year = Number(yearText);
85
+ const month = Number(monthText);
86
+ const day = Number(dayText);
87
+ const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
88
+ const daysInMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
89
+ const invalidDate = year < 1 || month < 1 || month > 12 || day < 1 || day > (daysInMonth[month - 1] ?? 0);
90
+ const invalidTime = hourText !== undefined && (
91
+ Number(hourText) > 23 || Number(minuteText) > 59 || (secondText !== undefined && Number(secondText) > 59)
92
+ );
93
+ const invalidOffset = offsetHourText !== undefined
94
+ && (Number(offsetHourText) > 23 || Number(offsetMinuteText) > 59);
95
+ if (invalidDate || invalidTime || invalidOffset || Number.isNaN(Date.parse(value))) {
96
+ throw new Error(`${label} must be an ISO-8601 timestamp`);
97
+ }
98
+ return value;
99
+ }
100
+ function validateReferenceProject(value, parentProject, label) { if (value.project == null) return; const project = identifier(value.project, `${label} project`); if (project !== parentProject) throw new Error(`${label} cross-project reference is not allowed`); }
101
+ function normalizeParameterRequest(value, parentProject) { object(value, "dataset parameter request"); validateReferenceProject(value, parentProject, "dataset parameter request"); let alignment = null; if (value.alignment != null) { object(value.alignment, "parameter alignment"); alignment = { grid: pythonString(defaultIfAbsent(value.alignment, "grid", "10min")), method: choice(defaultIfAbsent(value.alignment, "method", "last_before_or_at"), ["last_before_or_at", "nearest", "none"], "parameter alignment method") }; } return { name: identifier(value.name, "dataset parameter name"), version: identifier(value.version, "dataset parameter version"), project: parentProject, alias: optionalString(value.alias, "dataset parameter alias"), alignment, required: bool(defaultIfAbsent(value, "required", true), "dataset parameter required"), missing_policy: choice(defaultIfAbsent(value, "missing_policy", "report_only"), ["report_only", "fail_on_requested_range_gap", "drop_target_and_lookback"], "dataset parameter missing_policy") }; }
102
+ function normalizeMissingPolicy(value, parentProject) { exactKeys(value, ["parameter", "version", "project", "policy"], ["parameter", "version"], "dataset parameter missing policy"); validateReferenceProject(value, parentProject, "dataset parameter missing policy"); return { parameter: identifier(value.parameter, "dataset parameter missing policy parameter"), version: identifier(value.version, "dataset parameter missing policy version"), project: parentProject, policy: choice(defaultIfAbsent(value, "policy", "report_only"), ["report_only", "fail_on_requested_range_gap", "drop_target_and_lookback"], "dataset parameter missing policy policy") }; }
103
+ function normalizePreprocess(value) { object(value, "dataset preprocess"); return { name: identifier(value.name, "preprocess name"), version: identifier(value.version, "preprocess version"), function_hash: pythonString(defaultIfAbsent(value, "function_hash", "demo")), config: object(defaultIfAbsent(value, "config", {}), "preprocess config") }; }
104
+ function normalizeTarget(value, parentProject) { object(value, "dataset target"); const result = structuredClone(value); if (result.parameter && typeof result.parameter === "object" && !Array.isArray(result.parameter)) result.parameter = parameterRef(result.parameter, parentProject); return result; }
105
+ function distinct(left, right, label) { if (left === right) throw new Error(`${label} inputs must be different`); }
106
+ function normalizeRelationshipCheck(value, label) { object(value, label); const type = choice(value.type, ["compare", "absolute_difference", "ratio_range", "sum_equals", "co_presence"], `${label} type`); if (type === "co_presence") { exactKeys(value, ["type"], ["type"], label); return { type }; } if (type === "compare") { exactKeys(value, ["type", "left", "operator", "right", "tolerance"], ["type", "left", "operator", "right"], label); const left = identifier(value.left, `${label} left`); const right = identifier(value.right, `${label} right`); distinct(left, right, label); return { type, left, operator: choice(value.operator, ["lt", "lte", "gt", "gte", "eq", "ne"], `${label} operator`), right, tolerance: nonNegativeNumber(defaultIfAbsent(value, "tolerance", 0), `${label} tolerance`) }; } if (type === "absolute_difference") { exactKeys(value, ["type", "left", "right", "max_difference"], ["type", "left", "right", "max_difference"], label); const left = identifier(value.left, `${label} left`); const right = identifier(value.right, `${label} right`); distinct(left, right, label); return { type, left, right, max_difference: nonNegativeNumber(value.max_difference, `${label} max_difference`) }; } if (type === "ratio_range") { exactKeys(value, ["type", "numerator", "denominator", "min", "max", "inclusive_min", "inclusive_max"], ["type", "numerator", "denominator"], label); const numerator = identifier(value.numerator, `${label} numerator`); const denominator = identifier(value.denominator, `${label} denominator`); distinct(numerator, denominator, label); const range = validRange([defaultIfAbsent(value, "min", null), defaultIfAbsent(value, "max", null)]); return { type, numerator, denominator, min: range[0], max: range[1], inclusive_min: bool(defaultIfAbsent(value, "inclusive_min", true), `${label} inclusive_min`), inclusive_max: bool(defaultIfAbsent(value, "inclusive_max", true), `${label} inclusive_max`) }; } exactKeys(value, ["type", "terms", "target", "tolerance"], ["type", "terms", "target"], label); if (!Array.isArray(value.terms) || value.terms.some((item) => typeof item !== "string" || !item)) throw new Error(`${label} terms must be a list of non-empty strings`); const terms = value.terms.map((item) => identifier(item, `${label} term`)); if (!terms.length) throw new Error(`${label} terms must not be empty`); unique(terms, `${label} terms`); const target = identifier(value.target, `${label} target`); if (terms.includes(target)) throw new Error(`${label} target must not also be a term`); return { type, terms, target, tolerance: nonNegativeNumber(defaultIfAbsent(value, "tolerance", 0), `${label} tolerance`) }; }
107
+ function relationshipAliases(check) { if (["compare", "absolute_difference"].includes(check.type)) return [check.left, check.right]; if (check.type === "ratio_range") return [check.numerator, check.denominator]; if (check.type === "sum_equals") return [...check.terms, check.target]; return []; }
108
+ function normalizeRelationshipRule(value, relationshipId) { const label = `parameter relationship ${relationshipId} rule`; exactKeys(value, ["id", "stage", "check", "acceptance", "enforcement", "description"], ["id", "check"], label); return { id: identifier(value.id, `${label} id`), stage: choice(defaultIfAbsent(value, "stage", "normalized_source"), ["normalized_source", "post_preprocess", "aligned_grid"], `${label} stage`), check: normalizeRelationshipCheck(value.check, `${label} check`), acceptance: normalizeAcceptance(object(defaultIfAbsent(value, "acceptance", {}), `${label} acceptance`), label), enforcement: choice(defaultIfAbsent(value, "enforcement", "warn"), ["observe", "warn", "fail"], `${label} enforcement`), description: pythonString(defaultIfAbsent(value, "description", "")) }; }
109
+ function normalizeParameterRelationship(value, parentProject) { exactKeys(value, ["id", "inputs", "alignment", "rules", "description"], ["id", "inputs", "rules"], "parameter relationship"); const id = identifier(value.id, "parameter relationship id"); const inputs = objectList(value.inputs, "parameter relationship inputs").map((item) => { exactKeys(item, ["alias", "parameter", "version", "project"], ["alias", "parameter", "version"], "parameter relationship input"); validateReferenceProject(item, parentProject, "parameter relationship input"); return { alias: identifier(item.alias, "parameter relationship input alias"), parameter: identifier(item.parameter, "parameter relationship input parameter"), version: identifier(item.version, "parameter relationship input version"), project: parentProject }; }); if (inputs.length < 2) throw new Error("parameter relationship requires at least two inputs"); unique(inputs.map((item) => item.alias), "parameter relationship input aliases"); unique(inputs.map(parameterReferenceKey), "parameter relationship inputs"); const alignmentValue = object(defaultIfAbsent(value, "alignment", {}), "parameter relationship alignment"); exactKeys(alignmentValue, ["method", "tolerance"], [], "parameter relationship alignment"); const tolerance = optionalDuration(alignmentValue.tolerance, "parameter relationship alignment tolerance"); if (tolerance != null && durationSeconds(tolerance) === 0) throw new Error("parameter relationship alignment tolerance must be positive"); const alignment = { method: choice(defaultIfAbsent(alignmentValue, "method", "last_before_or_at"), ["exact", "last_before_or_at"], "parameter relationship alignment method"), tolerance }; const rules = objectList(value.rules, "parameter relationship rules").map((item) => normalizeRelationshipRule(item, id)); if (!rules.length) throw new Error("parameter relationship rules must not be empty"); unique(rules.map((item) => item.id), `parameter relationship ${id} rule ids`); const aliases = new Set(inputs.map((item) => item.alias)); for (const rule of rules) { if (rule.stage !== "aligned_grid") throw new Error(`parameter relationship ${id} rule ${rule.id} must use stage=aligned_grid`); const missing = relationshipAliases(rule.check).filter((item) => !aliases.has(item)); if (missing.length) throw new Error(`parameter relationship ${id} rule ${rule.id} references undeclared aliases: ${missing.join(", ")}`); } return { id, inputs, alignment, rules, description: pythonString(defaultIfAbsent(value, "description", "")) }; }
110
+ function normalizeDataset(payload) {
111
+ object(payload, "dataset"); payload = Object.fromEntries(Object.entries(payload).filter(([name]) => name !== "deleted_at"));
112
+ exactKeys(payload, ["schema_version", "project", "dataset_id", "dataset_version", "mode", "read_policy", "snapshot_id", "as_of", "time_range", "parameters", "feature_set", "preprocess", "output", "source_read", "realtime_fetch", "abnormal_windows", "prediction", "target", "label_materializations", "parameter_relationships", "parameter_missing_policies", "rowset", "rowset_splits", "endpoint_policy"], ["dataset_id", "time_range", "feature_set"], "dataset");
113
+ const parentProject = identifier(defaultIfAbsent(payload, "project", "default"), "dataset project");
114
+ const readPolicy = choice(defaultIfAbsent(payload, "read_policy", "snapshot"), ["snapshot", "as_of", "latest"], "dataset read_policy");
115
+ const snapshotId = payload.snapshot_id == null ? null : identifier(payload.snapshot_id, "dataset snapshot_id");
116
+ const asOf = optionalString(payload.as_of, "dataset as_of");
117
+ if (readPolicy === "as_of" && !asOf) throw new Error("read_policy=as_of requires as_of");
118
+ if (readPolicy !== "as_of" && asOf != null) throw new Error("as_of is only valid when read_policy=as_of");
119
+ if (readPolicy !== "snapshot" && snapshotId != null) throw new Error("snapshot_id is only valid when read_policy=snapshot");
120
+ if (asOf) timestamp(asOf, "dataset as_of");
121
+ const time = object(payload.time_range, "dataset time_range"); exactKeys(time, ["start", "end", "grid"], ["start", "end"], "dataset time_range");
122
+ const parameters = objectList(defaultIfAbsent(payload, "parameters", []), "dataset parameters").map((item) => normalizeParameterRequest(item, parentProject));
123
+ const missingPolicies = objectList(defaultIfAbsent(payload, "parameter_missing_policies", []), "dataset parameter_missing_policies").map((item) => normalizeMissingPolicy(item, parentProject));
124
+ unique(missingPolicies.map(parameterReferenceKey), "dataset parameter_missing_policies Parameter references");
125
+ const overlap = parameters.map(referenceKey).filter((item) => new Set(missingPolicies.map(parameterReferenceKey)).has(item)); if (overlap.length) throw new Error("dataset parameter_missing_policies is only for Feature-only Parameters");
126
+ if (payload.endpoint_policy != null) { const drops = [...parameters.filter((item) => item.missing_policy === "drop_target_and_lookback"), ...missingPolicies.filter((item) => item.policy === "drop_target_and_lookback")]; if (drops.length) throw new Error("dataset endpoint_policy is incompatible with drop_target_and_lookback"); }
127
+ const output = { schema_version: pythonString(defaultIfAbsent(payload, "schema_version", "ml_data_platform.dataset_manifest/v1")), project: parentProject, dataset_id: identifier(payload.dataset_id, "dataset id"), dataset_version: identifier(defaultIfAbsent(payload, "dataset_version", "v1"), "dataset version"), mode: choice(defaultIfAbsent(payload, "mode", "training"), ["training", "inference"], "dataset mode"), read_policy: readPolicy, snapshot_id: snapshotId, as_of: asOf, time_range: { start: timestamp(time.start, "dataset time_range start"), end: timestamp(time.end, "dataset time_range end"), grid: pythonString(defaultIfAbsent(time, "grid", "10min")) }, parameters, feature_set: ref(object(payload.feature_set, "dataset feature set"), "dataset feature set"), preprocess: objectList(defaultIfAbsent(payload, "preprocess", []), "dataset preprocess").map(normalizePreprocess), output: object(defaultIfAbsent(payload, "output", {}), "dataset output") };
128
+ if (missingPolicies.length) output.parameter_missing_policies = missingPolicies;
129
+ for (const name of ["source_read", "realtime_fetch", "abnormal_windows", "prediction", "rowset", "rowset_splits", "endpoint_policy"]) if (payload[name] != null) output[name] = payload[name];
130
+ if (payload.target != null) output.target = normalizeTarget(payload.target, parentProject);
131
+ if (payload.label_materializations != null) output.label_materializations = payload.label_materializations;
132
+ const relationships = objectList(defaultIfAbsent(payload, "parameter_relationships", []), "dataset parameter_relationships").map((item) => normalizeParameterRelationship(item, parentProject)); unique(relationships.map((item) => item.id), "dataset parameter_relationship ids"); if (relationships.length) output.parameter_relationships = relationships;
133
+ return output;
134
+ }
135
+
136
+ function normalizeRegistryDataset(payload) {
137
+ const value = object(payload, "registered dataset");
138
+ const responseOnly = ["entity_keys", "label_materializations"];
139
+ const contract = Object.fromEntries(Object.entries(value).filter(([name]) => !responseOnly.includes(name)));
140
+ const normalized = normalizeDataset(contract);
141
+ for (const name of responseOnly) {
142
+ if (!hasOwn(value, name)) continue;
143
+ if (!Array.isArray(value[name])) throw new Error(`registered dataset ${name} must be a list`);
144
+ normalized[name] = structuredClone(value[name]);
145
+ }
146
+ return normalized;
147
+ }
148
+
149
+ function assetPath(root, relative, suffix) { if (typeof relative !== "string" || !relative) throw new Error("catalog asset path must be a non-empty string"); if (path.isAbsolute(relative)) throw new Error(`catalog asset path must be relative: ${relative}`); const target = path.resolve(root, relative); if (path.extname(target).toLowerCase() !== suffix) throw new Error(`catalog asset must use ${suffix}: ${relative}`); if (!target.startsWith(`${root}${path.sep}`)) throw new Error(`catalog asset path escapes catalog directory: ${relative}`); if (!fs.existsSync(target) || !fs.statSync(target).isFile()) throw new Error(`catalog asset not found: ${target}`); return target; }
150
+ function readJson(file) { try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch (error) { throw new Error(`cannot read JSON asset ${file}: ${error.message}`); } }
151
+ function readObject(file) { return object(readJson(file), `JSON file ${file}`); }
152
+ function loadCollection(file, normalizer, collectionSchema, field, label) { const payload = readJson(file); if (Array.isArray(payload)) return payload.map((item) => normalizer(object(item, label))); if (payload && payload.schema_version === collectionSchema) return objectList(payload[field], `${label} collection`).map(normalizer); return [normalizer(object(payload, label))]; }
153
+ function validateUniqueCatalogKeys(catalog) { const groups = { parameter: catalog.parameters.map(key), operator: catalog.operators.map((item) => key(item.spec)), feature: catalog.features.map(key), "feature set": catalog.feature_sets.map(key), dataset: catalog.datasets.map(datasetKey) }; for (const [label, values] of Object.entries(groups)) { const duplicates = [...new Set(values.filter((item, index) => values.indexOf(item) !== index))].sort(); if (duplicates.length) throw new Error(`catalog has duplicate ${label} keys: ${duplicates.join(", ")}`); } }
154
+ function loadCatalog(directory) {
155
+ const root = path.resolve(directory); if (!fs.statSync(root).isDirectory()) throw new Error(`catalog path must be a directory: ${root}`); const catalogPath = path.join(root, "catalog.json"); const manifest = readObject(catalogPath); if (manifest.schema_version !== "ml_data_platform.catalog/v1") throw new Error(`unsupported catalog schema_version: ${manifest.schema_version}`);
156
+ exactKeys(manifest, ["schema_version", "name", "version", "parameters", "operators", "features", "feature_sets", "datasets"], ["schema_version", "name"], "catalog");
157
+ const stringList = (value, label) => { if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item)) throw new Error(`${label} must be a list of non-empty strings`); return value; };
158
+ const parameters = stringList(manifest.parameters ?? [], "catalog parameters").flatMap((relative) => loadCollection(assetPath(root, relative, ".json"), normalizeParameter, "ml_data_platform.parameter_collection/v1", "parameters", "parameter"));
159
+ const features = stringList(manifest.features ?? [], "catalog features").map((relative) => normalizeFeature(readObject(assetPath(root, relative, ".json"))));
160
+ const feature_sets = stringList(manifest.feature_sets ?? [], "catalog feature_sets").flatMap((relative) => loadCollection(assetPath(root, relative, ".json"), normalizeFeatureSet, "ml_data_platform.feature_set_collection/v1", "feature_sets", "feature set"));
161
+ const datasets = stringList(manifest.datasets ?? [], "catalog datasets").map((relative) => normalizeDataset(readObject(assetPath(root, relative, ".json"))));
162
+ if (!Array.isArray(manifest.operators ?? [])) throw new Error("catalog operators must be a list");
163
+ const operators = (manifest.operators ?? []).map((entry) => { exactKeys(entry, ["spec", "package"], ["spec"], "catalog operator entry"); return { spec: normalizeOperator(readObject(assetPath(root, requiredString(entry.spec, "catalog operator spec"), ".json"))), package_path: entry.package == null ? null : assetPath(root, requiredString(entry.package, "catalog operator package"), ".whl") }; });
164
+ const declaredPaths = [...(manifest.parameters ?? []), ...(manifest.operators ?? []).map((item) => item.spec), ...(manifest.features ?? []), ...(manifest.feature_sets ?? []), ...(manifest.datasets ?? [])]; unique(declaredPaths, "catalog asset paths");
165
+ const catalog = { root, path: catalogPath, manifest: { schema_version: "ml_data_platform.catalog/v1", name: identifier(manifest.name, "catalog name"), version: identifier(manifest.version ?? "v1", "catalog version"), parameters: manifest.parameters ?? [], operators: (manifest.operators ?? []).map((item) => ({ spec: item.spec, package: item.package ?? null })), features: manifest.features ?? [], feature_sets: manifest.feature_sets ?? [], datasets: manifest.datasets ?? [] }, parameters, operators, features, feature_sets, datasets };
166
+ validateUniqueCatalogKeys(catalog); return catalog;
167
+ }
168
+
169
+ async function fetchAll(client, endpoint, params = {}) { const rows = []; for (let offset = 0; ; offset += 500) { const response = await client.get(endpoint, { ...params, limit: 500, offset }); if (Array.isArray(response)) return response; const page = response.items; if (!Array.isArray(page)) throw new Error(`${endpoint} returned an invalid list response`); rows.push(...page); if (response.total == null || rows.length >= response.total || page.length < 500) return rows; } }
170
+ async function snapshot(client, targetProject) { const scope = { project: targetProject ?? "default" }; const [parameters, operators, features, featureSets, datasets] = await Promise.all([fetchAll(client, "/parameters", scope), fetchAll(client, "/operators", scope), fetchAll(client, "/features", scope), fetchAll(client, "/feature-sets", scope), fetchAll(client, "/datasets", { ...scope, include_deleted: true })]); return { parameters: new Map(parameters.map((x) => [key(x), x])), operators: new Map(operators.map((x) => [key(normalizeRegistryOperator(x)), normalizeRegistryOperator(x)])), features: new Map(features.map((x) => [key(normalizeFeature(x)), normalizeFeature(x)])), feature_sets: new Map(featureSets.map((x) => [key(normalizeFeatureSet(x)), normalizeFeatureSet(x)])), datasets: new Map(datasets.filter((x) => !x.deleted_at).map((x) => [datasetKey(normalizeRegistryDataset(x)), normalizeRegistryDataset(x)])) }; }
171
+
172
+ function requiresPackage(spec) { return ["custom_python", "feature", "spark_udf"].includes(spec.type) && spec.code_artifact == null && !String(spec.package_uri ?? "").startsWith("pythonpath://"); }
173
+ function validateCatalog(catalog, current) {
174
+ 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])]);
175
+ 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`);
176
+ 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}`); }
177
+ 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`); }
178
+ for (const dataset of catalog.datasets) {
179
+ const datasetId = datasetKey(dataset); const declared = new Set();
180
+ for (const item of dataset.parameters) { const itemKey = referenceKey(item); declared.add(itemKey); if (!parameters.has(itemKey)) throw new Error(`catalog dataset ${datasetId} references missing parameter ${itemKey}`); }
181
+ const featureSet = featureSets.get(referenceKey(dataset.feature_set)); if (!featureSet) throw new Error(`catalog dataset ${datasetId} references missing feature set`);
182
+ const featureInputs = new Set(); for (const featureRef of featureSet.features) { const feature = features.get(referenceKey(featureRef)); if (feature) for (const input of feature.inputs) featureInputs.add(parameterReferenceKey(input)); }
183
+ for (const policy of dataset.parameter_missing_policies ?? []) { const itemKey = parameterReferenceKey(policy); if (!parameters.has(itemKey)) throw new Error(`catalog dataset ${datasetId} missing-policy references missing parameter ${itemKey}`); if (declared.has(itemKey)) throw new Error(`catalog dataset ${datasetId} missing-policy also declares dataset parameter ${itemKey}`); if (!featureInputs.has(itemKey)) throw new Error(`catalog dataset ${datasetId} missing-policy may reference only Feature input Parameters: ${itemKey}`); }
184
+ for (const relationship of dataset.parameter_relationships ?? []) for (const input of relationship.inputs) { const itemKey = parameterReferenceKey(input); if (!parameters.has(itemKey)) throw new Error(`catalog dataset ${datasetId} relationship ${relationship.id} references missing parameter ${itemKey}`); }
185
+ for (const item of dataset.preprocess) if (!operators.has(referenceKey(item))) throw new Error(`catalog dataset ${datasetId} references missing preprocess operator ${referenceKey(item)}`);
186
+ }
187
+ }
188
+ function validateCatalogProject(catalog, targetProject) {
189
+ const projects = [...catalog.parameters, ...catalog.operators.map((item) => item.spec), ...catalog.features, ...catalog.feature_sets, ...catalog.datasets].map((item) => item.project ?? "default");
190
+ const uniqueProjects = [...new Set(projects)];
191
+ if (uniqueProjects.length > 1) throw new Error(`catalog contains multiple projects: ${uniqueProjects.sort().join(", ")}`);
192
+ const expected = targetProject ?? "default";
193
+ if (uniqueProjects.length && uniqueProjects[0] !== expected) throw new Error(`catalog project ${uniqueProjects[0]} does not match target project ${expected}`);
194
+ }
195
+ function canonical(value) { if (Array.isArray(value)) return value.map(canonical); if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((name) => [name, canonical(value[name])])); return value; }
196
+ function equal(left, right) { return JSON.stringify(canonical(left)) === JSON.stringify(canonical(right)); }
197
+ function assertImmutable(label, assetKey, candidate, existing) { if (existing && !equal(candidate, existing)) { const error = new Error(`${label} version is immutable: ${assetKey}`); error.code = "EEXIST"; throw error; } }
198
+ function sha256File(file) { return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex")}`; }
199
+
200
+ async function applyCatalog(catalog, client, dryRun, targetProject) {
201
+ validateCatalogProject(catalog, targetProject); const current = await snapshot(client, targetProject); validateCatalog(catalog, current); const groups = { parameters: catalog.parameters.length, operators: catalog.operators.length, features: catalog.features.length, feature_sets: catalog.feature_sets.length, datasets: catalog.datasets.length }; const summary = Object.fromEntries(Object.entries(groups).map(([name, count]) => [name, { declared: count, planned: 0, applied: 0, unchanged: 0 }]));
202
+ for (const asset of catalog.parameters) { const assetKey = key(asset); if (current.parameters.has(assetKey)) current.parameters.set(assetKey, normalizeParameter(await client.get(`/parameters/${encodeURIComponent(asset.project)}/${encodeURIComponent(asset.name)}/${encodeURIComponent(asset.version)}`))); }
203
+ for (const asset of catalog.parameters) assertImmutable("parameter", key(asset), asset, current.parameters.get(key(asset)));
204
+ for (const item of catalog.operators) { const existing = current.operators.get(key(item.spec)); if (!existing) continue; let candidate = item.spec; if (item.package_path) { if (existing.code_hash !== sha256File(item.package_path) || !existing.code_artifact) throw new Error(`operator version is immutable: ${key(item.spec)}; publish a new version`); candidate = normalizeOperator({ ...item.spec, code_hash: existing.code_artifact.sha256, package_uri: existing.code_artifact.uri, code_artifact: existing.code_artifact }); } assertImmutable("operator", key(item.spec), candidate, existing); }
205
+ const plain = async (assets, existing, label, endpoint, keyFn = key, params) => { for (const asset of assets) { const assetKey = keyFn(asset); assertImmutable(label, assetKey, asset, existing.get(assetKey)); if (existing.has(assetKey)) summary[label].unchanged += 1; else if (dryRun) summary[label].planned += 1; else { await client.post(endpoint, asset, params); summary[label].applied += 1; } } };
206
+ try {
207
+ await plain(catalog.parameters, current.parameters, "parameters", "/parameters");
208
+ for (const item of catalog.operators) { const assetKey = key(item.spec); if (current.operators.has(assetKey)) summary.operators.unchanged += 1; else if (dryRun) summary.operators.planned += 1; else { let candidate = item.spec; if (item.package_path) { const uploaded = await client.uploadOperatorPackage(item.spec.project, item.spec.name, item.spec.version, item.package_path); candidate = normalizeOperator({ ...item.spec, code_hash: uploaded.code_artifact.sha256, package_uri: uploaded.code_artifact.uri, code_artifact: uploaded.code_artifact }); } await client.post("/operators", candidate); summary.operators.applied += 1; } }
209
+ await plain(catalog.features, current.features, "features", "/features"); await plain(catalog.feature_sets, current.feature_sets, "feature_sets", "/feature-sets"); await plain(catalog.datasets, current.datasets, "datasets", "/datasets", datasetKey, { force: false });
210
+ } catch (error) { throw new Error(`catalog apply failed after progress=${JSON.stringify(canonical(summary))}: ${error.message}`); }
211
+ return { status: dryRun ? "validated" : "applied", profile: "server", catalog: { name: catalog.manifest.name, version: catalog.manifest.version, path: catalog.path }, summary };
212
+ }
213
+
214
+ module.exports = { applyCatalog, fetchAll, loadCatalog, normalizeDataset, normalizeFeature, normalizeFeatureSet, normalizeOperator, normalizeRegistryDataset, normalizeRegistryOperator, normalizeParameter, validateCatalog, validateCatalogProject };