@zhuoyuezs/ml-platform 0.1.0

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