@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,766 @@
1
+ # Current Contract Reference
2
+
3
+ Use this reference for the implemented V1 schemas. Prefer the running CLI and `src/data_platform_demo/schemas.py` when they disagree with historical design documents.
4
+
5
+ ## Contents
6
+
7
+ 1. Naming and versioning
8
+ 2. Projects and namespacing
9
+ 3. Parameter
10
+ 4. Operator
11
+ 5. Feature
12
+ 6. FeatureSet
13
+ 7. DatasetManifest
14
+ 8. Catalog
15
+ 9. Cross-object validation
16
+
17
+ ## Naming And Versioning
18
+
19
+ - Use identifiers matching `^[A-Za-z0-9][A-Za-z0-9_-]*$` and at most 128 characters.
20
+ - Prefer lowercase snake_case business identifiers.
21
+ - Use explicit immutable versions such as `v1`, `v2`.
22
+ - Create a new version when any semantic field changes.
23
+ - Keep credentials in the configured profile or server Secret, never in an asset.
24
+
25
+ ## Projects And Namespacing
26
+
27
+ - Every registry asset is namespaced by a `project`. Its true registry key is
28
+ `project/name:version`, so `name:version` alone is unique only within one
29
+ project; the same `name:version` may exist independently under other projects.
30
+ - The platform ships a built-in `default` project. When no project is given,
31
+ assets are created and read under `default`.
32
+ - Conceptually a project is a namespace, not an attribute of the asset. In the
33
+ current implementation, however, the project a Parameter/Operator/Feature/
34
+ FeatureSet/DatasetManifest is **created** under comes from the optional
35
+ `project` field in its own JSON spec (it defaults to `default` when omitted).
36
+ So to place an asset in a non-default project, set `"project": "<name>"` in the
37
+ spec before `apply`.
38
+ - For a confirmed non-default target, include that field in every draft
39
+ Parameter, Operator, Feature, Feature input, FeatureSet, and DatasetManifest;
40
+ do not rely on Pydantic's `default` fallback during authoring.
41
+ - Reads and filters, by contrast, take the project out-of-band: `list-* --project`
42
+ and the leading project argument of the artifact commands. `apply --project`
43
+ scopes the comparison snapshot that publication is validated against.
44
+ - All references inside one catalog resolve within a single project. Cross-project
45
+ references (a Feature in project A depending on a Parameter in project B) are
46
+ rejected by the platform.
47
+ - Immutability is per project: publishing `name:v1` into project A never touches
48
+ `name:v1` in project B.
49
+
50
+ ## Parameter
51
+
52
+ Required shape:
53
+
54
+ ```json
55
+ {
56
+ "schema_version": "ml_data_platform.parameter/v1",
57
+ "name": "hot_blast_pressure",
58
+ "display_name": "Hot blast pressure",
59
+ "version": "v1",
60
+ "data_type": "time_series",
61
+ "unit": "kPa",
62
+ "expected_frequency": "5s",
63
+ "source": {},
64
+ "online_source": null,
65
+ "time_semantics": {
66
+ "event_time_field": "event_time",
67
+ "ingested_at_field": "ingested_at",
68
+ "timezone": "Asia/Shanghai",
69
+ "availability": {
70
+ "strategy": "source_field",
71
+ "field": "ingested_at",
72
+ "accuracy": "exact"
73
+ }
74
+ },
75
+ "availability_sla": {
76
+ "max_delay": "PT10M"
77
+ },
78
+ "value_field": "value",
79
+ "quality_rules": {
80
+ "valid_range": [0.0, 450.0],
81
+ "allow_missing": true,
82
+ "rules": [
83
+ {
84
+ "id": "finite_source_values",
85
+ "stage": "normalized_source",
86
+ "check": {"type": "finite"},
87
+ "acceptance": {"max_violation_rate": 0.0},
88
+ "enforcement": "fail"
89
+ }
90
+ ]
91
+ },
92
+ "owner": "business_owner"
93
+ }
94
+ ```
95
+
96
+ `online_source` 是可选的实时来源覆盖。离线 Dataset build、`snapshot`、`as_of` 和历史回放
97
+ 只读取 `source`;实时因果读取在声明时读取 `online_source`,否则回退到 `source`。两者必须
98
+ 表达同一个 Parameter,产生相同语义的标准长表(`timestamp, metric_name, value, unit`),
99
+ 并保持值、单位、事件时间、频率、质量和可用时间语义一致。它们可以使用不同的受支持
100
+ adapter;凭据不进入 JSON。Feature/Operator/FeatureSet 不需要复制一份。
101
+
102
+ 例如:
103
+
104
+ ```json
105
+ {
106
+ "source": {
107
+ "adapter": "postgresql_direct",
108
+ "schema": "process_data",
109
+ "table": "pressure_history",
110
+ "time_column": "event_time",
111
+ "value_column": "pressure",
112
+ "metric_name": "hot_blast_pressure"
113
+ },
114
+ "online_source": {
115
+ "adapter": "influxdb_direct",
116
+ "measurement": "blast_furnace_live",
117
+ "field": "pressure",
118
+ "metric_name": "hot_blast_pressure"
119
+ }
120
+ }
121
+ ```
122
+
123
+ 实时 lineage 会标出 `source_role=online_source` 并保留 `offline_source`。`online_source`
124
+ 参与 Parameter hash;因此变更它必须发布新的 Parameter 版本及受影响的下游依赖。使用在线
125
+ 覆盖时,响应中的 replay 会明确标记为不支持,因为离线 manifest 只会重新读取 `source`,
126
+ 不能伪造线上读法的回放。
127
+
128
+ Supported source modes are `direct_column`, `sql`, and `derived`. Prefer `direct_column` for ordinary business parameters.
129
+
130
+ For `sql`, `source.sql` must contain one statement without a trailing semicolon,
131
+ and named placeholders must be supplied through `source.params`; `start` and
132
+ `end` are reserved runtime window parameters. `source.parameters` is not a
133
+ supported field.
134
+
135
+ `availability.strategy` 支持 `source_field`、`fixed_delay` 和 `unsupported`。只有一个
136
+ 事件时间列时,可以使用经过数据 owner 确认的固定延迟上界,例如
137
+ `{"strategy":"fixed_delay","delay":"PT10M","accuracy":"upper_bound"}`。
138
+ 当前严格 `as_of` 只接受 `exact` 或有合同支撑的 `upper_bound`;修改这些语义需要发布
139
+ 新的 Parameter 业务版本。
140
+
141
+ An optional `rounding` section states the fixed-point precision the platform
142
+ enforces at the Parameter boundary:
143
+
144
+ ```json
145
+ "rounding": {"mode": "half_up", "decimals": 2}
146
+ ```
147
+
148
+ Fixing only the decimal count leaves the tie case to whichever engine evaluates
149
+ it: PostgreSQL `ROUND` on `numeric` rounds halves away from zero (5.625 becomes
150
+ 5.63) while Python and NumPy round halves to even (5.625 becomes 5.62). A single
151
+ 0.01 difference spreads through rolling windows into many derived Features, so
152
+ declare both fields. `mode` is `half_up` or `half_even`.
153
+
154
+ The platform applies the rule with deterministic decimal quantization after the
155
+ adapter produces the standard metric frame and before quality checks, source
156
+ cache publication/replay, and Operator execution. Prefer an unrounded source
157
+ expression. A source that already applies the same rule is accepted because the
158
+ second application is idempotent, but an upstream path that already discarded a
159
+ tie with a different rule cannot be repaired downstream. Do not repeat the same
160
+ Parameter-level rounding in an Operator. Changing the mode, decimals, or stage
161
+ requires a new Parameter version and the affected reverse-dependency closure.
162
+
163
+ PostgreSQL, MySQL, and SQL Server direct-column Parameters share this source
164
+ shape. Set `adapter` to `postgresql_direct`, `mysql_direct`, or
165
+ `sqlserver_direct`; keep database-specific DSNs in server-managed Secrets.
166
+
167
+ ```json
168
+ {
169
+ "adapter": "postgresql_direct",
170
+ "mode": "direct_column",
171
+ "schema": "process_data",
172
+ "table": "sensor_readings",
173
+ "time_column": "event_time",
174
+ "value_column": "hot_blast_pressure",
175
+ "metric_name": "hot_blast_pressure",
176
+ "unit_column": "unit",
177
+ "filters": {"furnace_id": "BF12"}
178
+ }
179
+ ```
180
+
181
+ The same contract for MySQL or SQL Server changes only the adapter name:
182
+
183
+ ```json
184
+ {"adapter": "mysql_direct", "mode": "direct_column", "schema": "process_data", "table": "sensor_readings", "time_column": "event_time", "value_column": "hot_blast_pressure", "metric_name": "hot_blast_pressure"}
185
+ ```
186
+
187
+ ```json
188
+ {"adapter": "sqlserver_direct", "mode": "direct_column", "schema": "process_data", "table": "sensor_readings", "time_column": "event_time", "value_column": "hot_blast_pressure", "metric_name": "hot_blast_pressure"}
189
+ ```
190
+
191
+ InfluxDB direct-column example:
192
+
193
+ ```json
194
+ {
195
+ "adapter": "influxdb_direct",
196
+ "mode": "direct_column",
197
+ "measurement": "blast_furnace",
198
+ "field": "hot_blast_pressure",
199
+ "metric_name": "hot_blast_pressure",
200
+ "tags": {"furnace_id": "BF12"}
201
+ }
202
+ ```
203
+
204
+ Do not put DSNs, access keys, tokens, passwords, or connection endpoints containing credentials in `source`.
205
+
206
+ `quality_rules.rules` defines immutable single-Parameter assertions. Supported
207
+ checks are `not_null`, `finite`, `range`, and `max_gap`. `stage` is one of
208
+ `normalized_source`, `post_preprocess`, or `aligned_grid`; execution follows
209
+ that order. `acceptance` controls evidence thresholds, while `enforcement`
210
+ chooses `observe`, `warn`, or `fail`. Keep cross-Parameter assertions out of a
211
+ Parameter version and declare them in DatasetManifest `parameter_relationships`.
212
+
213
+ `quality_rules.missing_detection` controls source-observation gap detection and
214
+ is independent from value validity. It defaults to `disabled`, which is the
215
+ safe behavior for irregular event Parameters such as tapping events or silicon
216
+ samples. Use `fixed_frequency` only when `expected_frequency` is a real fixed
217
+ duration. For irregular Parameters, use an explicit `max_gap` or an
218
+ `interval_multiplier` over a `mean` or `median` observed interval:
219
+
220
+ ```json
221
+ "missing_detection": {
222
+ "mode": "interval_multiplier",
223
+ "multiplier": 10,
224
+ "baseline": "median",
225
+ "min_observations": 2
226
+ }
227
+ ```
228
+
229
+ `max_gap` accepts positive short durations including second precision (for
230
+ example `30s`, `10min`, or `2h`). Detection runs on the normalized,
231
+ post-preprocess source frame before target-grid interpolation, and its source
232
+ windows are recorded in the build's `missing_windows.json` artifact.
233
+
234
+ ## Operator
235
+
236
+ Feature Operator draft shape:
237
+
238
+ ```json
239
+ {
240
+ "schema_version": "ml_data_platform.operator/v1",
241
+ "name": "pressure_features",
242
+ "version": "v1",
243
+ "type": "feature",
244
+ "function_hash": "pressure_features.formulas.v1",
245
+ "entrypoint": "pressure_features:compute_features",
246
+ "code_hash": null,
247
+ "package_uri": null,
248
+ "code_artifact": null,
249
+ "input_schema": {
250
+ "parameters": ["hot_blast_pressure:v1"]
251
+ },
252
+ "output_schema": {
253
+ "columns": ["event_time", "pressure_mean_5m"]
254
+ },
255
+ "config_schema": {
256
+ "properties": {
257
+ "window": {"type": "string"}
258
+ },
259
+ "required": ["window"]
260
+ },
261
+ "runtime": {"engine": "python_entrypoint", "network": "none"},
262
+ "resources": {"timeout_seconds": 60},
263
+ "deterministic": true,
264
+ "supports_batch": true,
265
+ "supports_online": false,
266
+ "owner": "business_owner"
267
+ }
268
+ ```
269
+
270
+ For a catalog wheel entry, `catalog.json` supplies the package file. Publication calculates and stores `code_hash`, `package_uri`, and `code_artifact`.
271
+
272
+ Treat `pythonpath://` as platform-owned example behavior. Publish formal business Operators as immutable pure-Python wheels.
273
+
274
+ ## Feature
275
+
276
+ ```json
277
+ {
278
+ "schema_version": "ml_data_platform.feature/v1",
279
+ "name": "pressure_mean_5m",
280
+ "version": "v1",
281
+ "inputs": [
282
+ {"parameter": "hot_blast_pressure", "version": "v1"}
283
+ ],
284
+ "operator": "pressure_features",
285
+ "operator_version": "v1",
286
+ "config": {"window": "5min"},
287
+ "output_column": "pressure_mean_5m",
288
+ "output_dtype": "float64",
289
+ "offline_online_supported": false,
290
+ "owner": "business_owner",
291
+ "description": "Mean pressure over the causal five-minute window."
292
+ }
293
+ ```
294
+
295
+ Rules:
296
+
297
+ - Define exactly one public output column per Feature.
298
+ - Keep `inputs` nonempty, unique, versioned, and formula-exact.
299
+ - Require the referenced Operator to have `type=feature`.
300
+ - Require `output_column` to appear in the Operator output schema.
301
+ - Use `name` as the final dataset column; use `output_column` as the Operator's physical result column.
302
+
303
+ ## FeatureSet
304
+
305
+ ```json
306
+ {
307
+ "schema_version": "ml_data_platform.feature_set/v1",
308
+ "name": "pressure_core",
309
+ "version": "v1",
310
+ "features": [
311
+ {"name": "pressure_mean_5m", "version": "v1"}
312
+ ],
313
+ "owner": "business_owner",
314
+ "description": "Ordered pressure model inputs."
315
+ }
316
+ ```
317
+
318
+ The list must be nonempty, unique, and ordered exactly as consumers expect.
319
+
320
+ ## DatasetManifest
321
+
322
+ ```json
323
+ {
324
+ "schema_version": "ml_data_platform.dataset_manifest/v1",
325
+ "dataset_id": "pressure_training_20260701_20260702",
326
+ "dataset_version": "v1",
327
+ "mode": "training",
328
+ "read_policy": "snapshot",
329
+ "time_range": {
330
+ "start": "2026-07-01T00:00:00+08:00",
331
+ "end": "2026-07-02T00:00:00+08:00",
332
+ "grid": "10min"
333
+ },
334
+ "prediction": {
335
+ "horizon": "10min"
336
+ },
337
+ "parameters": [],
338
+ "parameter_missing_policies": [
339
+ {
340
+ "parameter": "hot_blast_pressure",
341
+ "version": "v1",
342
+ "project": "default",
343
+ "policy": "report_only"
344
+ }
345
+ ],
346
+ "feature_set": {"name": "pressure_core", "version": "v1"},
347
+ "preprocess": [],
348
+ "output": {
349
+ "format": "parquet",
350
+ "include_stats": true,
351
+ "include_lineage": true,
352
+ "engine": "chronon"
353
+ }
354
+ }
355
+ ```
356
+
357
+ Current schema rules:
358
+
359
+ - Accept exactly one `feature_set`.
360
+ - Reject old top-level `features` and `recipe` fields.
361
+ - Use a half-open `[start, end)` target grid. `start` must be earlier than
362
+ `end`, the window must span at least one grid step, both bounds must agree on
363
+ timezone awareness, and `grid` must be one of `1min`, `5min`, `10min`,
364
+ `15min`, `20min`, `30min`, `1h`, `2h`, `1d`. Violations are rejected at
365
+ submission with a field-level error on `time_range`; no Job is created.
366
+ - Require `as_of` only with `read_policy=as_of`.
367
+ - Allow `snapshot_id` only with `read_policy=snapshot`.
368
+ - Use `parameters` only to request explicit Parameter output columns.
369
+ - Declare one dataset-wide `prediction` contract for forecast Features. `horizon`
370
+ accepts non-negative whole-minute durations such as `0min`, `10min`, or `2h`.
371
+ The platform always derives `cutoff_times = target_times - horizon` and records
372
+ that derivation in resolved lineage; it is not a user-configurable policy.
373
+
374
+ ### Optional sections
375
+
376
+ These ten sections may be omitted: `source_read`, `realtime_fetch`,
377
+ `abnormal_windows`, `target`, `prediction`, `parameter_relationships`,
378
+ `parameter_missing_policies`, `rowset`, `rowset_splits`, and `endpoint_policy`.
379
+ Omitting a section keeps older manifest hashes stable.
380
+ Sections containing only inert empty values are also treated as unset, but a
381
+ declared `realtime_fetch` policy contains meaningful defaults and changes the
382
+ manifest hash. Declaring `endpoint_policy` also changes the manifest hash.
383
+
384
+ Each explicit `parameters` request may set `missing_policy`. A
385
+ `parameter_missing_policies` entry is reserved for a Parameter used only through
386
+ the selected FeatureSet's input closure; it must not also appear in
387
+ `parameters`. Its exact V1 shape is `{"parameter": "<name>", "version":
388
+ "<version>", "project": "<project>", "policy": "<policy>"}`. Do not use
389
+ the `ParameterRequest` field names `name` or `missing_policy` in this list. For
390
+ a confirmed non-default target, set `project` explicitly instead of relying on
391
+ its `default` fallback. The policies are:
392
+
393
+ - `report_only`: retain all target rows and report source gaps.
394
+ - `fail_on_requested_range_gap`: fail when a detected gap affects the requested
395
+ target range.
396
+ - `drop_target_and_lookback`: remove the target rows in the detected window and
397
+ the following rows whose resolved Feature input lookback still reaches into
398
+ that window. The same merged row mask is applied to both
399
+ `feature_dataset.parquet` and `parameter_dataset.parquet`.
400
+
401
+ The default `report_only` value is omitted from canonical manifest identity so
402
+ manifests published before `missing_policy` existed keep their artifact hash.
403
+ When `endpoint_policy` is declared, `drop_target_and_lookback` is rejected:
404
+ endpoint eligibility requires the canonical datasets and
405
+ `rowset_membership.parquet` to retain the same complete candidate rowset. Use
406
+ `report_only` or `fail_on_requested_range_gap` for those manifests.
407
+
408
+ The resolved per-Parameter windows, policy, affected rows, and dropped rows are
409
+ written to `missing_windows.json` and summarized in `validation.json`. A
410
+ partitioned build with a drop policy must be materialized as one full source
411
+ window so the same global gap contract is used for every rowset.
412
+
413
+ `source_read` pages the source reads. A long target window read in one request
414
+ can exceed the driver read timeout; chunking keeps one manifest covering the
415
+ whole window instead of splitting it into per-day manifests. Chunk windows,
416
+ attempts, rows, and elapsed time land in
417
+ `latency_stats.fetch_timing_summary`.
418
+
419
+ ```json
420
+ "source_read": {
421
+ "chunk_days": 1,
422
+ "timeout_seconds": 60,
423
+ "max_retries": 3,
424
+ "retry_sleep_seconds": 2
425
+ }
426
+ ```
427
+
428
+ `realtime_fetch` declares the policy for one low-latency causal-cutoff read:
429
+
430
+ ```json
431
+ "realtime_fetch": {
432
+ "max_workers": 4,
433
+ "min_lookback": "1h",
434
+ "validate_freshness": true,
435
+ "max_source_lag_hours": 3.0,
436
+ "max_source_lag_hours_by_parameter": {},
437
+ "allow_missing": false,
438
+ "allow_tail_edge_missing": true,
439
+ "max_tail_edge_missing": 1,
440
+ "tail_edge_missing_max_age_minutes": 65.0,
441
+ "allow_internal_gap_fill": true,
442
+ "internal_gap_fill_max_age_hours": 5.0
443
+ }
444
+ ```
445
+
446
+ `POST /inference-data/fetch` serves one cutoff without running a batch build,
447
+ writing parquet, or publishing a DatasetArtifact. It derives
448
+ `target_time = cutoff_time + prediction.horizon`, filters every source to
449
+ `timestamp <= cutoff_time`, and derives each Parameter read window from the
450
+ selected Operators' `input_schema.history_requirements`. The response carries
451
+ `contract.manifest_hash`; it also carries a replayable `read_policy=as_of` manifest
452
+ unless a Parameter's `online_source` override is active, in which case
453
+ `replay.supported=false` is returned because an offline manifest would read the
454
+ offline `source` instead.
455
+
456
+ Keep the default strict: `allow_missing=false`. Tail-edge and bounded internal
457
+ gap-fill tolerance may return a row, but the response must mark
458
+ `freshness.degraded=true` and include validation evidence. A freshness or
459
+ missing-data condition that may recover returns HTTP `503`; an invalid contract
460
+ returns HTTP `400`. Declaring `realtime_fetch` changes the manifest hash; leaving
461
+ it unset preserves older contract hashes and uses environment fallbacks.
462
+
463
+ `abnormal_windows` excludes target windows whose data must not be trusted. The
464
+ policy and the lookback apply to the whole window set, not to individual
465
+ windows: a dataset is either a filtered rowset or a marked full rowset.
466
+
467
+ ```json
468
+ "abnormal_windows": {
469
+ "policy": "drop_target_and_lookback",
470
+ "windows": [
471
+ {"start": "2026-05-25T19:30:00+08:00",
472
+ "end": "2026-05-26T09:50:00+08:00",
473
+ "reason": "shutdown"}
474
+ ]
475
+ }
476
+ ```
477
+
478
+ | policy | Meaning |
479
+ |-|-|
480
+ | `drop_target_and_lookback` | Drop rows in a window, and rows after it whose feature input lookback reaches back in. Default. |
481
+ | `drop_target_only` | Drop only rows inside a window. |
482
+ | `mark_only` | Drop nothing; record what is affected in lineage and validation. |
483
+
484
+ How far past a window to drop is derived from the widest resolved Operator history
485
+ requirement. Cutoff-relative history contributes `prediction.horizon + lookback`;
486
+ target-time-relative history contributes its declared lookback directly. The
487
+ maximum is used for source fetch and abnormal-window propagation. Do not restate
488
+ that number unless you want it asserted: an optional `input_lookback_hours` is
489
+ checked against the derived value and a mismatch is rejected. Note that a window
490
+ near the end of the target range can have its lookback clipped by the range end,
491
+ and a window close to the end can remove most remaining rows.
492
+
493
+ `target` states which instant the label is aligned to, so reviewing a manifest
494
+ shows the target definition rather than only a Parameter version string. When
495
+ the target is a Parameter, keep its complete versioned reference **inside**
496
+ `target.parameter`; do not flatten `version` or `project` onto `target`.
497
+ `si_time_source` is `weight_time` or `dispatch_time`. `interpolation` is an
498
+ object with a `method`, never a bare string.
499
+
500
+ ```json
501
+ "target": {
502
+ "parameter": {
503
+ "parameter": "hot_metal_si",
504
+ "version": "v1",
505
+ "project": "replace_with_project"
506
+ },
507
+ "si_time_source": "weight_time",
508
+ "offset_minutes": 0,
509
+ "interpolation": {"method": "linear"}
510
+ }
511
+ ```
512
+
513
+ Allowed interpolation methods are `none`, `linear`, and `forward_fill`. A
514
+ business phrase such as "allow and report target nulls" belongs in an existing
515
+ Parameter missing policy, Feature configuration, or the semantic review; it is
516
+ not a free-form `target` or DatasetManifest field.
517
+
518
+ The declaration is checked against the selected Si Parameter, but only for
519
+ `direct_column` reads, which name their time column in the contract. A `sql` or
520
+ `derived` Parameter can apply an offset inside its query text, so those are
521
+ recorded as unverified with a reason and a warning — do not read the absence of
522
+ an error as confirmation that the label instant matches.
523
+
524
+ `parameter_relationships` evaluates manifest-scoped assertions over exact
525
+ Parameter versions after preprocessing and grid alignment:
526
+
527
+ ```json
528
+ "parameter_relationships": [
529
+ {
530
+ "id": "temperature_pair_consistency",
531
+ "inputs": [
532
+ {"alias": "left", "parameter": "temperature_left", "version": "v1"},
533
+ {"alias": "right", "parameter": "temperature_right", "version": "v1"}
534
+ ],
535
+ "alignment": {"method": "last_before_or_at", "tolerance": "PT5M"},
536
+ "rules": [
537
+ {
538
+ "id": "bounded_difference",
539
+ "stage": "aligned_grid",
540
+ "check": {
541
+ "type": "absolute_difference",
542
+ "left": "left",
543
+ "right": "right",
544
+ "max_difference": 10.0
545
+ },
546
+ "acceptance": {"min_comparable_rate": 0.9},
547
+ "enforcement": "fail"
548
+ }
549
+ ]
550
+ }
551
+ ]
552
+ ```
553
+
554
+ Relationship checks are `compare`, `absolute_difference`, `ratio_range`,
555
+ `sum_equals`, and `co_presence`. `exact` compares only exact grid timestamps.
556
+ `last_before_or_at` is causal. With a tolerance, each relationship input fetch
557
+ starts at `manifest.start - tolerance`; without one, lookup is bounded to data
558
+ inside the manifest target window and does not trigger an unbounded history read.
559
+
560
+ `rowset` selects which instants become dataset rows:
561
+
562
+ ```json
563
+ "rowset": {
564
+ "strategy": "fixed_grid",
565
+ "grid": "10min"
566
+ }
567
+ ```
568
+
569
+ Supported strategies are `event_driven`, `fixed_grid`, and
570
+ `exact_horizon_measured`. `fixed_grid` requires `grid`;
571
+ `exact_horizon_measured` requires `horizon` and accepts a `tolerance`. An
572
+ optional rowset `time_range` overrides the manifest range for that strategy.
573
+ Use `rowset_splits` instead of `rowset` when training needs named strategies
574
+ such as `support`, `validation`, and `test`; it is mutually exclusive with the
575
+ single `rowset`. Split ranges must not overlap and must cover every emitted
576
+ target row. A split-level `fixed_grid.grid` is an explicit assertion and must
577
+ equal `DatasetManifest.time_range.grid`; it cannot silently resample one split.
578
+
579
+ `rowset_splits` defines labels inside one dataset contract. It does not by
580
+ itself publish three DatasetManifests or three DatasetArtifacts. A build emits
581
+ one canonical Feature dataset and one Parameter dataset, with `rowset_split`
582
+ identifying each candidate row. Publish separate manifests only when separate
583
+ artifact identities, permissions, retention, or build lifecycles are required.
584
+
585
+ ### Endpoint eligibility and membership
586
+
587
+ Use `endpoint_policy` when the platform must retain the complete canonical
588
+ rowset while declaring which target endpoints are safe for model training or
589
+ evaluation. Candidate construction and eligibility evaluation are two stages:
590
+
591
+ ```text
592
+ time_range + rowset_splits -> candidate target rows and rowset labels
593
+ candidate rows + endpoint_policy -> eligibility membership and reasons
594
+ ```
595
+
596
+ The policy is scoped to named rowsets, so one manifest can apply it to
597
+ `validation` and `test` while leaving `training` as pass-through:
598
+
599
+ ```json
600
+ {
601
+ "prediction": {"horizon": "2h"},
602
+ "rowset_splits": {
603
+ "training": {
604
+ "strategy": "fixed_grid",
605
+ "grid": "10min",
606
+ "time_range": {
607
+ "start": "2025-01-01T00:00:00+08:00",
608
+ "end": "2026-01-01T00:00:00+08:00",
609
+ "grid": "10min"
610
+ }
611
+ },
612
+ "validation": {
613
+ "strategy": "fixed_grid",
614
+ "grid": "10min",
615
+ "time_range": {
616
+ "start": "2026-01-01T00:00:00+08:00",
617
+ "end": "2026-04-01T00:00:00+08:00",
618
+ "grid": "10min"
619
+ }
620
+ },
621
+ "test": {
622
+ "strategy": "fixed_grid",
623
+ "grid": "10min",
624
+ "time_range": {
625
+ "start": "2026-04-01T00:00:00+08:00",
626
+ "end": "2026-06-01T00:00:00+08:00",
627
+ "grid": "10min"
628
+ }
629
+ }
630
+ },
631
+ "abnormal_windows": {
632
+ "policy": "mark_only",
633
+ "windows": [
634
+ {
635
+ "start": "2026-03-20T00:00:00+08:00",
636
+ "end": "2026-03-28T00:00:00+08:00",
637
+ "reason": "shutdown"
638
+ }
639
+ ]
640
+ },
641
+ "endpoint_policy": {
642
+ "scope": ["validation", "test"],
643
+ "policy": "exclude_target_or_input_overlap",
644
+ "anchor": "cutoff",
645
+ "context_lookback": "28h",
646
+ "window_source": "abnormal_windows",
647
+ "membership_column": "endpoint_eligible",
648
+ "emit_reason": true
649
+ }
650
+ }
651
+ ```
652
+
653
+ `endpoint_policy` requires `prediction.horizon`, at least one abnormal window,
654
+ and `abnormal_windows.policy=mark_only`. Every `scope` name must exist in
655
+ `rowset_splits`; without splits, the only valid scope is `default`. The
656
+ membership column must not collide with reserved or materialized dataset
657
+ columns.
658
+
659
+ Do not derive `context_lookback` from an Operator automatically. Operator
660
+ history requirements answer how much source history is needed to compute a
661
+ Feature. `endpoint_policy.context_lookback` answers how much model input context
662
+ must be free of abnormal observations before an endpoint is usable. They can be
663
+ different and are versioned independently in the manifest contract.
664
+
665
+ For target time `t`, prediction horizon `h`, context lookback `L`, and abnormal
666
+ window `[a, b)`, evaluation uses:
667
+
668
+ ```text
669
+ cutoff c = t - h
670
+ model context = [c - L, c]
671
+ target overlap = a <= t < b
672
+ context overlap = c >= a and c - L < b
673
+ eligible = not (target overlap or context overlap), within policy scope
674
+ ```
675
+
676
+ The abnormal window is half-open and the cutoff side is closed. Its effective
677
+ exclusion tail ends at `b + h + L`; a target exactly at that instant is
678
+ eligible. A scoped row receives `target_in_abnormal_window`,
679
+ `input_context_overlap`, or both in `reason_codes`. Overlapping abnormal windows
680
+ do not duplicate a reason code, but all matching `window_id` values are retained.
681
+
682
+ The build does not delete ineligible rows. It writes
683
+ `rowset_membership.parquet` with one row per candidate target:
684
+
685
+ | Column | Meaning |
686
+ |-|-|
687
+ | `target_time` | Candidate model target time. |
688
+ | `cutoff_time` | `target_time - prediction.horizon`. |
689
+ | `rowset_name` | Named split, or `default` without `rowset_splits`. |
690
+ | `candidate` | Always true for a row emitted by the current candidate resolver. |
691
+ | `policy_applied` | Whether `rowset_name` belongs to `endpoint_policy.scope`. |
692
+ | `eligible` | Overlap result; pass-through rows outside scope remain true. |
693
+ | `reason_codes` | Stable list of exclusion reasons; empty when eligible or `emit_reason=false`. |
694
+ | `matched_window_ids` | Stable IDs of all matching abnormal windows. |
695
+ | `effective_exclusion_end` | Latest matched window end plus horizon and context lookback. |
696
+ | `policy_hash` | Hash of the resolved endpoint policy and normalized windows. |
697
+ | `manifest_hash` | Dataset contract identity used for the build. |
698
+
699
+ For compatibility, `feature_dataset.parquet` and `parameter_dataset.parquet`
700
+ also receive the configured `membership_column`. Its value is:
701
+
702
+ ```text
703
+ endpoint_eligible = policy_applied AND eligible
704
+ ```
705
+
706
+ Consequently, a row outside `scope` is a pass-through membership row with
707
+ `eligible=true`, but its compatibility column is false. Use the membership
708
+ artifact when consumers need to distinguish "not evaluated" from "evaluated
709
+ and rejected"; do not interpret the compatibility column alone as a global
710
+ data-quality flag.
711
+
712
+ The resolved manifest, `quality_report.json`, `validation.json`, and lineage
713
+ record the resolved policy and counts. Reconcile at least
714
+ `candidate_rows`, `policy_applied_rows`, `evaluated_eligible_rows`,
715
+ `ineligible_rows`, `pass_through_rows`, `reason_counts`, and per-rowset counts.
716
+ `eligible_rows` includes true pass-through rows, whereas
717
+ `evaluated_eligible_rows` counts only scoped eligible rows. Single and
718
+ partitioned builds of the same manifest must produce equivalent merged
719
+ membership, counts, lineage, and `policy_hash`.
720
+
721
+ For the EXP301 validation contract, `h=2h` and `L=28h`, so the exclusion tail is
722
+ 30 hours after each abnormal-window end. The fixed 10-minute validation rowset
723
+ contains 20,853 candidates: 19,263 are evaluated eligible and 1,590 are
724
+ ineligible. Treat those figures as an example-specific regression contract,
725
+ not platform defaults.
726
+
727
+ `endpoint_policy` selects historical dataset rows and is not applied to
728
+ `POST /inference-data/fetch`. Realtime inference reports it as not applicable
729
+ and omits it from the replay manifest.
730
+
731
+ The current public schema still records `output.engine=chronon`, while the first-phase bridge executes Python Feature Operators and does not run Chronon compile/backfill. Report this limitation accurately.
732
+
733
+ ## Catalog
734
+
735
+ ```json
736
+ {
737
+ "schema_version": "ml_data_platform.catalog/v1",
738
+ "name": "pressure_feature_catalog",
739
+ "version": "v1",
740
+ "parameters": ["parameters/hot_blast_pressure.v1.json"],
741
+ "operators": [
742
+ {
743
+ "spec": "operators/pressure_features.v1.json",
744
+ "package": "operator_package/dist/pressure_features-1.0.0-py3-none-any.whl"
745
+ }
746
+ ],
747
+ "features": ["features/pressure_mean_5m.v1.json"],
748
+ "feature_sets": ["feature_sets/pressure_core.v1.json"],
749
+ "datasets": ["datasets/pressure_training_20260701_20260702.v1.json"]
750
+ }
751
+ ```
752
+
753
+ All paths must be unique, relative, remain inside the catalog directory, exist, and have the expected `.json` or `.whl` suffix.
754
+
755
+ ## Cross-Object Validation
756
+
757
+ Require:
758
+
759
+ - every Feature input Parameter to exist in the target Registry or the same catalog;
760
+ - every Feature Operator to exist and declare `type=feature`;
761
+ - every Feature input to be allowed by the Operator input schema when that schema declares parameters;
762
+ - every Feature physical output to be declared by the Operator output schema;
763
+ - every FeatureSet reference to exist;
764
+ - every Dataset FeatureSet and preprocess Operator to exist;
765
+ - every Dataset relationship input Parameter to exist;
766
+ - every new immutable version to differ by key from conflicting registered content.