The catalogue, as a module that owns the module graph

It links module-versions to each other and knows nothing about nodes; which
machine runs what stays the control plane's (novox/hq ADR 0070, 0072). Keeping
them apart is what lets the control plane carry on composing declarations while
this is down.

The builder announces what it built, this places it in the graph and announces
what that means, and the control plane hooks the meaning rather than the build
output. A rebuild producing the commit already current is registered and is not
an upgrade — announcing it would ripple outward forever through modules that did
not change.

Ordering is not computed. Modules stale and waiting on nothing that is itself
stale are announced as buildable; the rest stay stale and appear once whatever
they were waiting for is registered, so a chain and a diamond need no special
handling and nothing holds a plan.

Four tools over the graph: what this mesh holds, one module in full, what a
change to a module reaches, and what must be rebuilt and why. The edges are
derived from builds rather than declared, so they cannot drift from what the code
actually uses.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
2026-09-12 23:16:05 +02:00
parent a4d10341e7
commit a73cb8a2f8
7 changed files with 513 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
// mesh-catalog's tools — the module graph's query surface (novox/hq ADR 0070).
//
// These are the questions the graph exists to answer, and none of them can be answered anywhere
// else today: what does this mesh know how to run, what is this module made of, what does a change
// to this reach, and what is waiting to be rebuilt.
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
import { Graph } from "../store.js";
export function getCatalogueTools(graph: Graph): ToolDefinition[] {
return [
{
name: "catalog_modules",
description:
"Every module this mesh holds, with the commit the catalogue considers current and where it came from.",
input: {},
run: async () => ({ modules: await graph.modules() }),
},
{
name: "catalog_module",
description:
"One module in full — where it came from, what it requires and provides, and what it is made of. The current version unless a commit is named.",
input: {
module: { type: "string", description: "the module's name" },
commit: { type: "string", description: "a particular version (optional)" },
},
run: async (args) => {
const module = String(args.module ?? "");
if (!module) return { error: "name a module" };
const found = await graph.show(module, args.commit ? String(args.commit) : undefined);
return found ? { module: found } : { error: `the catalogue holds no ${module}` };
},
},
{
name: "catalog_dependents",
description:
"What was built against this module — the modules a change to it reaches. Derived from builds, not from a declared list, so it cannot drift from what the code actually uses.",
input: { module: { type: "string", description: "the module that would change" } },
run: async (args) => {
const module = String(args.module ?? "");
if (!module) return { error: "name a module" };
return { module, dependents: await graph.dependents(module) };
},
},
{
name: "catalog_stale",
description:
"What must be rebuilt and why — every module built against something that has since moved. `buildable` is the subset waiting on nothing that is itself stale, which is the set that can be built right now.",
input: {},
run: async () => ({
stale: await graph.stale(),
buildable: await graph.buildable(),
}),
},
];
}
// Opened from the environment when the runtime asks for the module's tools. When it cannot be —
// no DATABASE_URL — the module contributes no tools rather than taking the whole tool runtime down
// with it, which is the postgres precedent.
registerModuleTools("mesh-catalog", (env) => {
try {
return getCatalogueTools(Graph.fromEnv(env));
} catch {
return [];
}
});