From c92ca676a17387ffe2e3f21c17e8df71367f43e2 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 2 Oct 2026 00:04:54 +0800 Subject: [PATCH 01/28] Archive v0.9: add audited candidate promotion events --- .../0011_catalog_candidate_promotion.sql | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 packages/database/migrations/0011_catalog_candidate_promotion.sql diff --git a/packages/database/migrations/0011_catalog_candidate_promotion.sql b/packages/database/migrations/0011_catalog_candidate_promotion.sql new file mode 100644 index 0000000..d264eaf --- /dev/null +++ b/packages/database/migrations/0011_catalog_candidate_promotion.sql @@ -0,0 +1,69 @@ +BEGIN; + +SET search_path TO modelapse, public; + +CREATE TABLE catalog_promotion_events ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + candidate_id uuid NOT NULL REFERENCES catalog_discovery_candidates(id), + model_id uuid NOT NULL REFERENCES models(id), + source_record_id uuid NOT NULL REFERENCES source_records(id), + canonical_slug text NOT NULL CHECK (canonical_slug = btrim(canonical_slug) AND canonical_slug <> ''), + marketing_name text NOT NULL CHECK (marketing_name = btrim(marketing_name) AND marketing_name <> ''), + model_status text NOT NULL CHECK (model_status IN ('preview', 'active')), + actor text NOT NULL CHECK (btrim(actor) <> ''), + promoted_at timestamptz NOT NULL DEFAULT now(), + metadata jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (candidate_id) +); + +CREATE OR REPLACE FUNCTION validate_catalog_promotion_event() +RETURNS trigger LANGUAGE plpgsql AS $catalog_promotion$ +DECLARE + candidate_provider uuid; + candidate_status text; + model_provider uuid; + model_source uuid; +BEGIN + SELECT provider_id, status + INTO candidate_provider, candidate_status + FROM catalog_discovery_candidates + WHERE id = NEW.candidate_id; + + IF candidate_provider IS NULL THEN + RAISE EXCEPTION 'catalog promotion references a missing candidate'; + END IF; + + IF candidate_status <> 'matched' THEN + RAISE EXCEPTION 'catalog promotion candidate must be matched before event insertion'; + END IF; + + SELECT provider_id, canonical_source_id + INTO model_provider, model_source + FROM models + WHERE id = NEW.model_id; + + IF model_provider IS NULL OR model_provider <> candidate_provider THEN + RAISE EXCEPTION 'catalog promotion model provider mismatch'; + END IF; + + IF model_source IS DISTINCT FROM NEW.source_record_id THEN + RAISE EXCEPTION 'catalog promotion source must be the canonical model source'; + END IF; + + RETURN NEW; +END; +$catalog_promotion$; + +CREATE TRIGGER catalog_promotion_events_validate +BEFORE INSERT ON catalog_promotion_events +FOR EACH ROW EXECUTE FUNCTION validate_catalog_promotion_event(); + +CREATE TRIGGER catalog_promotion_events_append_only +BEFORE UPDATE OR DELETE ON catalog_promotion_events +FOR EACH ROW EXECUTE FUNCTION prevent_append_only_mutation(); + +CREATE INDEX catalog_promotion_events_model_promoted_idx + ON catalog_promotion_events (model_id, promoted_at DESC); + +COMMIT; From 497e0e7e367cdb40dbaacc50843273a69249f43c Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 2 Oct 2026 00:05:12 +0800 Subject: [PATCH 02/28] Archive v0.9: add explicit candidate promotion workflow --- .../catalog-admin/src/catalog-discovery.ts | 219 +++++++++++++++++- 1 file changed, 218 insertions(+), 1 deletion(-) diff --git a/packages/catalog-admin/src/catalog-discovery.ts b/packages/catalog-admin/src/catalog-discovery.ts index f936d07..ea71347 100644 --- a/packages/catalog-admin/src/catalog-discovery.ts +++ b/packages/catalog-admin/src/catalog-discovery.ts @@ -1,5 +1,6 @@ import { Pool, type PoolClient } from "pg"; import type { ObservedRemoteModel } from "./catalog-adapter.js"; +import { PgModelCatalogAdmin } from "./model-catalog.js"; export type CatalogDiscoveryStatus = | "discovered" @@ -40,6 +41,12 @@ export interface CatalogDiscoveryCandidate { readonly retrievedAt: string; readonly contentSha256: string | null; }; + readonly promotion: { + readonly id: string; + readonly promotedAt: string; + readonly actor: string; + readonly modelId: string; + } | null; readonly latestDecision: { readonly id: string; readonly action: CatalogReconciliationAction; @@ -306,6 +313,10 @@ export class PgCatalogDiscovery { decision_decided_at: Date | null; decision_actor: string | null; decision_note: string | null; + promotion_id: string | null; + promotion_promoted_at: Date | null; + promotion_actor: string | null; + promotion_model_id: string | null; }>( `SELECT candidate.id, @@ -332,7 +343,11 @@ export class PgCatalogDiscovery { decision.action AS decision_action, decision.decided_at AS decision_decided_at, decision.actor AS decision_actor, - decision.note AS decision_note + decision.note AS decision_note, + promotion.id AS promotion_id, + promotion.promoted_at AS promotion_promoted_at, + promotion.actor AS promotion_actor, + promotion.model_id AS promotion_model_id FROM modelapse.catalog_discovery_candidates candidate JOIN modelapse.providers provider ON provider.id = candidate.provider_id @@ -340,6 +355,8 @@ export class PgCatalogDiscovery { ON source.id = candidate.last_source_record_id LEFT JOIN modelapse.models model ON model.id = candidate.resolved_model_id + LEFT JOIN modelapse.catalog_promotion_events promotion + ON promotion.candidate_id = candidate.id LEFT JOIN LATERAL ( SELECT event.* FROM modelapse.catalog_reconciliation_events event @@ -390,6 +407,18 @@ export class PgCatalogDiscovery { retrievedAt: row.source_retrieved_at.toISOString(), contentSha256: row.source_content_sha256, }, + promotion: + row.promotion_id && + row.promotion_promoted_at && + row.promotion_actor && + row.promotion_model_id + ? { + id: row.promotion_id, + promotedAt: row.promotion_promoted_at.toISOString(), + actor: row.promotion_actor, + modelId: row.promotion_model_id, + } + : null, latestDecision: row.decision_id && row.decision_action && @@ -406,6 +435,194 @@ export class PgCatalogDiscovery { })); } + async promoteCandidate(input: { + readonly candidateId: string; + readonly canonicalSlug: string; + readonly marketingName: string; + readonly status?: "preview" | "active"; + readonly actor: string; + readonly note?: string; + }): Promise<{ + readonly candidateId: string; + readonly modelId: string; + readonly promotionEventId: string; + readonly reconciliationEventId: string; + }> { + const candidateId = nonEmpty(input.candidateId, "candidateId"); + const canonicalSlug = nonEmpty(input.canonicalSlug, "canonicalSlug"); + const marketingName = nonEmpty(input.marketingName, "marketingName"); + const actor = nonEmpty(input.actor, "actor"); + const note = input.note?.trim() || null; + const status = input.status ?? "active"; + + if (!/^[a-z0-9][a-z0-9-]*$/.test(canonicalSlug)) { + throw new Error("canonicalSlug must use lowercase letters, digits, and hyphens"); + } + + const client = await this.pool.connect(); + try { + await client.query("BEGIN"); + await client.query( + "SELECT pg_advisory_xact_lock(hashtext($1))", + ["modelapse:catalog-promotion:" + candidateId], + ); + + const candidateResult = await client.query<{ + id: string; + provider_id: string; + provider_slug: string; + remote_model_id: string; + status: CatalogDiscoveryStatus; + last_source_record_id: string; + source_type: string; + source_url: string | null; + source_title: string | null; + }>( + `SELECT + candidate.id, + candidate.provider_id, + provider.slug AS provider_slug, + candidate.remote_model_id, + candidate.status, + candidate.last_source_record_id, + source.source_type, + source.url AS source_url, + source.title AS source_title + FROM modelapse.catalog_discovery_candidates candidate + JOIN modelapse.providers provider ON provider.id = candidate.provider_id + JOIN modelapse.source_records source + ON source.id = candidate.last_source_record_id + WHERE candidate.id = $1 + FOR UPDATE OF candidate`, + [candidateId], + ); + const candidate = candidateResult.rows[0]; + if (!candidate) throw new Error("Catalog discovery candidate not found"); + if (candidate.status !== "promotion_ready") { + throw new Error("Catalog discovery candidate must be promotion_ready"); + } + if (!candidate.source_url || !candidate.source_title) { + throw new Error("Promotion requires a URL-backed first-party source"); + } + + const priorPromotion = await client.query<{ id: string }>( + `SELECT id + FROM modelapse.catalog_promotion_events + WHERE candidate_id = $1`, + [candidateId], + ); + if (priorPromotion.rows[0]) { + throw new Error("Catalog discovery candidate has already been promoted"); + } + + await client.query("COMMIT"); + + const modelAdmin = new PgModelCatalogAdmin(this.pool); + const registration = await modelAdmin.registerFirstPartyModel({ + providerSlug: candidate.provider_slug, + canonicalSlug, + marketingName, + apiModelId: candidate.remote_model_id, + status, + sourceUrl: candidate.source_url, + sourceTitle: candidate.source_title, + sourceType: candidate.source_type, + sourceRecordId: candidate.last_source_record_id, + }); + + await client.query("BEGIN"); + await client.query( + "SELECT pg_advisory_xact_lock(hashtext($1))", + ["modelapse:catalog-promotion:" + candidateId], + ); + const locked = await lockCandidate(client, candidateId); + if (locked.status !== "promotion_ready") { + throw new Error("Catalog discovery candidate changed during promotion"); + } + + const reconciliation = await client.query<{ id: string }>( + `INSERT INTO modelapse.catalog_reconciliation_events + (candidate_id, action, resolved_model_id, actor, note, metadata) + VALUES ($1, 'match_existing', $2, $3, $4, $5::jsonb) + RETURNING id`, + [ + candidateId, + registration.modelId, + actor, + note, + JSON.stringify({ + previousStatus: locked.status, + remoteModelId: locked.remote_model_id, + promotion: true, + }), + ], + ); + const reconciliationEventId = reconciliation.rows[0]?.id; + if (!reconciliationEventId) { + throw new Error("Promotion reconciliation event insert failed"); + } + + await client.query( + `UPDATE modelapse.catalog_discovery_candidates + SET status = 'matched', + resolved_model_id = $2, + resolved_at = now(), + updated_at = now() + WHERE id = $1`, + [candidateId, registration.modelId], + ); + + const promotion = await client.query<{ id: string }>( + `INSERT INTO modelapse.catalog_promotion_events + ( + candidate_id, + model_id, + source_record_id, + canonical_slug, + marketing_name, + model_status, + actor, + metadata + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb) + RETURNING id`, + [ + candidateId, + registration.modelId, + registration.sourceId, + canonicalSlug, + marketingName, + status, + actor, + JSON.stringify({ + remoteModelId: candidate.remote_model_id, + sourceType: candidate.source_type, + note, + }), + ], + ); + const promotionEventId = promotion.rows[0]?.id; + if (!promotionEventId) throw new Error("Catalog promotion event insert failed"); + + await client.query("COMMIT"); + return { + candidateId, + modelId: registration.modelId, + promotionEventId, + reconciliationEventId, + }; + } catch (error) { + if (!client.released) { + try { + await client.query("ROLLBACK"); + } catch {} + } + throw error; + } finally { + client.release(); + } + } + async reconcileCandidate(input: { readonly candidateId: string; readonly action: CatalogReconciliationAction; From e45b7f944a1d0bc52559815363d12c4b04dba5b0 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 2 Oct 2026 00:05:23 +0800 Subject: [PATCH 03/28] Archive v0.9: register promoted models from existing provenance --- packages/catalog-admin/src/model-catalog.ts | 42 ++++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/packages/catalog-admin/src/model-catalog.ts b/packages/catalog-admin/src/model-catalog.ts index 51ea817..0f58138 100644 --- a/packages/catalog-admin/src/model-catalog.ts +++ b/packages/catalog-admin/src/model-catalog.ts @@ -118,6 +118,8 @@ export class PgModelCatalogAdmin { readonly status?: "preview" | "active"; readonly sourceUrl: string; readonly sourceTitle: string; + readonly sourceType?: string; + readonly sourceRecordId?: string; }): Promise { const providerSlug = input.providerSlug.trim(); const canonicalSlug = input.canonicalSlug.trim(); @@ -126,6 +128,8 @@ export class PgModelCatalogAdmin { const sourceUrl = input.sourceUrl.trim(); const sourceTitle = input.sourceTitle.trim(); const status = input.status ?? "active"; + const sourceType = input.sourceType?.trim() || "provider_docs"; + const sourceRecordId = input.sourceRecordId?.trim() || null; if ( !providerSlug || @@ -146,11 +150,36 @@ export class PgModelCatalogAdmin { ["modelapse:model:" + providerSlug + ":" + canonicalSlug], ); - const sourceId = await ensureSource(client, { - sourceType: "provider_docs", - url: sourceUrl, - title: sourceTitle, - }); + let sourceId = sourceRecordId; + if (sourceId) { + const sourceResult = await client.query<{ + source_type: string; + url: string | null; + title: string | null; + }>( + `SELECT source_type, url, title + FROM modelapse.source_records + WHERE id = $1`, + [sourceId], + ); + const source = sourceResult.rows[0]; + if ( + !source || + source.source_type !== sourceType || + source.url !== sourceUrl || + source.title !== sourceTitle + ) { + throw new Error( + "Existing source record does not match first-party model registration", + ); + } + } else { + sourceId = await ensureSource(client, { + sourceType, + url: sourceUrl, + title: sourceTitle, + }); + } const endpoint = await client.query<{ provider_id: string; @@ -301,7 +330,7 @@ export class PgModelCatalogAdmin { confidence, raw_observation ) - VALUES ($1, $2, now(), 'provider_docs', $3, 1.0, $4::jsonb)`, + VALUES ($1, $2, now(), $5, $3, 1.0, $4::jsonb)`, [ aliasId, model.id, @@ -311,6 +340,7 @@ export class PgModelCatalogAdmin { endpointId, registration: "catalog-admin", }), + sourceType, ], ); } From 3971d015711cda5a06c09adc189179c6d8c7d85c Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 2 Oct 2026 00:05:31 +0800 Subject: [PATCH 04/28] Archive v0.9: require provider API evidence for promotion --- .../catalog-admin/src/catalog-discovery.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/catalog-admin/src/catalog-discovery.ts b/packages/catalog-admin/src/catalog-discovery.ts index ea71347..4c9e3e3 100644 --- a/packages/catalog-admin/src/catalog-discovery.ts +++ b/packages/catalog-admin/src/catalog-discovery.ts @@ -501,6 +501,11 @@ export class PgCatalogDiscovery { if (candidate.status !== "promotion_ready") { throw new Error("Catalog discovery candidate must be promotion_ready"); } + if (candidate.source_type !== "provider_api") { + throw new Error( + "Promotion requires a first-party provider_api model-list observation", + ); + } if (!candidate.source_url || !candidate.source_title) { throw new Error("Promotion requires a URL-backed first-party source"); } @@ -518,7 +523,9 @@ export class PgCatalogDiscovery { await client.query("COMMIT"); const modelAdmin = new PgModelCatalogAdmin(this.pool); - const registration = await modelAdmin.registerFirstPartyModel({ + let registration; + try { + registration = await modelAdmin.registerFirstPartyModel({ providerSlug: candidate.provider_slug, canonicalSlug, marketingName, @@ -528,7 +535,10 @@ export class PgCatalogDiscovery { sourceTitle: candidate.source_title, sourceType: candidate.source_type, sourceRecordId: candidate.last_source_record_id, - }); + }); + } catch (error) { + throw error; + } await client.query("BEGIN"); await client.query( @@ -612,11 +622,9 @@ export class PgCatalogDiscovery { reconciliationEventId, }; } catch (error) { - if (!client.released) { - try { - await client.query("ROLLBACK"); - } catch {} - } + try { + await client.query("ROLLBACK"); + } catch {} throw error; } finally { client.release(); From 676ffb8ea2ff917d91fc43d68314f2ec15a41418 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 2 Oct 2026 00:05:48 +0800 Subject: [PATCH 05/28] Archive v0.9: expose controlled discovery inbox API --- apps/api/src/app.ts | 163 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 03b22ac..4bdadc3 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -13,6 +13,10 @@ import type { RunRepository, RunView, } from "@modelapse/persistence"; +import type { + CatalogDiscoveryStatus, + PgCatalogDiscovery, +} from "@modelapse/catalog-admin"; const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; @@ -24,6 +28,11 @@ type ControlPlanner = Pick< PgRunPlanner, "ping" | "listModels" | "listTests" | "plan" >; +type CatalogDiscoveryRepository = Pick< + PgCatalogDiscovery, + "listCandidates" | "reconcileCandidate" | "promoteCandidate" +>; + type ArchiveRepository = Pick< PgArchiveRepository, | "ping" @@ -43,6 +52,7 @@ export interface AppDependencies { readonly jobs?: ControlQueue; readonly planner?: ControlPlanner; readonly archive?: ArchiveRepository; + readonly catalogDiscovery?: CatalogDiscoveryRepository; readonly controlToken?: string; } @@ -372,6 +382,159 @@ export function createApp(deps: AppDependencies) { return c.json({ run: publicRun(run) }); }); + app.get("/v1/control/catalog/discoveries", async (c) => { + if (!deps.catalogDiscovery || !controlToken) { + return c.json({ error: "control_plane_disabled" }, 503); + } + const authIssue = controlAuthIssue(c.req.header("authorization"), controlToken); + if (authIssue) return c.json(controlAuthError(authIssue), 401); + + const provider = c.req.query("provider"); + const status = c.req.query("status") as CatalogDiscoveryStatus | undefined; + const rawLimit = c.req.query("limit"); + if (provider && !PROVIDER_SLUG_RE.test(provider)) { + return c.json({ error: "invalid_provider" }, 400); + } + if ( + status && + !["discovered", "matched", "ignored", "promotion_ready"].includes(status) + ) { + return c.json({ error: "invalid_discovery_status" }, 400); + } + const limit = rawLimit === undefined ? undefined : Number(rawLimit); + if ( + limit !== undefined && + (!Number.isInteger(limit) || limit < 1 || limit > 200) + ) { + return c.json({ error: "invalid_limit" }, 400); + } + + return c.json({ + candidates: await deps.catalogDiscovery.listCandidates({ + ...(provider ? { providerSlug: provider } : {}), + ...(status ? { status } : {}), + ...(limit !== undefined ? { limit } : {}), + }), + }); + }); + + app.post("/v1/control/catalog/discoveries/:candidateId/reconcile", async (c) => { + if (!deps.catalogDiscovery || !controlToken) { + return c.json({ error: "control_plane_disabled" }, 503); + } + const authIssue = controlAuthIssue(c.req.header("authorization"), controlToken); + if (authIssue) return c.json(controlAuthError(authIssue), 401); + + const candidateId = c.req.param("candidateId"); + if (!UUID_RE.test(candidateId)) { + return c.json({ error: "invalid_candidate_id" }, 400); + } + let raw: unknown; + try { + raw = await c.req.json(); + } catch { + return c.json({ error: "invalid_json" }, 400); + } + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + return c.json({ error: "invalid_reconciliation" }, 400); + } + const body = raw as Record; + const action = body.action; + const actor = body.actor; + const resolvedModelId = body.resolvedModelId; + const note = body.note; + if ( + typeof action !== "string" || + !["match_existing", "ignore", "mark_promotion_ready", "reopen"].includes(action) || + typeof actor !== "string" || + !actor.trim() || + (resolvedModelId !== undefined && + (typeof resolvedModelId !== "string" || !UUID_RE.test(resolvedModelId))) || + (note !== undefined && typeof note !== "string") + ) { + return c.json({ error: "invalid_reconciliation" }, 400); + } + + try { + const result = await deps.catalogDiscovery.reconcileCandidate({ + candidateId, + action: action as "match_existing" | "ignore" | "mark_promotion_ready" | "reopen", + actor, + ...(typeof resolvedModelId === "string" ? { resolvedModelId } : {}), + ...(typeof note === "string" ? { note } : {}), + }); + return c.json(result); + } catch (error) { + return c.json( + { + error: "reconciliation_rejected", + message: error instanceof Error ? error.message : "Reconciliation rejected", + }, + 409, + ); + } + }); + + app.post("/v1/control/catalog/discoveries/:candidateId/promote", async (c) => { + if (!deps.catalogDiscovery || !controlToken) { + return c.json({ error: "control_plane_disabled" }, 503); + } + const authIssue = controlAuthIssue(c.req.header("authorization"), controlToken); + if (authIssue) return c.json(controlAuthError(authIssue), 401); + + const candidateId = c.req.param("candidateId"); + if (!UUID_RE.test(candidateId)) { + return c.json({ error: "invalid_candidate_id" }, 400); + } + let raw: unknown; + try { + raw = await c.req.json(); + } catch { + return c.json({ error: "invalid_json" }, 400); + } + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + return c.json({ error: "invalid_promotion" }, 400); + } + const body = raw as Record; + const canonicalSlug = body.canonicalSlug; + const marketingName = body.marketingName; + const status = body.status; + const actor = body.actor; + const note = body.note; + if ( + typeof canonicalSlug !== "string" || + !PROVIDER_SLUG_RE.test(canonicalSlug) || + typeof marketingName !== "string" || + !marketingName.trim() || + (status !== undefined && status !== "preview" && status !== "active") || + typeof actor !== "string" || + !actor.trim() || + (note !== undefined && typeof note !== "string") + ) { + return c.json({ error: "invalid_promotion" }, 400); + } + + try { + const result = await deps.catalogDiscovery.promoteCandidate({ + candidateId, + canonicalSlug, + marketingName, + ...(status === "preview" || status === "active" ? { status } : {}), + actor, + ...(typeof note === "string" ? { note } : {}), + }); + return c.json(result, 201); + } catch (error) { + return c.json( + { + error: "promotion_rejected", + message: error instanceof Error ? error.message : "Promotion rejected", + }, + 409, + ); + } + }); + app.get("/v1/control/catalog/models", async (c) => { if (!deps.planner || !controlToken) { return c.json({ error: "control_plane_disabled" }, 503); From f3a11110278745b031774d793246f44195ab667f Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 2 Oct 2026 00:05:55 +0800 Subject: [PATCH 06/28] Archive v0.9: add catalog admin API dependency --- apps/api/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/api/package.json b/apps/api/package.json index 32706bd..88a1fe3 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -13,7 +13,8 @@ "@hono/node-server": "2.1.1", "@modelapse/control-plane": "*", "@modelapse/persistence": "*", - "hono": "4.13.8" + "hono": "4.13.8", + "@modelapse/catalog-admin": "*" }, "devDependencies": { "vitest": "3.0.8" From b2a635d5c0364e7c22573725b1559dcb47eb7102 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 2 Oct 2026 00:05:58 +0800 Subject: [PATCH 07/28] Archive v0.9: wire catalog discovery into API runtime --- apps/api/src/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 5b8e6be..958330f 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,4 +1,5 @@ import { serve } from "@hono/node-server"; +import { PgCatalogDiscovery } from "@modelapse/catalog-admin"; import { PgRunJobQueue, PgRunPlanner, @@ -18,6 +19,7 @@ const runs = PgRunRepository.connect(databaseUrl); const jobs = PgRunJobQueue.connect(databaseUrl); const planner = PgRunPlanner.connect(databaseUrl); const archive = PgArchiveRepository.connect(databaseUrl); +const catalogDiscovery = PgCatalogDiscovery.connect(databaseUrl); const controlToken = process.env.MODELAPSE_CONTROL_TOKEN; const port = Number(process.env.PORT ?? "3000"); const app = createApp({ @@ -25,6 +27,7 @@ const app = createApp({ jobs, planner, archive, + catalogDiscovery, ...(controlToken ? { controlToken } : {}), }); @@ -46,6 +49,7 @@ function shutdown(signal: string): void { jobs.close(), planner.close(), archive.close(), + catalogDiscovery.close(), ]).finally(() => { if (error) { console.error(error); From 21858ee8fe35a96cfbf5a7af94406a046999cd39 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 2 Oct 2026 00:06:16 +0800 Subject: [PATCH 08/28] Archive v0.9: add server-gated Catalog Inbox actions --- apps/web/src/modelapse.ts | 196 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) diff --git a/apps/web/src/modelapse.ts b/apps/web/src/modelapse.ts index 7bf8c4e..4c8d85b 100644 --- a/apps/web/src/modelapse.ts +++ b/apps/web/src/modelapse.ts @@ -438,6 +438,47 @@ export interface ArchiveTemporalComparison { }[]; } +export type CatalogDiscoveryStatus = + | "discovered" + | "matched" + | "ignored" + | "promotion_ready"; + +export interface CatalogDiscoveryCandidate { + readonly id: string; + readonly provider: { + readonly id: string; + readonly slug: string; + readonly name: string; + }; + readonly remoteModelId: string; + readonly firstSeenAt: string; + readonly lastSeenAt: string; + readonly latestProviderSnapshotId: string | null; + readonly observationCount: number; + readonly status: CatalogDiscoveryStatus; + readonly resolvedModel: { + readonly id: string; + readonly canonicalSlug: string; + readonly marketingName: string; + } | null; + readonly resolvedAt: string | null; + readonly lastSource: ArchiveSource; + readonly promotion: { + readonly id: string; + readonly promotedAt: string; + readonly actor: string; + readonly modelId: string; + } | null; + readonly latestDecision: { + readonly id: string; + readonly action: "match_existing" | "ignore" | "mark_promotion_ready" | "reopen"; + readonly decidedAt: string; + readonly actor: string; + readonly note: string | null; + } | null; +} + export interface ControlJob { readonly id: string; readonly kind: string; @@ -476,6 +517,24 @@ interface ReadJobInput extends OperatorInput { readonly jobId: string; } +interface CatalogInboxInput extends OperatorInput { + readonly status?: CatalogDiscoveryStatus; +} + +interface ReconcileCatalogCandidateInput extends OperatorInput { + readonly candidateId: string; + readonly action: "ignore" | "mark_promotion_ready" | "reopen"; + readonly note?: string; +} + +interface PromoteCatalogCandidateInput extends OperatorInput { + readonly candidateId: string; + readonly canonicalSlug: string; + readonly marketingName: string; + readonly status: "preview" | "active"; + readonly note?: string; +} + interface ReadArchiveRunInput { readonly runId: string; } @@ -677,6 +736,89 @@ function parseReadJobInput(value: unknown): ReadJobInput { return { operatorToken, jobId }; } +function parseCatalogInboxInput(value: unknown): CatalogInboxInput { + const operatorToken = parseOperatorToken(value); + if (!isRecord(value)) throw new Error("Catalog Inbox request must be an object"); + const status = value.status; + if ( + status !== undefined && + status !== "discovered" && + status !== "matched" && + status !== "ignored" && + status !== "promotion_ready" + ) { + throw new Error("Invalid catalog discovery status"); + } + return { operatorToken, ...(status ? { status } : {}) }; +} + +function parseReconcileCatalogCandidateInput( + value: unknown, +): ReconcileCatalogCandidateInput { + const operatorToken = parseOperatorToken(value); + if (!isRecord(value)) throw new Error("Reconciliation request must be an object"); + const candidateId = value.candidateId; + const action = value.action; + const note = value.note; + if (typeof candidateId !== "string" || !UUID_RE.test(candidateId)) { + throw new Error("candidateId must be a UUID"); + } + if ( + action !== "ignore" && + action !== "mark_promotion_ready" && + action !== "reopen" + ) { + throw new Error("Unsupported Inbox reconciliation action"); + } + if (note !== undefined && typeof note !== "string") { + throw new Error("note must be a string"); + } + return { + operatorToken, + candidateId, + action, + ...(typeof note === "string" ? { note } : {}), + }; +} + +function parsePromoteCatalogCandidateInput( + value: unknown, +): PromoteCatalogCandidateInput { + const operatorToken = parseOperatorToken(value); + if (!isRecord(value)) throw new Error("Promotion request must be an object"); + const candidateId = value.candidateId; + const canonicalSlug = value.canonicalSlug; + const marketingName = value.marketingName; + const status = value.status; + const note = value.note; + if (typeof candidateId !== "string" || !UUID_RE.test(candidateId)) { + throw new Error("candidateId must be a UUID"); + } + if ( + typeof canonicalSlug !== "string" || + !/^[a-z0-9][a-z0-9-]*$/.test(canonicalSlug) + ) { + throw new Error("canonicalSlug must use lowercase letters, digits, and hyphens"); + } + if (typeof marketingName !== "string" || !marketingName.trim()) { + throw new Error("marketingName is required"); + } + if (status !== "preview" && status !== "active") { + throw new Error("status must be preview or active"); + } + if (note !== undefined && typeof note !== "string") { + throw new Error("note must be a string"); + } + return { + operatorToken, + candidateId, + canonicalSlug, + marketingName, + status, + ...(typeof note === "string" ? { note } : {}), + }; +} + function parseArchiveRunInput(value: unknown): ReadArchiveRunInput { if (!isRecord(value)) throw new Error("Archive Run request must be an object"); @@ -964,6 +1106,60 @@ export const compareArchive = createServerFn({ method: "POST" }) } }); +export const getCatalogInbox = createServerFn({ method: "POST" }) + .validator(parseCatalogInboxInput) + .handler(async ({ data }): Promise => { + requireOperator(data.operatorToken); + const params = new URLSearchParams({ limit: "200" }); + if (data.status) params.set("status", data.status); + const result = await requestJson<{ + candidates: readonly CatalogDiscoveryCandidate[]; + }>(`/v1/control/catalog/discoveries?${params.toString()}`, { + control: true, + }); + return result.candidates; + }); + +export const reconcileCatalogCandidate = createServerFn({ method: "POST" }) + .validator(parseReconcileCatalogCandidateInput) + .handler(async ({ data }) => { + requireOperator(data.operatorToken); + return requestJson<{ eventId: string; status: CatalogDiscoveryStatus }>( + `/v1/control/catalog/discoveries/${data.candidateId}/reconcile`, + { + method: "POST", + control: true, + body: { + action: data.action, + actor: "web-operator", + ...(data.note ? { note: data.note } : {}), + }, + }, + ); + }); + +export const promoteCatalogCandidate = createServerFn({ method: "POST" }) + .validator(parsePromoteCatalogCandidateInput) + .handler(async ({ data }) => { + requireOperator(data.operatorToken); + return requestJson<{ + candidateId: string; + modelId: string; + promotionEventId: string; + reconciliationEventId: string; + }>(`/v1/control/catalog/discoveries/${data.candidateId}/promote`, { + method: "POST", + control: true, + body: { + canonicalSlug: data.canonicalSlug, + marketingName: data.marketingName, + status: data.status, + actor: "web-operator", + ...(data.note ? { note: data.note } : {}), + }, + }); + }); + export const getControlCatalog = createServerFn({ method: "POST" }) .validator(parseOperatorInput) .handler(async ({ data }): Promise => { From f954aabf0221328a0ac0521d7d73f2ad071840c0 Mon Sep 17 00:00:00 2001 From: WangEn Date: Fri, 2 Oct 2026 00:06:46 +0800 Subject: [PATCH 09/28] Archive v0.9: add operator Catalog Inbox UI --- apps/web/src/routes/catalog-inbox.tsx | 409 ++++++++++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 apps/web/src/routes/catalog-inbox.tsx diff --git a/apps/web/src/routes/catalog-inbox.tsx b/apps/web/src/routes/catalog-inbox.tsx new file mode 100644 index 0000000..291e5c8 --- /dev/null +++ b/apps/web/src/routes/catalog-inbox.tsx @@ -0,0 +1,409 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { useMemo, useState } from "react"; +import { + getCatalogInbox, + promoteCatalogCandidate, + reconcileCatalogCandidate, + type CatalogDiscoveryCandidate, + type CatalogDiscoveryStatus, +} from "../modelapse"; + +export const Route = createFileRoute("/catalog-inbox")({ + component: CatalogInbox, +}); + +function suggestedSlug(remoteModelId: string): string { + return remoteModelId + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 80); +} + +function suggestedName(remoteModelId: string): string { + return remoteModelId + .split(/[-_.:/]+/) + .filter(Boolean) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); +} + +function formatTimestamp(value: string): string { + return value.replace("T", " ").slice(0, 19) + "Z"; +} + +function CatalogInbox() { + const [operatorToken, setOperatorToken] = useState(""); + const [candidates, setCandidates] = useState([]); + const [filter, setFilter] = useState("discovered"); + const [selectedId, setSelectedId] = useState(null); + const [canonicalSlug, setCanonicalSlug] = useState(""); + const [marketingName, setMarketingName] = useState(""); + const [modelStatus, setModelStatus] = useState<"preview" | "active">("active"); + const [note, setNote] = useState(""); + const [busy, setBusy] = useState(false); + const [message, setMessage] = useState(null); + const [error, setError] = useState(null); + + const selected = useMemo( + () => candidates.find((candidate) => candidate.id === selectedId) ?? null, + [candidates, selectedId], + ); + + async function loadInbox(status = filter): Promise { + if (!operatorToken) return; + setBusy(true); + setError(null); + try { + const result = await getCatalogInbox({ + data: { + operatorToken, + ...(status === "all" ? {} : { status }), + }, + }); + setCandidates(result); + if (selectedId && !result.some((candidate) => candidate.id === selectedId)) { + setSelectedId(null); + } + } catch (cause) { + setCandidates([]); + setError(cause instanceof Error ? cause.message : "Catalog Inbox request failed"); + } finally { + setBusy(false); + } + } + + function selectCandidate(candidate: CatalogDiscoveryCandidate): void { + setSelectedId(candidate.id); + setCanonicalSlug(suggestedSlug(candidate.remoteModelId)); + setMarketingName(suggestedName(candidate.remoteModelId)); + setModelStatus("active"); + setNote(""); + setMessage(null); + setError(null); + } + + async function reconcile( + action: "ignore" | "mark_promotion_ready" | "reopen", + ): Promise { + if (!selected) return; + setBusy(true); + setError(null); + setMessage(null); + try { + await reconcileCatalogCandidate({ + data: { + operatorToken, + candidateId: selected.id, + action, + ...(note ? { note } : {}), + }, + }); + setMessage( + action === "mark_promotion_ready" + ? "Candidate marked promotion ready. Review canonical identity fields before promotion." + : action === "ignore" + ? "Candidate ignored; observation history remains intact." + : "Candidate reopened for review.", + ); + await loadInbox(); + } catch (cause) { + setError(cause instanceof Error ? cause.message : "Reconciliation failed"); + } finally { + setBusy(false); + } + } + + async function promote(): Promise { + if (!selected) return; + setBusy(true); + setError(null); + setMessage(null); + try { + const result = await promoteCatalogCandidate({ + data: { + operatorToken, + candidateId: selected.id, + canonicalSlug, + marketingName, + status: modelStatus, + ...(note ? { note } : {}), + }, + }); + setMessage(`Promoted to canonical Model ${result.modelId}. The promotion and reconciliation events are immutable.`); + await loadInbox(); + } catch (cause) { + setError(cause instanceof Error ? cause.message : "Promotion failed"); + } finally { + setBusy(false); + } + } + + return ( +
+
+ + M + + Modelapse + Catalog Discovery Inbox + + + Public catalog changes → +
+ +
+
+

OPERATOR / ARCHIVE v0.9

+

+ Discovery is evidence. +
+ Promotion is a decision. +

+

+ Review remote model IDs observed from first-party catalog sources. + Reconcile or promote them without rewriting the immutable collection + history that discovered them. +

+
+
+ +
+
+
+

ACCESS

+

Unlock Catalog Inbox

+
+ + {candidates.length ? `${candidates.length} loaded` : "operator locked"} + +
+
+ + +
+ +
+
+ {error ?
{error}
: null} + {message ?
{message}
: null} +
+ +
+
+
+

DISCOVERY QUEUE

+

First-party remote IDs

+
+
+ +
+
+ {candidates.map((candidate) => ( + + ))} + {candidates.length === 0 ? ( +
+ Unlock the Inbox, or no candidates match this state. +
+ ) : null} +
+ +