/**
*
* RFC-1296: derive honest routeCount/sitemapHash from the built dist
* tree instead of parsing `astro build` stdout or assuming a
* `dist/sitemap-index.xml` layout that the Cloudflare adapter does
* not emit.
* Mission / engine
* mission-materialization-commands.ts (full + reuse paths)
*
* - routeCount must always reflect actually rendered non-redirect
* pages — never fall back to a stdout regex.
*
*
- Not a contract source of truth — callers depend on the module's exported API, not this file's internals.
- RFC-1311: Compass header sweep — repairs pre-existing contract debt surfaced by the test-file scaffold rollout.
*/
import path from "node:path";
import { collectFiles } from "@warpgogol/werkstatt-shared/node/fs";
import { isHtmlRedirectPage } from "@warpgogol/werkstatt-shared/semantic";
import type { WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel";
import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel";
import { byteHashFile } from "@warpgogol/werkstatt-engine/fingerprint";
export interface DistFacts {
routeCount: number;
sitemapHash: string;
}
const SITEMAP_CANDIDATES = [
"client/sitemap-index.xml",
"client/sitemap.xml",
"sitemap-index.xml",
] as const;
/**
* Compute dist facts from the rendered tree:
*
* - `routeCount` — number of non-redirect `index.html` files under
* `/client` (meta-refresh stubs excluded via `isHtmlRedirectPage`).
* Falls back to `` itself when no `client/` subtree exists (static
* adapters emit at the dist root).
* - `sitemapHash` — sha256 of the first existing file among
* `client/sitemap-index.xml`, `client/sitemap.xml`, `sitemap-index.xml`;
* `"sha256:no-sitemap"` when none exist.
*/
export async function computeDistFacts(
distDir: string,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
const clientDir = path.join(distDir, "client");
const htmlRoot = (await io.exists(clientDir)) ? clientDir : distDir;
let routeCount = 0;
try {
const htmlFiles = (await collectFiles(htmlRoot, { extensions: [".html"] })).filter(
(f) =>
f.replace(/\\/g, "/").endsWith("/index.html") || f.replace(/\\/g, "/") === "index.html",
);
for (const file of htmlFiles) {
try {
const html = await io.readFile(file);
if (!isHtmlRedirectPage(html)) routeCount += 1;
} catch {
// unreadable page — not a route
}
}
} catch {
// dist missing entirely — count stays 0
}
let sitemapHash = "sha256:no-sitemap";
for (const rel of SITEMAP_CANDIDATES) {
const candidate = path.join(distDir, rel);
if (await io.exists(candidate)) {
sitemapHash = await byteHashFile(candidate);
break;
}
}
return { routeCount, sitemapHash };
}