diff --git a/modules/mesh-catalog/index.ts b/modules/mesh-catalog/index.ts index 7d7f29d..20c64f0 100644 --- a/modules/mesh-catalog/index.ts +++ b/modules/mesh-catalog/index.ts @@ -10,7 +10,7 @@ // diamond need no special handling, and nothing holds a plan. import { on, emit } from "@novox/mesh-sdk/events"; -import { Graph, type BuiltAgainst } from "./store.js"; +import { Graph, type Made } from "./store.js"; const graph = Graph.fromEnv(); @@ -27,8 +27,15 @@ interface Built { path?: string; ref?: string; manifest?: unknown; - /** Every artifact this was built against, so the edge is derived rather than declared. */ - against?: { module: string; commit: string }[]; + /** What this build published, each pinned as anything else would name it. */ + made?: Made[]; + /** + * Every artifact this was built on top of, so the edge is derived rather than declared. + * + * References, not module names: the builder sees a pinned image and cannot see which module + * produced it. Turning that into an edge between module-versions is this module's job. + */ + against?: string[]; } await on("module.builder.built", async (event) => { @@ -40,13 +47,6 @@ await on("module.builder.built", async (event) => { return; } - const against: BuiltAgainst[] = (body.against ?? []).map((a) => ({ - module: body.module as string, - commit: body.commit as string, - againstModule: a.module, - againstCommit: a.commit, - })); - const { upgraded, previous } = await graph.register({ module: body.module, commit: body.commit, @@ -54,7 +54,7 @@ await on("module.builder.built", async (event) => { path: body.path ?? "", ref: body.ref ?? "", manifest: body.manifest ?? {}, - }, against); + }, body.made ?? [], body.against ?? []); await emit("module.mesh-catalog.registered", { module: body.module, commit: body.commit, upgraded, diff --git a/modules/mesh-catalog/store.ts b/modules/mesh-catalog/store.ts index 7863acd..b929204 100644 --- a/modules/mesh-catalog/store.ts +++ b/modules/mesh-catalog/store.ts @@ -27,15 +27,25 @@ export interface ModuleVersion { manifest: unknown; } -/** An edge: this module-version was built against that one. Derived, never declared (ADR 0009). */ -export interface BuiltAgainst { - module: string; - commit: string; - againstModule: string; - againstCommit: string; +/** An artifact a module-version produced, as the builder published it. */ +export interface Made { + name: string; + kind: string; + /** How anything else names it — for an image, a registry reference pinned to a digest. */ + reference: string; } -/** A module whose artifacts were built against something that is no longer current. */ +/** + * A module whose artifacts were built against something that is no longer current. + * + * **An edge is an artifact reference, not a module name.** The builder can see exactly what it + * built on top of — a pinned image — and cannot see which module produced it: that is a fact about + * the graph, and the graph is here. So the reference is what is stored, and resolving it to a + * module-version is a join against what each version says it made. An edge to an artifact no + * module here produced is kept rather than dropped; it resolves by itself the day that module is + * registered, which is the ordinary case while a mesh is still being filled in. + */ + export interface Stale { module: string; commit: string; @@ -64,15 +74,39 @@ CREATE TABLE IF NOT EXISTS module_current ( moved timestamptz NOT NULL DEFAULT now() ); -CREATE TABLE IF NOT EXISTS built_against ( - module text NOT NULL, - commit_sha text NOT NULL, - against_module text NOT NULL, - against_commit text NOT NULL, - PRIMARY KEY (module, commit_sha, against_module, against_commit) +-- What a module-version published. This is what makes a build edge resolvable: an edge names an +-- artifact, and this says which version put that artifact there. +CREATE TABLE IF NOT EXISTS module_artifact ( + module text NOT NULL, + commit_sha text NOT NULL, + name text NOT NULL, + kind text NOT NULL DEFAULT '', + reference text NOT NULL, + PRIMARY KEY (module, commit_sha, name) ); -CREATE INDEX IF NOT EXISTS built_against_target ON built_against (against_module); +CREATE INDEX IF NOT EXISTS module_artifact_reference ON module_artifact (reference); + +CREATE TABLE IF NOT EXISTS built_against ( + module text NOT NULL, + commit_sha text NOT NULL, + against_reference text NOT NULL, + PRIMARY KEY (module, commit_sha, against_reference) +); + +CREATE INDEX IF NOT EXISTS built_against_target ON built_against (against_reference); +`; + +// The graph first keyed its edges on a module and a commit, which the builder does not know and +// never sent — so every build that had been built against anything was rejected, and the only +// versions that registered were the ones built against nothing. No edge was ever stored, so there +// is nothing to carry across: the old columns are dropped and the new one added. +const MIGRATE = ` +ALTER TABLE built_against ADD COLUMN IF NOT EXISTS against_reference text; +DELETE FROM built_against WHERE against_reference IS NULL; +ALTER TABLE built_against DROP COLUMN IF EXISTS against_module; +ALTER TABLE built_against DROP COLUMN IF EXISTS against_commit; +ALTER TABLE built_against ALTER COLUMN against_reference SET NOT NULL; `; export class Graph { @@ -91,6 +125,7 @@ export class Graph { async migrate(): Promise { await this.pool.query(DDL); + await this.pool.query(MIGRATE); } /** @@ -100,7 +135,7 @@ export class Graph { * rebuild producing the commit already current changes nothing, and announcing it as an upgrade * would ripple outward forever through modules that did not change (ADR 0072). */ - async register(version: ModuleVersion, against: BuiltAgainst[]): Promise<{ upgraded: boolean; previous: string | null }> { + async register(version: ModuleVersion, made: Made[], against: string[]): Promise<{ upgraded: boolean; previous: string | null }> { const client = await this.pool.connect(); try { await client.query("BEGIN"); @@ -114,15 +149,25 @@ export class Graph { JSON.stringify(version.manifest ?? {})], ); - // The edges are replaced rather than added to: they describe this build, and a previous - // build of the same commit that saw different dependencies was wrong about one of them. + // Both are replaced rather than added to: they describe this build, and a previous build of + // the same commit that saw different artifacts was wrong about one of them. + await client.query(`DELETE FROM module_artifact WHERE module = $1 AND commit_sha = $2`, + [version.module, version.commit]); + for (const a of made) { + await client.query( + `INSERT INTO module_artifact (module, commit_sha, name, kind, reference) + VALUES ($1,$2,$3,$4,$5) ON CONFLICT (module, commit_sha, name) DO UPDATE SET + kind = excluded.kind, reference = excluded.reference`, + [version.module, version.commit, a.name, a.kind, a.reference]); + } + await client.query(`DELETE FROM built_against WHERE module = $1 AND commit_sha = $2`, [version.module, version.commit]); - for (const e of against) { + for (const reference of against) { await client.query( - `INSERT INTO built_against (module, commit_sha, against_module, against_commit) - VALUES ($1,$2,$3,$4) ON CONFLICT DO NOTHING`, - [version.module, version.commit, e.againstModule, e.againstCommit]); + `INSERT INTO built_against (module, commit_sha, against_reference) + VALUES ($1,$2,$3) ON CONFLICT DO NOTHING`, + [version.module, version.commit, reference]); } const was = await client.query( @@ -175,13 +220,18 @@ export class Graph { }; } - /** What was built against this module — the modules a change to it reaches. */ + /** + * What was built against this module — the modules a change to it reaches. + * + * Resolved through the artifacts each version published, because that is what an edge names. + */ async dependents(module: string): Promise<{ module: string; commit: string; againstCommit: string }[]> { const { rows } = await this.pool.query( - `SELECT b.module, b.commit_sha, b.against_commit + `SELECT b.module, b.commit_sha, a.commit_sha AS against_commit FROM built_against b JOIN module_current c ON c.module = b.module AND c.commit_sha = b.commit_sha - WHERE b.against_module = $1 + JOIN module_artifact a ON a.reference = b.against_reference + WHERE a.module = $1 ORDER BY b.module`, [module]); return rows.map((r) => ({ module: r.module as string, commit: r.commit_sha as string, @@ -199,11 +249,13 @@ export class Graph { */ async stale(): Promise { const { rows } = await this.pool.query( - `SELECT b.module, b.commit_sha, b.against_module, b.against_commit, c2.commit_sha AS now_at + `SELECT b.module, b.commit_sha, a.module AS against_module, a.commit_sha AS against_commit, + c2.commit_sha AS now_at FROM built_against b JOIN module_current c1 ON c1.module = b.module AND c1.commit_sha = b.commit_sha - JOIN module_current c2 ON c2.module = b.against_module - WHERE c2.commit_sha <> b.against_commit + JOIN module_artifact a ON a.reference = b.against_reference + JOIN module_current c2 ON c2.module = a.module + WHERE c2.commit_sha <> a.commit_sha ORDER BY b.module`); const by = new Map(); for (const r of rows) {