import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import path from "node:path"; export function artifactCatalogAuthorityPath(catalogPath) { return `${catalogPath}.authority.json`; } export function artifactCatalogSha256(catalog) { return createHash("sha256").update(JSON.stringify(catalog)).digest("hex"); } export function artifactCatalogSourceCommitId(catalog) { const direct = text(catalog?.publish?.sourceCommitId) || text(catalog?.sourceCommitId) || text(catalog?.commitId); if (direct) return direct; const serviceCommits = unique((catalog?.services ?? []).map((service) => text(service?.sourceCommitId)).filter(Boolean)); return serviceCommits.length === 1 ? serviceCommits[0] : null; } export function artifactCatalogServiceCoverage(catalog, selectedServices) { if (!Array.isArray(catalog?.services)) throw new Error("artifact catalog services must be an array"); const selectedServiceIds = normalizedServiceIds(selectedServices, "selected services"); const catalogServiceIds = normalizedServiceIds(catalog.services.map((service) => service?.serviceId), "artifact catalog services"); const missingSelectedServices = selectedServiceIds.filter((serviceId) => !catalogServiceIds.includes(serviceId)); const extraCatalogServices = catalogServiceIds.filter((serviceId) => !selectedServiceIds.includes(serviceId)); return { status: missingSelectedServices.length > 0 ? (extraCatalogServices.length > 0 ? "divergent" : "partial") : (extraCatalogServices.length > 0 ? "superset" : "exact"), selectedServiceCount: selectedServiceIds.length, selectedServicePresentCount: selectedServiceIds.length - missingSelectedServices.length, catalogServiceCount: catalogServiceIds.length, selectedServiceIds, catalogServiceIds, missingSelectedServices, extraCatalogServices }; } export function createHydratedArtifactCatalogAuthority({ catalog, catalogPath, gitopsBranch, gitopsCommitId, selectedServices }) { return { schemaVersion: "v1", status: "hydrated", authority: "gitops-branch", catalogPath: normalizeCatalogPath(catalogPath), gitopsBranch: text(gitopsBranch), gitopsCommitId: text(gitopsCommitId), catalogSourceCommitId: artifactCatalogSourceCommitId(catalog), serviceCount: Array.isArray(catalog?.services) ? catalog.services.length : 0, catalogSha256: artifactCatalogSha256(catalog), coverage: artifactCatalogServiceCoverage(catalog, selectedServices) }; } export function createBootstrapArtifactCatalogAuthority({ catalog, catalogPath, sourceCommitId, reason, selectedServices }) { return { schemaVersion: "v1", status: "bootstrap", authority: "source-bootstrap", catalogPath: normalizeCatalogPath(catalogPath), sourceCommitId: text(sourceCommitId), bootstrapReason: text(reason), catalogSourceCommitId: artifactCatalogSourceCommitId(catalog), serviceCount: Array.isArray(catalog?.services) ? catalog.services.length : 0, catalogSha256: artifactCatalogSha256(catalog), coverage: artifactCatalogServiceCoverage(catalog, selectedServices) }; } export async function readArtifactCatalogSummary({ repoRoot, catalogPath, authorityPath = artifactCatalogAuthorityPath(catalogPath), catalog = undefined }) { const loadedCatalog = catalog === undefined ? await readJsonIfPresent(repoRoot, catalogPath) : catalog; const authority = await readJsonIfPresent(repoRoot, authorityPath); const normalizedCatalogPath = normalizeCatalogPath(catalogPath); if (!loadedCatalog) { if (authority) throw new Error(`artifact catalog authority exists without catalog ${normalizedCatalogPath}`); return { status: "missing", authority: "none", catalogPath: normalizedCatalogPath, authorityPath: null, gitopsBranch: null, gitopsCommitId: null, catalogSourceCommitId: null, serviceCount: 0, catalogSha256: null, coverage: null, bootstrapReason: null }; } const serviceCount = Array.isArray(loadedCatalog.services) ? loadedCatalog.services.length : 0; const catalogSha256 = artifactCatalogSha256(loadedCatalog); const catalogSourceCommitId = artifactCatalogSourceCommitId(loadedCatalog); if (!authority) { return { status: "loaded", authority: "workspace-file", catalogPath: normalizedCatalogPath, authorityPath: null, gitopsBranch: null, gitopsCommitId: null, catalogSourceCommitId, serviceCount, catalogSha256, coverage: null, bootstrapReason: null }; } const coverage = validateCatalogAuthority({ authority, catalog: loadedCatalog, catalogPath: normalizedCatalogPath, serviceCount, catalogSha256 }); if (authority.authority === "gitops-branch") { return { status: "hydrated", authority: "gitops-branch", catalogPath: normalizedCatalogPath, authorityPath: normalizeCatalogPath(authorityPath), gitopsBranch: text(authority.gitopsBranch), gitopsCommitId: text(authority.gitopsCommitId), catalogSourceCommitId: text(authority.catalogSourceCommitId) || catalogSourceCommitId, serviceCount, catalogSha256, coverage, bootstrapReason: null }; } return { status: "bootstrap", authority: "source-bootstrap", catalogPath: normalizedCatalogPath, authorityPath: normalizeCatalogPath(authorityPath), gitopsBranch: null, gitopsCommitId: null, sourceCommitId: text(authority.sourceCommitId), catalogSourceCommitId: text(authority.catalogSourceCommitId) || catalogSourceCommitId, serviceCount, catalogSha256, coverage, bootstrapReason: text(authority.bootstrapReason) }; } function validateCatalogAuthority({ authority, catalog, catalogPath, serviceCount, catalogSha256 }) { const gitopsAuthority = authority.schemaVersion === "v1" && authority.status === "hydrated" && authority.authority === "gitops-branch"; const bootstrapAuthority = authority.schemaVersion === "v1" && authority.status === "bootstrap" && authority.authority === "source-bootstrap"; if (!gitopsAuthority && !bootstrapAuthority) { throw new Error(`artifact catalog authority for ${catalogPath} is not a supported authority record`); } if (normalizeCatalogPath(authority.catalogPath) !== catalogPath) { throw new Error(`artifact catalog authority path mismatch: expected ${catalogPath}, got ${authority.catalogPath ?? "missing"}`); } if (authority.serviceCount !== serviceCount) { throw new Error(`artifact catalog authority service count mismatch for ${catalogPath}: expected ${serviceCount}, got ${authority.serviceCount ?? "missing"}`); } if (authority.catalogSha256 !== catalogSha256) { throw new Error(`artifact catalog authority fingerprint mismatch for ${catalogPath}`); } const coverage = artifactCatalogServiceCoverage(catalog, authority.coverage?.selectedServiceIds); validateCoverageRecord(authority.coverage, coverage, catalogPath); if (gitopsAuthority && (!text(authority.gitopsBranch) || !/^[a-f0-9]{40}$/u.test(text(authority.gitopsCommitId) ?? ""))) { throw new Error(`artifact catalog authority gitops provenance is incomplete for ${catalogPath}`); } if (bootstrapAuthority) { const sourceCommitId = text(authority.sourceCommitId); if (!/^[a-f0-9]{40}$/u.test(sourceCommitId ?? "") || !text(authority.bootstrapReason)) { throw new Error(`artifact catalog bootstrap authority is incomplete for ${catalogPath}`); } if (sourceCommitId !== artifactCatalogSourceCommitId(catalog)) { throw new Error(`artifact catalog bootstrap source commit mismatch for ${catalogPath}`); } } return coverage; } async function readJsonIfPresent(repoRoot, filePath) { const resolved = path.isAbsolute(filePath) ? filePath : path.join(repoRoot, filePath); try { return JSON.parse(await readFile(resolved, "utf8")); } catch (error) { if (error?.code === "ENOENT") return null; throw error; } } function normalizeCatalogPath(value) { return String(value ?? "").replaceAll("\\", "/").replace(/^\.\//u, "").trim(); } function text(value) { const result = typeof value === "string" ? value.trim() : ""; return result || null; } function unique(values) { return [...new Set(values)]; } function normalizedServiceIds(values, label) { if (!Array.isArray(values) || values.length === 0) throw new Error(`${label} must be a non-empty array`); const normalized = values.map(text); if (normalized.some((value) => value === null)) throw new Error(`${label} contains an empty service id`); const deduplicated = unique(normalized); if (deduplicated.length !== normalized.length) throw new Error(`${label} contains duplicate service ids`); return deduplicated.sort(); } function validateCoverageRecord(actual, expected, catalogPath) { const scalarFields = ["status", "selectedServiceCount", "selectedServicePresentCount", "catalogServiceCount"]; const arrayFields = ["selectedServiceIds", "catalogServiceIds", "missingSelectedServices", "extraCatalogServices"]; const expectedFields = [...scalarFields, ...arrayFields].sort(); const actualFields = actual && typeof actual === "object" && !Array.isArray(actual) ? Object.keys(actual).sort() : []; if (!sameArray(actualFields, expectedFields)) { throw new Error(`artifact catalog authority coverage fields mismatch for ${catalogPath}`); } for (const field of scalarFields) { if (actual[field] !== expected[field]) { throw new Error(`artifact catalog authority coverage ${field} mismatch for ${catalogPath}`); } } for (const field of arrayFields) { if (!sameArray(actual[field], expected[field])) { throw new Error(`artifact catalog authority coverage ${field} mismatch for ${catalogPath}`); } } } function sameArray(left, right) { return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => value === right[index]); }