The audio node is the master for the audiobooks. UmbraTrans holds the metadata needed to reason about a file and a reference to where it is; it does not hold the bytes. DECISION-6 (object storage) stays unresolved, and this is what lets the system be useful before it is resolved rather than after. POST /api/compute/artifacts takes a manifest against a project and declares each entry: content hash, size, media type, kind, and a storage_uri naming where the file actually is. GET /api/compute/artifacts lists the catalogue. There were no artifact routes at all before this - the tables existed since migration 0003 and nothing had ever written to them, so "the controller has the artifacts to show a job is complete" had nothing behind it. Every declared row lands integrity='unverified', and that is the point rather than a caveat. Ingesting is a claim: a node reporting the hash of a file is a statement about bytes nothing here has read, because there is no object storage to read them from. A design where submitting a manifest marks the catalogue verified would make the first ingest the most reassuring and least true event in the system's life. verified_at is set by reading the bytes back and hashing them, and nothing in this path does that. storage_node_id is part of the reference rather than an afterthought, because a bare path means nothing six months later: /audio/book.mp3 is on whichever machine happened to serve it that day. Idempotent on (project_id, content_hash), so the same manifest can be submitted twice, or by two operators who did not talk to each other, without the catalogue doubling - and a partly-present manifest reports what was new rather than just "done". An operator who submitted 300 entries and was told "done" would not know two were malformed and dropped, so malformed entries are named in the response instead. The listing is scoped by organization on every call. artifacts is platform-level, as every table a node serves from is, so a missing predicate there is a cross-tenant read rather than a style issue. A project in another tenant's schema is refused as *not found*, not as *not permitted*, because a refusal that says "this exists but is not yours" answers the question the caller was actually asking. Migration 0007 applied to the deployment. Mutation-verified: marking an ingest verified fails 1, dropping the idempotency check fails 2, and dropping the organization predicate from the listing fails 7. 704 passed, 4 skipped.
51 lines
2.9 KiB
SQL
51 lines
2.9 KiB
SQL
-- 0007: artifacts that live somewhere else.
|
|
--
|
|
-- The audio node is the master for the audiobooks. UmbraTrans holds the metadata needed
|
|
-- to reason about a file and a reference to where it actually is; it does not hold the
|
|
-- bytes. DECISION-6 (object storage) stays unresolved, and this is what lets the system
|
|
-- be useful before it is resolved rather than after.
|
|
--
|
|
-- `storage_key` was already NOT NULL and already carried a path, so nothing here is
|
|
-- required to register a file - these columns make the *reference* explicit and checkable
|
|
-- rather than implicit in a string.
|
|
|
|
ALTER TABLE public.artifacts
|
|
ADD COLUMN IF NOT EXISTS storage_uri text,
|
|
ADD COLUMN IF NOT EXISTS storage_node_id uuid REFERENCES public.compute_nodes (id) ON DELETE SET NULL,
|
|
ADD COLUMN IF NOT EXISTS integrity text NOT NULL DEFAULT 'unverified',
|
|
ADD COLUMN IF NOT EXISTS verified_at timestamptz;
|
|
|
|
-- The audio node is the master, so the reference has to say which machine and how. A bare
|
|
-- path means nothing to anybody reading this row six months later: `/audio/book.mp3` is
|
|
-- on whichever node happened to serve it that day.
|
|
COMMENT ON COLUMN public.artifacts.storage_uri IS
|
|
'A URI naming where the bytes actually are, e.g. file:///media/audiobooks/… or '
|
|
's3://…. Deliberately not assumed to be retrievable by this system: with no object '
|
|
'storage behind it, nothing here fetches from it, and integrity is how a reader can '
|
|
'tell a checked reference from an unchecked one.';
|
|
|
|
COMMENT ON COLUMN public.artifacts.integrity IS
|
|
'unverified - a node reported it and nobody has checked the bytes against '
|
|
'content_hash. verified - the bytes were read back and hashed, and verified_at says '
|
|
'when. The default is unverified because a node reporting a hash is a claim, and '
|
|
'treating the claim as the check is the mistake this column exists to prevent.';
|
|
|
|
-- Re-ingesting a library must not duplicate it. Content-addressed, so the natural key is
|
|
-- the hash within a project: the same bytes under the same project are the same artifact
|
|
-- however many times they are declared.
|
|
--
|
|
-- Partial on storage_uri rather than unique outright, so two projects may legitimately
|
|
-- reference the same file - which is what happens when a shared library is referenced by
|
|
-- more than one book - while one project cannot end up with two rows for one file.
|
|
CREATE UNIQUE INDEX IF NOT EXISTS artifacts_once_per_project
|
|
ON public.artifacts (project_id, content_hash)
|
|
WHERE storage_uri IS NOT NULL;
|
|
|
|
CREATE INDEX IF NOT EXISTS artifacts_by_node
|
|
ON public.artifacts (storage_node_id)
|
|
WHERE storage_node_id IS NOT NULL;
|
|
|
|
-- Listing a project's catalogue is the only read the console and evaluation tooling do,
|
|
-- and without this it is a sequential scan of every artifact ever ingested.
|
|
CREATE INDEX IF NOT EXISTS artifacts_by_project
|
|
ON public.artifacts (project_id, created_at); |