The graph holds what a module declares, not only what it was built on
Build edges are discovered by building; requires and provides are stated by the module about itself. Both belong in the graph and answer different questions — and "what provides postgres-database" needed a sweep over every manifest, which only something holding all of them can do.
This commit is contained in:
@@ -87,6 +87,28 @@ CREATE TABLE IF NOT EXISTS module_artifact (
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS module_artifact_reference ON module_artifact (reference);
|
CREATE INDEX IF NOT EXISTS module_artifact_reference ON module_artifact (reference);
|
||||||
|
|
||||||
|
-- What a module-version declares. Separate from the build edges above and never mixed with them:
|
||||||
|
-- a build edge is a fact about what was compiled, discovered by building it, and these are
|
||||||
|
-- intentions the module states about itself (ADR 0072). They answer different questions and go
|
||||||
|
-- stale for different reasons.
|
||||||
|
CREATE TABLE IF NOT EXISTS module_requires (
|
||||||
|
module text NOT NULL,
|
||||||
|
commit_sha text NOT NULL,
|
||||||
|
requirement text NOT NULL,
|
||||||
|
PRIMARY KEY (module, commit_sha, requirement)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS module_provides (
|
||||||
|
module text NOT NULL,
|
||||||
|
commit_sha text NOT NULL,
|
||||||
|
provision text NOT NULL,
|
||||||
|
scope text NOT NULL DEFAULT '',
|
||||||
|
PRIMARY KEY (module, commit_sha, provision)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS module_provides_provision ON module_provides (provision);
|
||||||
|
CREATE INDEX IF NOT EXISTS module_requires_requirement ON module_requires (requirement);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS built_against (
|
CREATE TABLE IF NOT EXISTS built_against (
|
||||||
module text NOT NULL,
|
module text NOT NULL,
|
||||||
commit_sha text NOT NULL,
|
commit_sha text NOT NULL,
|
||||||
@@ -154,6 +176,34 @@ export class Graph {
|
|||||||
|
|
||||||
// Both are replaced rather than added to: they describe this build, and a previous build of
|
// 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.
|
// the same commit that saw different artifacts was wrong about one of them.
|
||||||
|
// Read from the manifest rather than taken as a separate argument: the manifest the builder
|
||||||
|
// resolved IS the module's declaration, and a second copy passed alongside it could disagree
|
||||||
|
// with it. Replaced wholesale, because a version declares exactly one set of these.
|
||||||
|
const declared = (version.manifest ?? {}) as {
|
||||||
|
requires?: unknown[];
|
||||||
|
provides?: unknown[];
|
||||||
|
};
|
||||||
|
await client.query(`DELETE FROM module_requires WHERE module = $1 AND commit_sha = $2`,
|
||||||
|
[version.module, version.commit]);
|
||||||
|
for (const r of declared.requires ?? []) {
|
||||||
|
const name = typeof r === "string" ? r : String((r as { name?: unknown }).name ?? "");
|
||||||
|
if (!name) continue;
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO module_requires (module, commit_sha, requirement) VALUES ($1,$2,$3)
|
||||||
|
ON CONFLICT DO NOTHING`, [version.module, version.commit, name]);
|
||||||
|
}
|
||||||
|
await client.query(`DELETE FROM module_provides WHERE module = $1 AND commit_sha = $2`,
|
||||||
|
[version.module, version.commit]);
|
||||||
|
for (const pv of declared.provides ?? []) {
|
||||||
|
const name = typeof pv === "string" ? pv : String((pv as { name?: unknown }).name ?? "");
|
||||||
|
const scope = typeof pv === "string" ? "" : String((pv as { scope?: unknown }).scope ?? "");
|
||||||
|
if (!name) continue;
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO module_provides (module, commit_sha, provision, scope) VALUES ($1,$2,$3,$4)
|
||||||
|
ON CONFLICT (module, commit_sha, provision) DO UPDATE SET scope = excluded.scope`,
|
||||||
|
[version.module, version.commit, name, scope]);
|
||||||
|
}
|
||||||
|
|
||||||
await client.query(`DELETE FROM module_artifact WHERE module = $1 AND commit_sha = $2`,
|
await client.query(`DELETE FROM module_artifact WHERE module = $1 AND commit_sha = $2`,
|
||||||
[version.module, version.commit]);
|
[version.module, version.commit]);
|
||||||
for (const a of made) {
|
for (const a of made) {
|
||||||
@@ -285,6 +335,35 @@ export class Graph {
|
|||||||
return stale.filter((s) => !s.because.some((b) => waiting.has(b.module)));
|
return stale.filter((s) => !s.because.some((b) => waiting.has(b.module)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What provides a given provision, and what needs it.
|
||||||
|
*
|
||||||
|
* **Answered from the graph rather than by scanning manifests**, which is how it is answered
|
||||||
|
* today — a sweep over every module's declaration, which only whoever holds every declaration
|
||||||
|
* can do and which is wrong the moment one of them changes.
|
||||||
|
*/
|
||||||
|
async whoProvides(provision: string): Promise<{
|
||||||
|
provides: { module: string; commit: string; scope: string }[];
|
||||||
|
requires: { module: string; commit: string }[];
|
||||||
|
}> {
|
||||||
|
const provides = await this.pool.query(
|
||||||
|
`SELECT p.module, p.commit_sha, p.scope
|
||||||
|
FROM module_provides p
|
||||||
|
JOIN module_current c ON c.module = p.module AND c.commit_sha = p.commit_sha
|
||||||
|
WHERE p.provision = $1 ORDER BY p.module`, [provision]);
|
||||||
|
const requires = await this.pool.query(
|
||||||
|
`SELECT r.module, r.commit_sha
|
||||||
|
FROM module_requires r
|
||||||
|
JOIN module_current c ON c.module = r.module AND c.commit_sha = r.commit_sha
|
||||||
|
WHERE r.requirement = $1 ORDER BY r.module`, [provision]);
|
||||||
|
return {
|
||||||
|
provides: provides.rows.map((r) => ({
|
||||||
|
module: r.module as string, commit: r.commit_sha as string, scope: r.scope as string })),
|
||||||
|
requires: requires.rows.map((r) => ({
|
||||||
|
module: r.module as string, commit: r.commit_sha as string })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
await this.pool.end();
|
await this.pool.end();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,17 @@ export function getCatalogueTools(graph: Graph): ToolDefinition[] {
|
|||||||
return found ? { module: found } : { error: `the catalogue holds no ${module}` };
|
return found ? { module: found } : { error: `the catalogue holds no ${module}` };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "catalog_provides",
|
||||||
|
description:
|
||||||
|
"What provides a provision, and what needs it. Answered from what modules declare about themselves, so it does not require holding every manifest at once.",
|
||||||
|
input: { provision: { type: "string", description: "the provision, e.g. postgres-database" } },
|
||||||
|
run: async (args) => {
|
||||||
|
const provision = String(args.provision ?? "");
|
||||||
|
if (!provision) return { error: "name a provision" };
|
||||||
|
return await graph.whoProvides(provision);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "catalog_dependents",
|
name: "catalog_dependents",
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user