Repository navigation
Conversation
Change map
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. |
33f889d to
b1a607d
Compare
#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>
d6a5b87 to
6afff95
Compare
#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>
6afff95 to
02b13a4
Compare
#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>
02b13a4 to
2ab9712
Compare
#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>
2ab9712 to
996fa16
Compare
#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>
996fa16 to
75f9e80
Compare
75f9e80 to
a748d54
Compare
996fa16 to
f3790cf
Compare
…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>
915e10c to
27996c2
Compare
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>
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>
f7c62c3 to
7858ace
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#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>
There was a problem hiding this comment.
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_exportsindependent 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_sidecarssucceeds and no warning is emitted.pytest.warnsthen 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 osto 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_colandspec.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_tablecan use the same source version with differentweight_colortimestamp_unit. They get the sameid._storethen sees_SUCCESSfor the first table and skips storing the second. The artifact undertraining_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_commitOutside the selected range, add a
source_options: Any = Noneparameter to_table_id. Add the same parameter to_finishand forward it. Infrom_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-
- Source mode: Auto
- Repository: NVIDIA/kumo-engine
- Branch: vedaant/mlops-snapshot-manifest
- File: .coderabbit.yaml
- Commit: 27996c2
- Layers: Repository YAML
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
00527b03-8cfb-4035-9bb4-0b17e43695fe
Files selected for processing: 7-
packages/kumo-core/src/kumo_core/object_store_sink.pypackages/kumo-engine/docs/mlops.mdpackages/kumo-engine/kumo/engine/__init__.pypackages/kumo-engine/kumo/engine/_manifest.pypackages/kumo-engine/kumo/engine/pquery/predictive_query.pypackages/kumo-engine/kumo/engine/pquery/training_table.pypackages/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.
-
| **self._export_sidecars(connector, | ||
| output_config)) |
There was a problem hiding this comment.
🔎 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.
27996c2 to
f590d71
Compare
|
Following up on your question about consuming the training-table export manifest and native artifact stores, now that this PR is folded into #50:
|
|
Folded into #50 (artifacts & provenance), which now contains this PR's final code: the consolidated stack shows only the end state. Review continues there. |
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 withTrainingTable.exportas 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:
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:
tt_ingest_spec.jsonholds att_id, but nothing links it to the snapshot, the query or the plan.TrainingTable.exportwrites Parquet parts with aSPLITcolumn, plus the export's ownership marker and completion marker:TrainingTable.from_source_tablestages 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:Example from a run of this branch on a three-table synthetic graph (
users200 rows,items20,orders1,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 withTrainingTable.load_from_connector(connector, path='tt/', pq=pq, snapshot=snapshot)(new; it isfrom_source_tablewithsplit_col='SPLIT') adds"source_table": {"connector": "LOCAL_FILE", "location": "<out>/tt", "version": "d9d2eb30…", "version_kind": "listing"}toparentsand records aManualSplitplan.What each field records:
id(TrainingTable.id) istt-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 owntt_id(TrainingTable.tt_id, the scratch extension's key) stays random per generation, so two generations never share a staging directory.parents.snapshotis 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 tofrom_source_tableto 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_tableis recorded only for custom tables, in the same{connector, location, version, version_kind}shape as a snapshot'sparents.source_tables.<alias>(Record provenance for every artifact under one MLOps artifact root #50; Custom SQL also recordsdependencies):connectoris the connector type (LOCAL_FILE,S3,GCS,SNOWFLAKE,DATABRICKS,BIGQUERY, ...).locationis where the rows were read from, in full: the absolute path for aFileConnector, the fulls3://orgs://URI (bucket and root included) for object stores, the fully qualified table name for warehouses, andsql:<sha256 of the query>for Custom SQL (never the SQL text).versionis 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 whatget_data_versionreports fortt/afterwards.version_kindsays 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 aslisting.from_source_tablewas taken withgraph.snapshot(record_sources=False),locationis 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 fromGraphSnapshot.record_sources(Record provenance for every artifact under one MLOps artifact root #50), then from the snapshot manifest'srecord_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.nulland a warning is logged.query,task_typeandgeneration_planrecord what produced the rows. The plan is the effective generation plan the table carries intoTrainer.fit.split_seedis the seed of a random split: theRandomSplitseed, or DiskGraph's default of 0. It isnullfor temporal and manual splits, as in this example.environmentandcreated_atfollow the Record provenance for every artifact under one MLOps artifact root #50 snapshot manifest.TrainingTable.manifestreturns aTrainingTableManifestdataclass built on Record provenance for every artifact under one MLOps artifact root #50's (for examplemanifest.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_idandTrainingTable.manifestexpose these values.manifestreturnsNonefor 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 ownmanifest.json(stage_counts,source_mode), and the privateTrainingTable._source_modereads it.Parquet directory exports to file destinations (any sink whose
holds_filesaccepts the path, #112) gain one file,_provenance.json, a copy of the staged manifest. It is written after the parts and before_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_SUCCESSis still written.Artifact root. With MLOps on (
mlops=ongenerate_training_table/from_source_table, or akumo.mlopsblock, #50), each table is also stored once under<root>/training_tables/<id>/through the same export. A second generation of the same id finds_SUCCESSthere and writes nothing; a failure warns. With MLOps off, nothing is stored.Plumbing:
ObjectStoreSinkBackend.write_filesgainssidecars: Mapping[str, bytes] = {}. kumo-coreexport_selected_fileswrites the sidecars into directory destinations, after the data and before_SUCCESS. This is not akumoapiprotocol change.Back-compat
This PR only records and exposes information. New reads and writes are best-effort: they warn and continue, and nothing new raises.
_provenance.jsonwritten. Rows, splits and the returnedTrainingTableare unchanged.parents.source_table. No extra calls: the version is the one staging already read._provenance.jsonis added before_SUCCESS. Re-exporting the same table to the same directory rewrites the identical sidecar.manifestreturnsNone.tt_idstill works, because it is read fromtt_ingest_spec.json. Exporting it writes no sidecar._KUMO_EXPORTand_SUCCESS._SUCCESSis still written.null. The load completes as before.versionisnull.record_sources=Falselocationis the table name only.record_sourceschoice is unknown (hand-builtGraphSnapshot, manifest missing or unreadable)locationis the table name only (connector + table name), rather than the full location.Known limitations
ObjectStoreSinkBackendsubclass that overrideswrite_fileswithout**kwargswould get aTypeErroron 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 withfrom_source_tablefrom that directory, fitted with the bundle-provenance PR (#56) on top. This run predates two later changes: theconnectorkey inparents.source_table, and takingversionfrom the staged read (at the time it was a separateget_data_versioncall; both gave the same value in this run). It was not re-run after them, after the removal ofrows/source, or after this revision (deterministic id, embedded snapshot,_provenance.json)._kumo_manifest.json(the sidecar's name then) and_SUCCESSpresent; the reloaded table recordedparents.source_table={"location": "s3://<bucket>/<prefix>/tt", "version": "c030a09e…"}, withversionequal toget_data_versionfor it; the bundle trained on it recorded the reloaded table id, its source as custom (recorded then ascustom, nowcustom_tt), the snapshot id, location and versionTesting Done
At the current head:
test_training_table_export.py(19),test_custom_source.py(20),test_custom_sql.py(11) andtest_predictive_query.py(67) pass. New in this revision:id(DiskGraphtt_ids differ), and the embedded snapshot equalssnapshot.manifest;kumo.mlopsblock stores the table once undertraining_tables/<id>/(a second generation rewrites nothing), and a block that does not export training tables stores nothing;parentsreads as a stub;load_from_connectorround-trips an export.New engine tests:
idequalstt_id,parentshas onlysnapshot, norowsorsource, andsource_modeispquery); exported parts byte-identical to the plain exporter; the sidecar equals the table manifest; aFileConnectorreload recordsLOCAL_FILE, the absolute path and the version staging read withversion_kindlisting, hassource_modecustom_tt, and has the same split counts;record_sources=Falserecords only the table name, one taken with the default records the full location, and one whose choice is unknown (norecord_sources, no manifest) records only the table name;sidecars.New kumo-core test: sidecars are written before
_SUCCESS, and a failing sidecar write warns while_SUCCESSis 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'stest_snapshot_manifest.py(shared helper).Run at the current head:
test_training_table_export.py,test_custom_source.pyandtest_snapshot_manifest.py(61 passed). The Custom SQL and predictive-query suites and kumo-coretest_artifact_export.pywere 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
🤖 Generated with Claude Code