feat(kernel): ingest pipeline — hashing, blob dedup, asset origin, batch materialization (#20) - #91
Merged
Merged
Conversation
…tch materialization (#20) `IngestService` is the one door that turns a registered source into rows: it hashes every item, stores the bytes once, records what the decoder made of them, and puts the result in a draft batch. It closes the last write in the kernel that had no service behind it — `_add_assets` is gone from the SDK example, which now registers a directory and ingests it like a real caller. - One `ingest(source_id, *, batch_id=None, batch_name=None)`, branching on `SourceKind`: registration needed two methods because its arguments differed, this does not — the source already carries the kind, the path and the rate. - Identity is content, origin is provenance. The same bytes in two sources are one blob and one asset, and the asset keeps the origin of the first sighting. - Four transactions with the decode outside all of them, and blobs written before any row: an out-of-process decoder inside a write transaction is how a single-writer SQLite store starts reporting "database is locked". - Failure splits by remedy: unsupported and corrupt items are reported per file and the run carries on; a missing ffmpeg fails the job and is re-raised. - First emitter of `IngestCompleted`, flipping the tripwire held since #13. Migration 8 (`FORMAT_VERSION` 8) gives `asset` its format and origin, links a run to its batch, and adds the two unique indexes both rules had been running without — `uq_asset_project_content_hash`, and the expression index `uq_source_project_kind_path_fps` that #18 named as owed. `asset` is altered rather than rebuilt (four cascading children, and pre-existing rows that were legitimate); `ingest_job` is rebuilt so its new key can match `create_all`.
JArmandoAnaya
added a commit
that referenced
this pull request
Aug 21, 2026
…tch materialization (#20) (#91) `IngestService` is the one door that turns a registered source into rows: it hashes every item, stores the bytes once, records what the decoder made of them, and puts the result in a draft batch. It closes the last write in the kernel that had no service behind it — `_add_assets` is gone from the SDK example, which now registers a directory and ingests it like a real caller. - One `ingest(source_id, *, batch_id=None, batch_name=None)`, branching on `SourceKind`: registration needed two methods because its arguments differed, this does not — the source already carries the kind, the path and the rate. - Identity is content, origin is provenance. The same bytes in two sources are one blob and one asset, and the asset keeps the origin of the first sighting. - Four transactions with the decode outside all of them, and blobs written before any row: an out-of-process decoder inside a write transaction is how a single-writer SQLite store starts reporting "database is locked". - Failure splits by remedy: unsupported and corrupt items are reported per file and the run carries on; a missing ffmpeg fails the job and is re-raised. - First emitter of `IngestCompleted`, flipping the tripwire held since #13. Migration 8 (`FORMAT_VERSION` 8) gives `asset` its format and origin, links a run to its batch, and adds the two unique indexes both rules had been running without — `uq_asset_project_content_hash`, and the expression index `uq_source_project_kind_path_fps` that #18 named as owed. `asset` is altered rather than rebuilt (four cascading children, and pre-existing rows that were legitimate); `ingest_job` is rebuilt so its new key can match `create_all`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #20.
IngestServiceis the one door that turns a registered source into rows: it hashes every item, stores the bytes once, records what the decoder made of them, and puts the result in a draft batch. It closes the last write in the kernel that had no service behind it —_add_assetsis gone fromexamples/sdk_end_to_end.py, which now writes its frames toincoming/, registers the directory as a source and ingests it like a real caller would.Acceptance criteria
source_idon every asset, plusframe_index/frame_timestampand aclip.mp4#frame=7uri for a decomposed clip.Design
ingest, not two. Registration splitregister_images/register_videobecause its arguments genuinely differed; here they do not — the source already carries the kind, the path and the extraction rate, so the branch is onSourceKind.Source.registered_atalready follows.IngestFailure, with the name and the reason kept apart and anIngestFailureKinda report can group on) and the run carries on. A missing ffmpeg fails the job and is re-raised — which is exactly whyMediaToolUnavailablesits outsideMediaError.FRAME_FORMATat the probed dimensions, and re-decoding would route our own encoder's output into an operator's report.thumbnail_hash(kernel: thumbnails cached in the blob store (needed by the M5 gallery) #21).Migration 8 —
FORMAT_VERSIONis now 8Adds
asset.format/source_id/frame_index/frame_timestamp, linksingest_jobto its batch, and puts indexes under the two uniqueness rules that had been running as service-level pre-checks with nothing beneath them:uq_asset_project_content_hash— "the same bytes are the same asset", claimed byAsset's docstring since M1.uq_source_project_kind_path_fps— the index kernel: SourceService — source registration with provenance metadata (origin, date, original framerate, parameters) #18 named as owed onceasset.source_idhad a target. Its fourth term iscoalesce(json_extract(video, '$.extraction_fps'), 0), because SQLite treats NULLs in a unique index as distinct and an image directory would otherwise never collide with itself.Two findings the fresh-versus-migrated test caught while writing it, both now documented:
checkfirstreports it absent and re-issues theCREATE. That one index usesCREATE UNIQUE INDEX IF NOT EXISTS, still compiled from the sharedIndexobject.ALTER— SQLite spells an added key inline whilecreate_allspells it as a table constraint, so the two paths emit different DDL.ingest_jobis rebuilt (no children, provably empty, counted) sobatch_idkeeps a real key;asset.source_idis added without one, becauseassethas four cascading children and rows that were legitimately already there.Duplicate pre-checks refuse a workspace either index could not accept, naming both counts, rather than letting an
IntegrityErrorescapeinitialize().Also
IngestCompletedhas its first emitter, flipping the tripwiretests/kernel/test_events.pyhas held since kernel: in-process EventBus + domain events (BatchApproved, ReleasePublished, IngestCompleted) #13 — narrowed there rather than deleted, so the annotation cycle still cannot quietly claim to be an ingest.BatchService._require_draftpromoted to publicrequire_draft(uow, batch_id), so a frozen target batch is refused before anything is decoded.docs/ingest.md;sources.md,persistence.md,events.md,examples.mdandmedia.mdupdated where they described this as future work.Checks
VERSIONstays0.0.1.dev0; no new dependency.