Skip to content

Record training-table lineage across export and reload - #55

Closed
Vedaant-J wants to merge 33 commits into
vedaant/mlops-snapshot-manifestfrom
vedaant/mlops-training-table-manifest
Closed

Vedaant-J wants to merge 33 commits into
vedaant/mlops-snapshot-manifestfrom
vedaant/mlops-training-table-manifest

Conversation

@Vedaant-J

@Vedaant-J Vedaant-J commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Series: kumo-engine MLOps series: every snapshot, training table, model and prediction run carries a self-contained _provenance.json; with kumo.mlops(...) stores each artifact under one artifact root, tracks fits and prediction runs in MLflow or Snowflake and registers models; volumes and stages can hold files; prediction runs are monitored for drift and realized performance.
Stack: Base (lineage, MLOps switch): #50 → #55 → #56
Merge order: one linear stack; merge bottom to top (#53 first)
This PR: Adds lineage for training tables: a manifest recording which snapshot, query and generation plan a training table came from, and, for a table loaded with TrainingTable.from_source_table, the connector, full location and data version its rows were read from, in the same shape #50 uses for snapshot source tables.

Summary

This PR adds lineage for training tables: a manifest recording which snapshot, query and generation plan a training table came from, and, for a table loaded with TrainingTable.from_source_table, the connector, full location and data version its rows were read from, in the same shape #50 uses for snapshot source tables. The manifest also travels with TrainingTable.export as a write-only sidecar. #50 records where a snapshot came from; this PR embeds that snapshot's manifest in the training table's. Each table also gets a deterministic id, and with MLOps on (#50) it is stored once under the artifact root.

Problem. A common workflow is to export a training table, inspect or edit it outside Kumo, then reload it and train on it. Teams also train on labeled tables built entirely outside Kumo. For a model trained either way, they need to answer:

  • Which snapshot was this table generated from, or validated against?
  • Which predictive query and generation plan produced its rows and splits?
  • For a table loaded from files or a warehouse: which connector, which exact path or table were the rows read from, and which version of it?
  • For someone handed an exported table: which query, plan and snapshot produced it?

Today none of these can be answered: generation records no lineage, and a loaded table keeps no reference to where it came from.

Cause. A generated table records only DiskGraph's internal files in its staging directory:

<staging>/
  tt_ingest_spec.json  manifest.json  stats.json  split_stats.json  train_table_col_stats.json
  tt.parquet/part-00000.parquet
  train_seeds/part-00000.parquet  val_seeds/part-00000.parquet  test_seeds/part-00000.parquet

tt_ingest_spec.json holds a tt_id, but nothing links it to the snapshot, the query or the plan. TrainingTable.export writes Parquet parts with a SPLIT column, plus the export's ownership marker and completion marker:

<export>/
  part-00000.parquet  part-00001.parquet  part-00002.parquet
  _KUMO_EXPORT                        # hash of the export identity (tt_id + export format)
  _SUCCESS

TrainingTable.from_source_table stages the rows from the connector and generates an anonymous custom table. The source path or table name and its version are not kept.

Change. Generation now also writes _provenance.json (the series' one manifest name) into the staging directory:

<staging>/
  ...same files as above...
  _provenance.json                    # new

Example from a run of this branch on a three-table synthetic graph (users 200 rows, items 20, orders 1,000), ids shortened, the data directory replaced by <data>, and the generation plan trimmed:

{
  "kind": "training_table", "schema_version": 1,
  "id": "tt-83b45ad0…",
  "parents": {
    "snapshot": {
      "kind": "snapshot", "schema_version": 1, "id": "1d0a5c21…@83dc97878fde6a4e",
      "parents": {"source_tables": {
        "orders": {"connector": "LOCAL_FILE", "location": "<data>/orders.parquet", "version": "1792862945", "version_kind": "timestamp"},
        "...": "…"}},
      "record_sources": true, "environment": {"...": "…"}, "created_at": "2026-10-24T17:29:05+00:00"}},
  "query": "PREDICT COUNT(orders.*, 0, 14, days) > 0 FOR EACH users.USER_ID",
  "task_type": "binary_classification",
  "split_seed": null,
  "generation_plan": {"split": "inferred", "timeframe_step": 14, "...": "…"},
  "environment": {"kumo_engine": "1.0.0.dev0", "build_commit": "7858aceb", "...": "…"},
  "created_at": "2026-10-24T17:29:06+00:00"
}

The same table exported with table.export(TrainingTableOutputConfig(FileConnector(root_dir=<out>), 'tt/')) and loaded back with TrainingTable.load_from_connector(connector, path='tt/', pq=pq, snapshot=snapshot) (new; it is from_source_table with split_col='SPLIT') adds "source_table": {"connector": "LOCAL_FILE", "location": "<out>/tt", "version": "d9d2eb30…", "version_kind": "listing"} to parents and records a ManualSplit plan.

What each field records:

  • id (TrainingTable.id) is tt- plus a hash of the snapshot id, query, generation plan, the drawn split seed, the custom source's version and the engine and DiskGraph builds. Generating the same table twice gives the same id. A custom source that reports no version gets a random id. DiskGraph's own tt_id (TrainingTable.tt_id, the scratch extension's key) stays random per generation, so two generations never share a staging directory.
  • parents.snapshot is the Record provenance for every artifact under one MLOps artifact root #50 snapshot manifest, embedded in full, of the snapshot the table was generated from (for a custom table, the one passed to from_source_table to validate it). If that snapshot has no manifest, a stub {kind, id, reconstructed: true} takes its place. A manifest written earlier with a bare snapshot id reads as such a stub.
  • parents.source_table is recorded only for custom tables, in the same {connector, location, version, version_kind} shape as a snapshot's parents.source_tables.<alias> (Record provenance for every artifact under one MLOps artifact root #50; Custom SQL also records dependencies):
    • connector is the connector type (LOCAL_FILE, S3, GCS, SNOWFLAKE, DATABRICKS, BIGQUERY, ...).
    • location is where the rows were read from, in full: the absolute path for a FileConnector, the full s3:// or gs:// URI (bucket and root included) for object stores, the fully qualified table name for warehouses, and sql:<sha256 of the query> for Custom SQL (never the SQL text).
    • version is the version the connector reported for the rows it actually staged (the same stamp Record provenance for every artifact under one MLOps artifact root #50 records per snapshot table). It comes from the staging read itself, so there is no extra call, and it is set for Custom SQL too. In the example it equals what get_data_version reports for tt/ afterwards.
    • version_kind says what the version proves (exact, timestamp, listing, hour_bucket, unknown), as in Record provenance for every artifact under one MLOps artifact root #50; an exported directory reloads as listing.
    • If the snapshot passed to from_source_table was taken with graph.snapshot(record_sources=False), location is the connector's table name only (and Custom SQL is a hash only, without its text), so one choice covers the snapshot and every custom table validated against it. The choice is read from GraphSnapshot.record_sources (Record provenance for every artifact under one MLOps artifact root #50), then from the snapshot manifest's record_sources; when neither is known (a hand-built snapshot, or a manifest that is missing or unreadable) only the connector and table name are recorded, never full locations.
    • No credentials are recorded. If the connector or location cannot be determined, they are null and a warning is logged.
  • query, task_type and generation_plan record what produced the rows. The plan is the effective generation plan the table carries into Trainer.fit.
  • split_seed is the seed of a random split: the RandomSplit seed, or DiskGraph's default of 0. It is null for temporal and manual splits, as in this example.
  • environment and created_at follow the Record provenance for every artifact under one MLOps artifact root #50 snapshot manifest.
  • TrainingTable.manifest returns a TrainingTableManifest dataclass built on Record provenance for every artifact under one MLOps artifact root #50's (for example manifest.parents.source_table.location). The file and the exported sidecar are unchanged, and a newer file round-trips byte for byte (test_training_table_manifest_round_trips_unchanged).

TrainingTable.id, TrainingTable.tt_id and TrainingTable.manifest expose these values. manifest returns None for tables generated before this PR. Row counts per split and whether the table was generated or loaded are not repeated in the manifest: they stay in DiskGraph's own manifest.json (stage_counts, source_mode), and the private TrainingTable._source_mode reads it.

Parquet directory exports to file destinations (any sink whose holds_files accepts the path, #112) gain one file, _provenance.json, a copy of the staged manifest. It is written after the parts and before _SUCCESS:

<export>/
  part-00000.parquet  part-00001.parquet  part-00002.parquet   # byte-identical to before
  _KUMO_EXPORT
  _provenance.json                    # new: the training-table manifest above
  _SUCCESS

In the run above the parsed sidecar equals table.manifest. The sidecar is for whoever receives the export: it says which query, plan and snapshot produced the rows. The engine never reads it. Reloading an export records the export's connector, location and version like any other custom table, and an edited or sidecar-less export loads exactly as before. The sidecar is best-effort end to end: if the staged manifest cannot be read or the sidecar cannot be prepared, the rows are exported without it and a warning names _provenance.json; a failed sidecar write warns and _SUCCESS is still written.

Artifact root. With MLOps on (mlops= on generate_training_table/from_source_table, or a kumo.mlops block, #50), each table is also stored once under <root>/training_tables/<id>/ through the same export. A second generation of the same id finds _SUCCESS there and writes nothing; a failure warns. With MLOps off, nothing is stored.

Plumbing:

  • ObjectStoreSinkBackend.write_files gains sidecars: Mapping[str, bytes] = {}. kumo-core export_selected_files writes the sidecars into directory destinations, after the data and before _SUCCESS. This is not a kumoapi protocol change.
  • Single-file, non-Parquet (CSV) and warehouse destinations get no sidecar.

Back-compat

This PR only records and exposes information. New reads and writes are best-effort: they warn and continue, and nothing new raises.

Situation Behaviour
New training table _provenance.json written. Rows, splits and the returned TrainingTable are unchanged.
New custom table Same, plus parents.source_table. No extra calls: the version is the one staging already read.
New Parquet directory export Part files are byte-identical to before. _provenance.json is added before _SUCCESS. Re-exporting the same table to the same directory rewrites the identical sidecar.
Table generated before this PR manifest returns None. tt_id still works, because it is read from tt_ingest_spec.json. Exporting it writes no sidecar.
Export written before this PR, or edited, reloaded Loads as before. The new table records the export's connector, location and version.
Export written by this PR, read by an older engine The extra file is ignored, in the same way as _KUMO_EXPORT and _SUCCESS.
Manifest or sidecar write fails, or the staged manifest is unreadable at export A warning is logged. Generation or export completes as before, without the file; _SUCCESS is still written.
Connector type or location can't be determined A warning is logged and the fields are null. The load completes as before.
Connector reports no version for the staged rows version is null.
Snapshot taken with record_sources=False location is the table name only.
Snapshot whose record_sources choice is unknown (hand-built GraphSnapshot, manifest missing or unreadable) location is the table name only (connector + table name), rather than the full location.

Known limitations

  • Warehouse locations are the connector's database (or catalog, or project) and schema (or dataset) joined with the table name. A table name that is already fully qualified still gets that prefix.
  • A third-party ObjectStoreSinkBackend subclass that overrides write_files without **kwargs would get a TypeError on directory exports. No such subclass exists in this repo.

Live S3/GCS run

Synthetic users/items/orders data, against a dev S3 bucket with the instance role (fresh prefix, deleted afterwards, 0 objects left). Training table exported to the bucket through a rooted S3Connector, reloaded with from_source_table from that directory, fitted with the bundle-provenance PR (#56) on top. This run predates two later changes: the connector key in parents.source_table, and taking version from the staged read (at the time it was a separate get_data_version call; both gave the same value in this run). It was not re-run after them, after the removal of rows/source, or after this revision (deterministic id, embedded snapshot, _provenance.json).

Store Result Timings Sizes
S3 PASS: sidecar equals the exported table's manifest, _kumo_manifest.json (the sidecar's name then) and _SUCCESS present; the reloaded table recorded parents.source_table = {"location": "s3://<bucket>/<prefix>/tt", "version": "c030a09e…"}, with version equal to get_data_version for it; the bundle trained on it recorded the reloaded table id, its source as custom (recorded then as custom, now custom_tt), the snapshot id, location and version export 1.4 s, reload 1.1 s 6 objects, 9 KB
GCS Not run: the repo's GCS test credential is available only in CI — —

Testing Done

  • At the current head: test_training_table_export.py (19), test_custom_source.py (20), test_custom_sql.py (11) and test_predictive_query.py (67) pass. New in this revision:

    • the same table generated twice has one id (DiskGraph tt_ids differ), and the embedded snapshot equals snapshot.manifest;
    • a kumo.mlops block stores the table once under training_tables/<id>/ (a second generation rewrites nothing), and a block that does not export training tables stores nothing;
    • a bare snapshot id in parents reads as a stub;
    • load_from_connector round-trips an export.
  • New engine tests:

    • a generated table's manifest (id equals tt_id, parents has only snapshot, no rows or source, and source_mode is pquery); exported parts byte-identical to the plain exporter; the sidecar equals the table manifest; a FileConnector reload records LOCAL_FILE, the absolute path and the version staging read with version_kind listing, has source_mode custom_tt, and has the same split counts;
    • a custom table validated against a snapshot taken with record_sources=False records only the table name, one taken with the default records the full location, and one whose choice is unknown (no record_sources, no manifest) records only the table name;
    • a failing manifest write warns, and generation and export complete without the file;
    • an export whose staged manifest is unreadable warns, writes the parts and no sidecar;
    • a CSV directory export gets no sidecar, and a warehouse sink receives no sidecars.
  • New kumo-core test: sidecars are written before _SUCCESS, and a failing sidecar write warns while _SUCCESS is still written.

  • Location rendering for object stores, warehouses and Custom SQL, and the null-with-warning case, are covered by Record provenance for every artifact under one MLOps artifact root #50's test_snapshot_manifest.py (shared helper).

  • Run at the current head: test_training_table_export.py, test_custom_source.py and test_snapshot_manifest.py (61 passed). The Custom SQL and predictive-query suites and kumo-core test_artifact_export.py were reported passing by the implementer before this change and were not re-run.

  • The examples above were captured from a real run of this branch on synthetic data (version strings of the engine and kumo-core shown as released-style values).

Checklist

  • No breaking changes.
  • Unit tests added.

🤖 Generated with Claude Code

@Vedaant-J

Vedaant-J commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor Author

Change map

File Function / scope What changed Why
packages/kumo-core/src/kumo_core/artifact_export.py export_selected_files sidecars={} written into directory outputs before _SUCCESS; a failed sidecar write warns and publishing continues sidecar must precede the success marker and never block the export
packages/kumo-core/src/kumo_core/object_store_sink.py ObjectStoreSinkBackend.write_files, _ArrowExportFiles.write_bytes pass sidecars through; write raw bytes export the manifest through the connector's filesystem
packages/kumo-core/tests/test_artifact_export.py test_sidecars_are_written_before_success, test_failed_sidecar_write_still_publishes new ordering, directory-only, best-effort
packages/kumo-engine/docs/mlops.md docs training-table section: manifest, deterministic id, export sidecar, artifact root user docs for this PR
packages/kumo-engine/kumo/engine/__init__.py imports, __all__ exports TrainingTableManifest public type of TrainingTable.manifest
packages/kumo-engine/kumo/engine/_manifest.py TrainingTableParents, TrainingTableManifest training-table manifest dataclass; parents.snapshot is the embedded snapshot manifest; fields optional for stubs typed, self-contained TrainingTable.manifest
packages/kumo-engine/kumo/engine/pquery/_custom_source.py stage_source_table returns (path, version), the version prepare_ingest reported for the staged rows version of the rows actually read, no extra call
packages/kumo-engine/kumo/engine/pquery/prediction_table.py PredictionTable.from_source_table unpacks the new return value signature change
packages/kumo-engine/kumo/engine/pquery/predictive_query.py generate_training_table mlops=; finishes the table through _finish (id, manifest, root store) snapshot is only known at generation
packages/kumo-engine/kumo/engine/pquery/training_table.py TrainingTable.id, manifest, _source_mode, export, _export_sidecars, load_from_connector, _store, from_source_table, _finish, _table_id, _record_manifest deterministic id; _provenance.json (embedded snapshot, query, task type, split seed, plan, custom source_table) at generation; export sidecar for any file destination (is_file_destination); load_from_connector re-materializes an export; with MLOps on, stored once under training_tables/<id>/ training-table lineage, a stable id and the artifact root
packages/kumo-engine/test/connector/test_custom_source.py, packages/kumo-engine/test/test_custom_sql.py, packages/kumo-engine/test/test_predictive_query.py staging call sites, fake prepared sources unpack (path, version); fakes carry version signature change
packages/kumo-engine/test/test_training_table_export.py manifest, sidecar, record_sources, best-effort, MLOps store, stub tests deterministic id across generations, embedded snapshot, sidecar equals manifest, custom source_table, table name only under record_sources=False or unknown, failed manifest write, unreadable staged manifest, no sidecar for CSV or warehouse, stored once under the root, bare snapshot id reads as a stub behaviour coverage

Requirement: a bundle trained on a generated, exported or externally built table must be able to record its origin. Existing behaviour: a loaded table is anonymous. Smallest change: one staging manifest, one write-only export sidecar, and the connector, location and staged version of custom tables (no re-read of the data). Stopping condition: lineage recorded; nothing else changes.

@copy-pr-bot

copy-pr-bot Bot commented Oct 20, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Walkthrough

Training-table generation now assigns an ID and records a manifest with snapshot and optional source-table metadata. Source staging returns the source version when available. Training tables expose the ID, manifest, and source mode. When configured, MLOps stores the table under the artifact root. File exports can include the manifest as a sidecar for non-CSV output. Manifest and sidecar failures produce warnings without preventing table return or row export.

Sequence Diagram(s)

sequenceDiagram
  participant TrainingTable
  participant ObjectStoreSinkBackend
  participant export_selected_files
  participant Filesystem
  TrainingTable->>ObjectStoreSinkBackend: Pass manifest sidecar bytes to write_files
  ObjectStoreSinkBackend->>export_selected_files: Forward sidecars
  export_selected_files->>Filesystem: Write sidecars before _SUCCESS
Loading

Priority: Normal

Merge Risk: Moderate · up to 732f4

Resolve the export compatibility and training-table ID issues before merging. The permission-dependent test can fail under privileged test runners, and the prior sidecar-name risk also needs resolution.

Pre-merge checks: Passed 2 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Change Map Design Review Warning The change map is incomplete for the stated export-and-reload requirement. TrainingTable.export writes _provenance.json, but load_from_connector at training_table.py:130-139 ignores that sidec… Make TrainingTable.load_from_connector read and validate _provenance.json when present. Preserve the exported manifest's original snapshot, query, and generation plan, while recording the current validation snapshot and staged source se…
Passed Passed checks: 2 passed
Check name Status Explanation
Pr Size Passed The pull request changes 14 files. The limit is 40 files, so the PR passes this check.
Public Resource Access Passed The pull request adds no external URL, repository location, or package source location. The changed documentation contains only API names, manifest filenames, and the illustrative `/training_tab…
Full details: Public Resource Access

Explanation

The pull request adds no external URL, repository location, or package source location. The changed documentation contains only API names, manifest filenames, and the illustrative &lt;root&gt;/training_tables/&lt;id&gt;/ path. Code imports refer to modules within this repository, and test paths are local fixtures.

Full details: Change Map Design Review

Explanation

The change map is incomplete for the stated export-and-reload requirement. TrainingTable.export writes _provenance.json, but load_from_connector at training_table.py:130-139 ignores that sidecar and calls from_source_table, which creates a new manifest from the current snapshot and exported directory. The test at test_training_table_export.py:333-345 checks only the new custom-source entry and explicitly accepts a different ID; it does not verify preservation of the original query, plan, or snapshot lineage. The sidecar plumbing in artifact_export.py and object_store_sink.py is at the appropriate shared export layer, and source-version staging is necessary, but the reload design omits the central behavior claimed by the PR.

Resolution

Make TrainingTable.load_from_connector read and validate _provenance.json when present. Preserve the exported manifest's original snapshot, query, and generation plan, while recording the current validation snapshot and staged source separately. Treat missing or unreadable sidecars as best-effort cases. Add a test that exports a generated table, reloads it, and asserts that the original lineage survives.

Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@Vedaant-J
Vedaant-J added this pull request to stack #63 October 20, 2026 22:18
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-training-table-manifest branch from 33f889d to b1a607d Compare October 20, 2026 23:29
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 20, 2026 23:29 — with GitHub Actions Inactive
Vedaant-J added a commit that referenced this pull request Oct 21, 2026
#55 no longer records derived_from or modified for a reloaded table, so
provenance.json's training_table block stopped having anything to copy.

The block now records the table id, whether it was generated (pql) or
loaded (custom), the snapshot it was generated on or validated against,
and, for custom tables, the location and data version they were loaded
from. The bundle stays self-contained without the scratch staging
directory; query and plans stay only in training-spec.json.

Verified with test_bundle_provenance.py (pql fit, custom-table stub,
manifest-less stub) and test_trainer_registry.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 07:18 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-training-table-manifest branch from d6a5b87 to 6afff95 Compare October 21, 2026 07:57
Vedaant-J added a commit that referenced this pull request Oct 21, 2026
#55 no longer records derived_from or modified for a reloaded table, so
provenance.json's training_table block stopped having anything to copy.

The block now records the table id, whether it was generated (pql) or
loaded (custom), the snapshot it was generated on or validated against,
and, for custom tables, the location and data version they were loaded
from. The bundle stays self-contained without the scratch staging
directory; query and plans stay only in training-spec.json.

Verified with test_bundle_provenance.py (pql fit, custom-table stub,
manifest-less stub) and test_trainer_registry.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 07:57 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-training-table-manifest branch from 6afff95 to 02b13a4 Compare October 21, 2026 08:47
Vedaant-J added a commit that referenced this pull request Oct 21, 2026
#55 no longer records derived_from or modified for a reloaded table, so
provenance.json's training_table block stopped having anything to copy.

The block now records the table id, whether it was generated (pql) or
loaded (custom), the snapshot it was generated on or validated against,
and, for custom tables, the location and data version they were loaded
from. The bundle stays self-contained without the scratch staging
directory; query and plans stay only in training-spec.json.

Verified with test_bundle_provenance.py (pql fit, custom-table stub,
manifest-less stub) and test_trainer_registry.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 08:47 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-training-table-manifest branch from 02b13a4 to 2ab9712 Compare October 21, 2026 09:01
Vedaant-J added a commit that referenced this pull request Oct 21, 2026
#55 no longer records derived_from or modified for a reloaded table, so
provenance.json's training_table block stopped having anything to copy.

The block now records the table id, whether it was generated (pql) or
loaded (custom), the snapshot it was generated on or validated against,
and, for custom tables, the location and data version they were loaded
from. The bundle stays self-contained without the scratch staging
directory; query and plans stay only in training-spec.json.

Verified with test_bundle_provenance.py (pql fit, custom-table stub,
manifest-less stub) and test_trainer_registry.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 09:01 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-training-table-manifest branch from 2ab9712 to 996fa16 Compare October 21, 2026 09:58
Vedaant-J added a commit that referenced this pull request Oct 21, 2026
#55 no longer records derived_from or modified for a reloaded table, so
provenance.json's training_table block stopped having anything to copy.

The block now records the table id, whether it was generated (pql) or
loaded (custom), the snapshot it was generated on or validated against,
and, for custom tables, the location and data version they were loaded
from. The bundle stays self-contained without the scratch staging
directory; query and plans stay only in training-spec.json.

Verified with test_bundle_provenance.py (pql fit, custom-table stub,
manifest-less stub) and test_trainer_registry.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 09:58 — with GitHub Actions Inactive
@Vedaant-J Vedaant-J closed this Oct 21, 2026
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-training-table-manifest branch from 996fa16 to 75f9e80 Compare October 21, 2026 16:27
@Vedaant-J Vedaant-J reopened this Oct 21, 2026
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 21, 2026 16:28 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-snapshot-manifest branch from 75f9e80 to a748d54 Compare October 21, 2026 18:55
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-training-table-manifest branch from 996fa16 to f3790cf Compare October 21, 2026 18:55
…ests

The series turns MLOps on per block or per call rather than through
environment variables, every manifest file gets one name, and
platforms record their own job ids.

- kumo.MLOps(artifact_root, tracking=, registry=, export=) and the
  kumo.mlops(cfg) block (a contextvar: the nearest block, replaced by a
  call's mlops=; per thread and asyncio task). The root is a URI through
  connector_for_root (moved here) or a (connector, path) pair whose sink
  holds files there; a missing root or unknown export kind raises.
- Manifests: kinds snapshot/training_table/model/prediction; a parent
  manifest with no record (or a bare id) reads as a stub with only kind,
  id and reconstructed set; embedded() drops a manifest's statistics
  and mutable fields where a child embeds it. The snapshot manifest is
  _provenance.json (kumo_core.artifacts.PROVENANCE_FILE).
- Removed SchedulerContext, Manifest.scheduler and scheduler detection.
  GraphSnapshot.counts is private.
- docs/mlops.md holds the series docs; the README links it.

Verified: test_mlops.py (root forms, precedence, nesting, threads and
tasks, stubs), test_snapshot_manifest.py, registry and import suites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-snapshot-manifest branch from 915e10c to 27996c2 Compare October 24, 2026 17:22
Vedaant-J and others added 13 commits October 24, 2026 17:22
A training table exported with TrainingTable.export and reloaded with
from_source_table becomes an anonymous custom table: its tt_id, snapshot
id, query and generation plan are lost, so a model trained on an exported
or edited table cannot say where its rows came from.

Generation (PredictiveQuery.generate_training_table and
TrainingTable.from_source_table) now writes training-table-manifest.json
into the staging directory: tt_id, parent snapshot id, query, generation
plan, task type, rows per split, effective split seed, source (pql or
custom), environment and scheduler. TrainingTable.tt_id and
TrainingTable.manifest read it back (None for older tables). Directory
exports to file and object-store sinks add _kumo_manifest.json, the
manifest plus a content digest of the parts and the export identity,
through a new sidecars argument of ObjectStoreSinkBackend.write_files that
export_selected_files writes before _SUCCESS. Reloading that exact
directory, with a matching _KUMO_EXPORT marker, records derived_from and
whether the staged rows differ from the exported digest. All new writes
and reads are best-effort: failures warn, nothing new raises, and
exported part files are unchanged.

Verified with new tests for the manifest fields, the sidecar contents and
idempotent re-export, byte-identical parts versus the plain exporter,
reload of an untouched, edited, sidecar-less, garbled or foreign export,
a failing manifest write, a warehouse sink receiving no sidecars, and
sidecar ordering in export_selected_files; plus the existing training-table
export, custom-source, file-connector, snapshot-manifest, prediction
export, registry, artifact-export and S3 suites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reload compared the export digest against the staged copy, but staging
re-encodes some Parquet (narrow integer keys, list<double> columns,
dictionary columns), so an untouched export could read as modified.

The digest is now read from the export directory itself:
ObjectStoreSinkBackend.content_digest streams the visible Parquet parts
through the connector filesystem, and artifact_export.content_sha256 is
the one formula export and reload share. Sink lookup moves inside the
best-effort guard, a missing sidecar or destination logs at debug, CSV
directory exports get no sidecar, and _record_manifest lineage arguments
are keyword-only.

Verified with a reload of an export carrying a list<double> weight column
(staged bytes differ, modified stays false), a CSV directory export, and
the training-table export, custom-source and artifact-export suites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reload lineage (derived_from, modified) read the export sidecar back and
re-downloaded and hashed every part to compare against content_sha256:
a second full remote read on top of staging, for a flag that could only
be recorded.

A custom table now records where its rows came from in
parents.source_table: the full location (absolute path for a
FileConnector, the full s3:// or gs:// URI for object stores, the fully
qualified name for warehouse tables, a sha256 of the SQL for Custom SQL)
and the connector's get_data_version stamp. Either is null with a warning
when it cannot be determined. The export sidecar stays write-only and now
holds just the training-table manifest; derived_from, modified,
content_sha256, export_identity, ObjectStoreSinkBackend.read_sidecar and
content_digest are removed. No engine code reads the sidecar.

Verified with the training-table export, custom-source, snapshot-manifest
and kumo-core artifact-export suites, including a FileConnector reload
recording its path and version, failing location and version lookups
recording nulls, and S3, warehouse and SQL location rendering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parents.source_table now uses the shared source_table helper from the
snapshot manifest: {"connector", "location", "version"}, with the same
location rules, so a custom table and a snapshot source can be matched
on connector and location. The version is still the connector's
get_data_version stamp, null with a warning when it cannot be read.

Verified with test_training_table_export.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failing _kumo_manifest.json write (a bucket policy, a transient error)
raised out of export_selected_files before _SUCCESS, so an export that
works today failed. Each sidecar write now warns and continues, and a
failure building the training-table manifest exports without it.

A custom table's version came from a second get_data_version call after
staging, which could describe a later state of the source and was null
for Custom SQL. stage_source_table now returns the version prepare_ingest
reports for the rows it staged, the same stamp the snapshot manifest
records.

Verified with test_training_table_export.py, connector/test_custom_source.py,
test_custom_sql.py and kumo-core test_artifact_export.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The export sidecar path re-serialised the staged manifest behind a
redundant directory check and a catch-all, and the tests restated
fields and covered helpers owned by the snapshot-manifest PR.

Copy the staged manifest bytes as the sidecar, inline the single-use
source-lineage helper, shorten docstrings, and merge the lineage
tests into one export/reload test plus best-effort and no-sidecar
cases. Behaviour is unchanged.

Verified: test_training_table_export, test_custom_source,
test_custom_sql, test_predictive_query -k custom and kumo-core
test_artifact_export pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
training-table-manifest.json repeated the per-stage row counts and the
pql/custom source that DiskGraph already records in the table's
manifest.json (stage_counts, source_mode) and tt_ingest_spec.json.

Drop rows and source from the training-table manifest and expose
TrainingTable.source_mode from DiskGraph's manifest; a custom table is
still identified by parents.source_table.

Verified with the training-table export, custom-source and predictive
query suites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A custom training table always recorded its source's full location and
no version kind, even when its snapshot was taken with
record_locations=False.

The source entry now carries version_kind and follows the snapshot
manifest's record_locations choice.

Verified with the training-table export tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failure while preparing _kumo_manifest.json (an unreadable staging
manifest, an unexpected sink) failed the whole export. Preparing the
sidecar is now best-effort: the rows are exported without it and a
warning names the file.

Verified with test_training_table_export.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
graph.snapshot(record_locations=False) was renamed record_sources=False
in the snapshot manifest; a custom training table built against such a
snapshot reads the new field and records its source the same way.

Verified with test_training_table_export.py and test_snapshot_manifest.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#50 types a directory's listing fingerprint as "listing"; the reloaded
custom table records that kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A custom training table read the snapshot's record_sources choice from
the snapshot manifest and recorded full source locations when that file
was missing or unreadable, so a snapshot taken with record_sources=False
could still leak locations through the table's manifest.

The table now follows GraphSnapshot.record_sources, then the manifest's
recorded choice, and records connector and table name only when neither
says the snapshot recorded sources.

Verified with test_training_table_export.py and test_snapshot_manifest.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TrainingTable.manifest returned a dict like the snapshot manifest did.

It now returns a TrainingTableManifest (parents: snapshot and, for a
custom table, its source table) built on the shared manifest dataclasses;
the file on disk and the exported _kumo_manifest.json are unchanged.

Verified with test_training_table_export.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J removed this pull request from stack #98 October 24, 2026 17:23
The training-table manifest named its snapshot by id only and its own
id was DiskGraph's random tt id, so the same table generated twice got
two ids and no artifact store could recognise it.

- TrainingTable.id: tt- plus a hash of the snapshot id, query,
  generation plan, drawn split seed, the custom source's version and
  the engine/DiskGraph builds (random when a custom source has no
  version). DiskGraph's extension dir keeps its random tt id.
- The manifest is _provenance.json and embeds the snapshot manifest
  (a stub when the snapshot has none; a bare id reads as a stub).
- With MLOps on (mlops= or a kumo.mlops block), each table is stored
  once under <root>/training_tables/<id>/; failures warn.
- TrainingTable.load_from_connector(connector, path=, pq=, snapshot=)
  materializes an export again. The export sidecar follows any file
  destination (is_file_destination). source_mode is private.

Verified: test_training_table_export.py (19: deterministic id, embedded
snapshot, stored once, stub reading), custom source, Custom SQL and
predictive query suites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-training-table-manifest branch from f7c62c3 to 7858ace Compare October 24, 2026 17:28
@Vedaant-J
Vedaant-J had a problem deploying to gha-kumo-workflow-tests October 24, 2026 17:28 — with GitHub Actions Error
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vedaant-J added a commit that referenced this pull request Oct 24, 2026
#55 no longer records derived_from or modified for a reloaded table, so
provenance.json's training_table block stopped having anything to copy.

The block now records the table id, whether it was generated (pql) or
loaded (custom), the snapshot it was generated on or validated against,
and, for custom tables, the location and data version they were loaded
from. The bundle stays self-contained without the scratch staging
directory; query and plans stay only in training-spec.json.

Verified with test_bundle_provenance.py (pql fit, custom-table stub,
manifest-less stub) and test_trainer_registry.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Vedaant-J
Vedaant-J temporarily deployed to gha-kumo-workflow-tests October 24, 2026 17:42 — with GitHub Actions Inactive
@Vedaant-J
Vedaant-J added this pull request to stack #113 October 24, 2026 19:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Other comments: 2
packages/kumo-engine/test/test_training_table_export.py: 1

410-416: Minor · Functional Correctness · Quick win

Make test_unreadable_manifest_still_exports independent of the user's privileges.

manifest.chmod(0) blocks the read only for non-root users. Many CI containers run as root. As root, manifest.read_bytes() in _export_sidecars succeeds and no warning is emitted. pytest.warns then fails with "DID NOT WARN". The test also depends on POSIX permission semantics.

Skip the test for root. Alternatively, patch the read so the failure no longer depends on file mode.

🧪 Proposed fix
+@pytest.mark.skipif(
+    not hasattr(os, 'geteuid') or os.geteuid() == 0,
+    reason='file permissions do not block reads for root or on this OS')
 def test_unreadable_manifest_still_exports(graph):

Add import os to the imports at the top of the file if it is not already imported.

packages/kumo-engine/kumo/engine/pquery/training_table.py: 1

334-340: Minor · Data Integrity & Integration · Quick win

Include the custom-table options and the source identity in the table ID key.

For caller-supplied tables, the ID key contains the snapshot, query, plan, seed and source version. It omits spec.weight_col and spec.timestamp_unit. It also omits which source table the version belongs to. Both options change the generated rows. Neither appears in the manifest.

Two calls to from_source_table can use the same source version with different weight_col or timestamp_unit. They get the same id. _store then sees _SUCCESS for the first table and skips storing the second. The artifact under training_tables/<id>/ and its manifest then describe a different table than the caller built.

Pass these options into the key. Also add the source entry's connector and location. This makes the ID depend on the source as well as its version.

🛠️ Proposed fix
-        None if source_version is None else str(source_version),
+        None if source_version is None else str(source_version),
+        source_options,
         build.kumo_engine, build.build_commit, build.diskgraph,
         build.diskgraph_build_commit

Outside the selected range, add a source_options: Any = None parameter to _table_id. Add the same parameter to _finish and forward it. In from_source_table, pass the options:

source_options={
    'weight_col': spec.weight_col,
    'timestamp_unit': spec.timestamp_unit,
    'source': source_table_entry_dict,  # connector and location from the SourceTableEntry
}

Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between f7c62c3 and 732f4c3.

Files selected for processing: 7
  • packages/kumo-core/src/kumo_core/object_store_sink.py
  • packages/kumo-engine/docs/mlops.md
  • packages/kumo-engine/kumo/engine/__init__.py
  • packages/kumo-engine/kumo/engine/_manifest.py
  • packages/kumo-engine/kumo/engine/pquery/predictive_query.py
  • packages/kumo-engine/kumo/engine/pquery/training_table.py
  • packages/kumo-engine/test/test_training_table_export.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 5 remain after this review.

Comment on lines +108 to +109
**self._export_sidecars(connector,
output_config))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major · Stability & Availability · Quick win

🔎 Supported by static analysis

Script executed:

#!/bin/bash
# Find every write_files definition and check for sidecars or **kwargs.
rg -nP --type=py -A6 'def write_files\s*\(' -g '!**/tests/**' -g '!**/test/**'
# Find the FileStoreSink protocol and its write_files contract.
rg -nP --type=py -C3 'class FileStoreSink|def holds_files|def _file_sink'

Repository: NVIDIA/kumo-engine

Length of output: 7750


Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- training_table export context ---'
nl -ba packages/kumo-engine/kumo/engine/pquery/training_table.py | sed -n '70,125p;430,535p'
printf '%s\n' '--- file destination and sink resolution ---'
rg -n -F --glob '*.py' -e 'is_file_destination' -e '_file_sink(' -e 'write_files(' packages/kumo-engine/kumo packages/kumo-core/src/kumo_core packages/kumo-api/kumoapi packages/kumo-databricks/kumo_databricks packages/kumo-snowflake/kumo_snowflake
printf '%s\n' '--- exact connector resolution block ---'
nl -ba packages/kumo-engine/kumo/engine/connector/__init__.py | sed -n '1,130p'
printf '%s\n' '--- sink class declarations and complete write methods ---'
rg -n -F --glob '*.py' -e 'class Databricks' -e 'class Snowflake' -e 'class ObjectStore' -e 'class BigQuery' packages/kumo-databricks packages/kumo-snowflake packages/kumo-core packages/kumo-bigquery
nl -ba packages/kumo-databricks/src/kumo_databricks/sink.py | sed -n '1,205p'
nl -ba packages/kumo-snowflake/src/kumo_snowflake/sink.py | sed -n '1,215p'

Repository: NVIDIA/kumo-engine

Length of output: 5583


Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- connector file-sink dispatch ---'
nl -ba packages/kumo-engine/kumo/engine/connector/__init__.py | sed -n '45,95p'
nl -ba packages/kumo-engine/kumo/engine/connector/file.py | sed -n '145,225p'
printf '%s\n' '--- sink declarations and signatures ---'
rg -n -F --glob '*.py' -e 'class ' -e 'def write_files' packages/kumo-databricks/src/kumo_databricks/sink.py packages/kumo-snowflake/src/kumo_snowflake/sink.py packages/kumo-core/src/kumo_core/object_store_sink.py
nl -ba packages/kumo-databricks/src/kumo_databricks/sink.py | sed -n '1,180p'
nl -ba packages/kumo-snowflake/src/kumo_snowflake/sink.py | sed -n '1,190p'
printf '%s\n' '--- connector sink construction references ---'
rg -n -F --glob '*.py' -e 'sink_backend' -e 'DatabricksSink' -e 'SnowflakeSink' packages/kumo-engine packages/kumo-databricks packages/kumo-snowflake packages/kumo-api

Repository: NVIDIA/kumo-engine

Length of output: 39739


Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- FileStoreSink protocol and filesystem adapter ---'
nl -ba packages/kumo-api/kumoapi/plugin.py | sed -n '570,635p'
nl -ba packages/kumo-core/src/kumo_core/object_store_sink.py | sed -n '385,435p'
printf '%s\n' '--- repository FileStoreSink implementations and tests ---'
rg -n -F --glob '*.py' -e 'holds_files' -e 'class StageSink' -e 'class .*FileStore' -e 'FileStoreSink' packages/kumo-engine packages/kumo-core packages/kumo-api packages/kumo-databricks packages/kumo-snowflake
nl -ba packages/kumo-engine/test/test_filestore_sink.py | sed -n '1,175p'
printf '%s\n' '--- sink protocol/base declarations ---'
nl -ba packages/kumo-api/kumoapi/plugin.py | sed -n '540,615p'
nl -ba packages/kumo-engine/kumo/engine/connector/base.py | sed -n '65,105p'

Repository: NVIDIA/kumo-engine

Length of output: 19703


Make the sidecars keyword part of the file-export contract.

TrainingTable.export passes sidecars for non-CSV file destinations. FileArtifactSink.write_files does not declare this keyword, so a sink implementing both FileArtifactSink and FileStoreSink with the declared fixed signature can raise TypeError.

Extend the contract and all applicable implementations to accept sidecars, or pass the keyword only when the sink supports it. The inspected stage and local implementations already forward arbitrary keyword arguments.

@Vedaant-J
Vedaant-J force-pushed the vedaant/mlops-snapshot-manifest branch from 27996c2 to f590d71 Compare October 26, 2026 18:23
@Vedaant-J
Vedaant-J removed this pull request from stack #113 October 26, 2026 18:25
@Vedaant-J

Copy link
Copy Markdown
Contributor Author

Following up on your question about consuming the training-table export manifest and native artifact stores, now that this PR is folded into #50:

@Vedaant-J

Copy link
Copy Markdown
Contributor Author

Folded into #50 (artifacts & provenance), which now contains this PR's final code: the consolidated stack shows only the end state. Review continues there.

@Vedaant-J Vedaant-J closed this Oct 26, 2026

This branch was previously deployed

1 inactive deployment
gha-kumo-workflow-tests — 732f4c32 Deployed Oct 24, 2026 by Vedaant-J via import #709
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants