134 lines
3.8 KiB
TypeScript
134 lines
3.8 KiB
TypeScript
import type { HousekeepingRegistry } from "../foundation/registry";
|
|
import type { MigrationEntry } from "../migration/types";
|
|
import type { HousekeepingRouteHandler } from "../route-handlers";
|
|
|
|
export interface HousekeepingParityReport {
|
|
readonly discovered: 137;
|
|
readonly mapped: number;
|
|
readonly verified: number;
|
|
readonly removed: number;
|
|
readonly unresolved: readonly string[];
|
|
readonly capabilityGaps: readonly string[];
|
|
readonly handlerGaps: readonly string[];
|
|
}
|
|
|
|
function expectedEvidence(row: MigrationEntry): string {
|
|
return `runtime-parity:${row.legacyPath}`;
|
|
}
|
|
|
|
function capabilityMatches(
|
|
row: MigrationEntry,
|
|
route: HousekeepingRegistry["domains"][number]["routes"][number],
|
|
): boolean {
|
|
const declared = new Set(row.capabilities.read);
|
|
if (route.capability.mode === "all") {
|
|
return route.capability.slugs.every((slug) => declared.has(slug));
|
|
}
|
|
return route.capability.slugs.some((slug) => declared.has(slug));
|
|
}
|
|
|
|
export function verifyHousekeepingRuntimeParity(
|
|
matrix: readonly MigrationEntry[],
|
|
registry: HousekeepingRegistry,
|
|
handlers: readonly HousekeepingRouteHandler[],
|
|
): HousekeepingParityReport {
|
|
if (matrix.length !== 137) {
|
|
throw new Error(
|
|
`Housekeeping parity requires exactly 137 discovered rows; received ${matrix.length}`,
|
|
);
|
|
}
|
|
|
|
const routes = registry.domains.flatMap((domain) => domain.routes);
|
|
const routesByHref = new Map<string, (typeof routes)[number]>(
|
|
routes.map((route) => [route.href, route]),
|
|
);
|
|
const routesById = new Map(routes.map((route) => [route.id, route]));
|
|
const handlersByRouteId = new Map<string, HousekeepingRouteHandler[]>();
|
|
for (const handler of handlers) {
|
|
const matching = handlersByRouteId.get(handler.routeId) ?? [];
|
|
matching.push(handler);
|
|
handlersByRouteId.set(handler.routeId, matching);
|
|
}
|
|
|
|
const unresolved = new Set<string>();
|
|
const capabilityGaps = new Set<string>();
|
|
const handlerGaps = new Set<string>();
|
|
let mapped = 0;
|
|
let verified = 0;
|
|
let removed = 0;
|
|
|
|
for (const row of matrix) {
|
|
let rowMapped = false;
|
|
let rowCapabilityVerified = true;
|
|
let rowHandlerVerified = true;
|
|
|
|
if (row.decision === "REMOVE") {
|
|
removed += 1;
|
|
rowMapped = row.targetPath === null;
|
|
if (!rowMapped)
|
|
unresolved.add(`${row.legacyPath}: removed target exists`);
|
|
} else if (row.targetPath === null) {
|
|
unresolved.add(`${row.legacyPath}: retained target is missing`);
|
|
} else {
|
|
const route = routesByHref.get(row.targetPath);
|
|
if (!route) {
|
|
unresolved.add(
|
|
`${row.legacyPath}: no registered route for ${row.targetPath}`,
|
|
);
|
|
} else {
|
|
rowMapped = true;
|
|
if (!capabilityMatches(row, route)) {
|
|
rowCapabilityVerified = false;
|
|
capabilityGaps.add(
|
|
`${row.legacyPath}: ${row.targetPath} capability mismatch`,
|
|
);
|
|
}
|
|
|
|
const matchingHandlers = handlersByRouteId.get(route.id) ?? [];
|
|
if (matchingHandlers.length !== 1) {
|
|
rowHandlerVerified = false;
|
|
handlerGaps.add(
|
|
`${row.legacyPath}: ${route.id} has ${matchingHandlers.length} handlers`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (rowMapped) mapped += 1;
|
|
const expectedStatus = row.decision === "REMOVE" ? "REMOVED" : "VERIFIED";
|
|
if (
|
|
rowMapped &&
|
|
rowCapabilityVerified &&
|
|
rowHandlerVerified &&
|
|
row.status === expectedStatus &&
|
|
row.parityEvidence.includes(expectedEvidence(row))
|
|
) {
|
|
verified += 1;
|
|
}
|
|
}
|
|
|
|
for (const route of routes) {
|
|
const matchingHandlers = handlersByRouteId.get(route.id) ?? [];
|
|
if (matchingHandlers.length !== 1) {
|
|
handlerGaps.add(
|
|
`registry route ${route.id} has ${matchingHandlers.length} handlers`,
|
|
);
|
|
}
|
|
}
|
|
for (const handler of handlers) {
|
|
if (!routesById.has(handler.routeId)) {
|
|
handlerGaps.add(`orphan handler ${handler.routeId}`);
|
|
}
|
|
}
|
|
|
|
return {
|
|
discovered: 137,
|
|
mapped,
|
|
verified,
|
|
removed,
|
|
unresolved: [...unresolved].sort(),
|
|
capabilityGaps: [...capabilityGaps].sort(),
|
|
handlerGaps: [...handlerGaps].sort(),
|
|
};
|
|
}
|