fix(catalog): digest provider projections
This commit is contained in:
parent
8ec9bb1bfe
commit
c4cd0330e0
|
|
@ -671,7 +671,8 @@
|
|||
"tools_sha256": "5a88bf598a1e0fe705761db6af92f1ceabaf026cb6efa425e87b9633f1d7a3de",
|
||||
"prompts_sha256": "f6da3ad38a1000c11364d1710f122ac4990551f04efd8c53ffb2e819b5cf5707",
|
||||
"resources_sha256": "339a984ea8cd8be5f234adf24395bf0295258d990492005cf6fe1f293d12cd46",
|
||||
"combined_sha256": "861cd8618e793342daa8b149bc2944360d84b077fd19cf22f6e0a57b6b249c73"
|
||||
"combined_sha256": "861cd8618e793342daa8b149bc2944360d84b077fd19cf22f6e0a57b6b249c73",
|
||||
"providers_sha256": "918ccd6b8ae442cd5844dcfde55086f458176241c8d166c1e9dd0f242ccd2bb0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { createHash } from "node:crypto";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import canonicalize from "canonicalize";
|
||||
|
||||
const load = (root, path) => import(pathToFileURL(join(root, path)).href);
|
||||
|
||||
/** Sort tool/provider names alphabetically. */
|
||||
|
|
@ -9,6 +12,14 @@ export const names = (contracts) =>
|
|||
.map(({ name }) => name)
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
|
||||
/** Return the stable digest for a generated catalog projection. */
|
||||
export const digest = (value) => {
|
||||
const encoded = canonicalize(value);
|
||||
if (encoded === undefined)
|
||||
throw new TypeError("Catalog projection is not canonical JSON");
|
||||
return createHash("sha256").update(encoded).digest("hex");
|
||||
};
|
||||
|
||||
/** Throw when two name lists diverge. */
|
||||
export const assertSameNames = (label, actual, expected) => {
|
||||
const actualSet = new Set(actual);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ export interface ProductCatalog {
|
|||
}
|
||||
|
||||
export function createProductCatalog(root: string): Promise<ProductCatalog>;
|
||||
export function providerCatalogDigest(
|
||||
providers: ProductCatalog["providers"],
|
||||
): string;
|
||||
export function createCliInventory(cli: unknown): {
|
||||
readonly primary: readonly string[];
|
||||
readonly aliases: readonly {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { format } from "prettier";
|
||||
|
||||
import { assertSameNames, loadSources } from "./catalog-core.mjs";
|
||||
import { assertSameNames, digest, loadSources } from "./catalog-core.mjs";
|
||||
import {
|
||||
createCliInventory,
|
||||
cliCommandDescriptionIssues,
|
||||
|
|
@ -15,6 +15,9 @@ export {
|
|||
cliCommandOptionNames,
|
||||
};
|
||||
|
||||
/** Return the stable digest for the source-derived provider projection. */
|
||||
export const providerCatalogDigest = (providers) => digest(providers);
|
||||
|
||||
/** Project current runtime contracts into deterministic, machine-readable facts. */
|
||||
export const createProductCatalog = async (root) => {
|
||||
const sources = await loadSources(root);
|
||||
|
|
@ -25,6 +28,7 @@ export const createProductCatalog = async (root) => {
|
|||
sources.catalogIdentity.CLI_COMMAND_NAMES,
|
||||
);
|
||||
const tools = toolFamilyCatalog(sources);
|
||||
const providers = providerCatalog(sources);
|
||||
const metadata = sources.packageMetadata.PACKAGE_METADATA;
|
||||
return {
|
||||
catalog_schema_version: 1,
|
||||
|
|
@ -39,7 +43,7 @@ export const createProductCatalog = async (root) => {
|
|||
skill_version: metadata.skillVersion,
|
||||
},
|
||||
tools,
|
||||
providers: providerCatalog(sources),
|
||||
providers,
|
||||
setup_clients: sources.supportedClients.SUPPORTED_CLIENT_DEFINITIONS.map(
|
||||
({ name, displayName, format }) => ({
|
||||
id: name,
|
||||
|
|
@ -56,7 +60,10 @@ export const createProductCatalog = async (root) => {
|
|||
},
|
||||
runtime_catalog: {
|
||||
counts: sources.catalogIdentity.CATALOG_IDENTITY.counts,
|
||||
digests: sources.catalogIdentity.CATALOG_IDENTITY.digests,
|
||||
digests: {
|
||||
...sources.catalogIdentity.CATALOG_IDENTITY.digests,
|
||||
providers_sha256: providerCatalogDigest(providers),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import {
|
|||
cliCommandDescriptionIssues,
|
||||
cliCommandOptionNames,
|
||||
createProductCatalog,
|
||||
providerCatalogDigest,
|
||||
serializeProductCatalog,
|
||||
} from "../scripts/lib/product-catalog.mjs";
|
||||
|
||||
|
|
@ -139,6 +140,9 @@ describe("canonical product catalog", () => {
|
|||
"project_managed_application_graph",
|
||||
"verify_managed_native_boundaries",
|
||||
]);
|
||||
expect(catalog.runtime_catalog.digests.providers_sha256).toBe(
|
||||
providerCatalogDigest(catalog.providers),
|
||||
);
|
||||
expect(
|
||||
z.toJSONSchema(analysisSnapshotSchema).properties?.snapshot_version,
|
||||
).toMatchObject({
|
||||
|
|
@ -230,6 +234,19 @@ describe("canonical product catalog", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("changes the provider projection digest when provider facts drift", async () => {
|
||||
const catalog = await createProductCatalog(root);
|
||||
const firstProvider = catalog.providers[0];
|
||||
if (firstProvider === undefined) throw new TypeError("Missing provider");
|
||||
const driftedProviders = [
|
||||
{ ...firstProvider, name: `${firstProvider.name} drifted` },
|
||||
...catalog.providers.slice(1),
|
||||
];
|
||||
expect(providerCatalogDigest(driftedProviders)).not.toBe(
|
||||
catalog.runtime_catalog.digests.providers_sha256,
|
||||
);
|
||||
});
|
||||
|
||||
it("fails check mode without rewriting a stale generated artifact", async () => {
|
||||
const directory = await createTestTempDirectory("rea-generated-check-");
|
||||
temporaryRoots.push(directory);
|
||||
|
|
|
|||
Loading…
Reference in New Issue