/*
Architecture review 2026-07-10: compose the final IndexDecision from pure gate results.
Each gate (demand, evidence, substance, freshness, budget) is a small pure function that
returns a GateResult. The composer applies precedence and produces the final IndexDecision.
This concentrates all indexability logic in one module instead of scattered inline mutations
in the consumer's expandBlueprint.
- Do not load data from the filesystem — gate inputs are supplied by the caller.
- Do not mutate VirtualRouteEntry — the caller applies the composed decision.
- RFC-1097: step 6 — compass.migrate codemod run
Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order.
- RFC-1097: sweep — werkstatt-engine clean
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.
*/
import type { IndexDecision, IndexReason } from "./types.ts";
/** The outcome of one gate evaluation. */
export interface GateResult {
readonly gate: "demand" | "evidence" | "substance" | "freshness" | "budget";
readonly pass: boolean;
readonly reason?: IndexReason;
readonly noindex: boolean;
/** When true, the entry should be dropped entirely (do-not-emit). */
readonly suppress?: boolean;
/** Gate-specific score (e.g. substance score 0..100). */
readonly score?: number;
/** Gate-specific metadata (e.g. werkEvidenceCount). */
readonly metadata?: Readonly>;
}
/** Precedence order: earlier gates take priority for the reason field when multiple fail. */
const GATE_PRECEDENCE: readonly GateResult["gate"][] = [
"demand",
"evidence",
"freshness",
"substance",
"budget",
];
/**
* Compose the final IndexDecision from a base decision (record gate) and an array of gate results.
* Applies precedence: the first failing gate in precedence order sets the reason. Any failing gate
* sets noindex=true. If any gate requests suppress, the entry should be dropped entirely.
*/
export function composeIndexDecision(
base: IndexDecision,
results: readonly GateResult[],
): { decision: IndexDecision; suppress: boolean } {
const decision = { ...base };
let suppress = false;
const byGate = new Map();
for (const r of results) byGate.set(r.gate, r);
for (const r of results) {
if (r.suppress) suppress = true;
if (!r.pass && r.noindex) decision.noindex = true;
if (r.score !== undefined) decision.substanceScore = r.score;
if (r.metadata?.werkEvidenceCount !== undefined) {
decision.werkEvidenceCount = r.metadata.werkEvidenceCount as number;
}
}
for (const gate of GATE_PRECEDENCE) {
const r = byGate.get(gate);
if (r && !r.pass && r.reason) {
decision.reason = r.reason;
break;
}
}
if (byGate.get("demand")) decision.demandGate = byGate.get("demand")!.pass;
if (byGate.get("evidence")) decision.evidenceGate = byGate.get("evidence")!.pass;
if (byGate.get("substance")) decision.substanceGate = byGate.get("substance")!.pass;
if (byGate.get("freshness")) decision.fresh = byGate.get("freshness")!.pass;
if (byGate.get("budget")) decision.withinBudget = byGate.get("budget")!.pass;
return { decision, suppress };
}
/** Demand gate: checks whether at least one qualifying demand signal exists for this entry. */
export function evaluateDemandGate(
hasMatchingDemand: boolean,
missingPolicy: "noindex" | "do-not-emit" = "noindex",
): GateResult {
if (hasMatchingDemand) {
return { gate: "demand", pass: true, noindex: false };
}
return {
gate: "demand",
pass: false,
reason: "missing-demand",
noindex: true,
suppress: missingPolicy === "do-not-emit",
};
}
/** Evidence gate: checks whether enough Werk evidence records exist for this entry. */
export function evaluateEvidenceGate(
matchingCount: number,
minRequired: number | undefined,
existenceSource: "works" | undefined,
): GateResult {
if (typeof minRequired !== "number" && !existenceSource) {
return { gate: "evidence", pass: true, noindex: false };
}
const pass = typeof minRequired !== "number" || matchingCount >= minRequired;
if (pass) {
return {
gate: "evidence",
pass: true,
noindex: false,
metadata: { werkEvidenceCount: matchingCount },
};
}
return {
gate: "evidence",
pass: false,
reason: "missing-werk-evidence",
noindex: true,
suppress: existenceSource === "works",
metadata: { werkEvidenceCount: matchingCount },
};
}
/** Substance gate: checks whether the page substance score meets the minimum threshold. */
export function evaluateSubstanceGate(score: number, substanceMin: number): GateResult {
if (substanceMin === 0 || score >= substanceMin) {
return { gate: "substance", pass: true, noindex: false, score };
}
return { gate: "substance", pass: false, reason: "thin", noindex: true, score };
}
/** Freshness gate: checks whether the page's records are fresh within the SLA. */
export function evaluateFreshnessGate(fresh: boolean, hasInvalidDate: boolean): GateResult {
if (hasInvalidDate) {
return { gate: "freshness", pass: false, reason: "invalid-freshness", noindex: true };
}
if (fresh) {
return { gate: "freshness", pass: true, noindex: false };
}
return { gate: "freshness", pass: false, reason: "decayed", noindex: true };
}
/** Budget gate: checks whether the entry is within the index budget. */
export function evaluateBudgetGate(withinBudget: boolean): GateResult {
if (withinBudget) {
return { gate: "budget", pass: true, noindex: false };
}
return { gate: "budget", pass: false, reason: "over-budget", noindex: true };
}