Files
harness-tools/extensions/workflow/ui-policy.ts
T

112 lines
2.5 KiB
TypeScript

import type { UiImpact, WorkflowFacts } from "./domain.ts";
const IMPACT_RANK: Record<UiImpact, number> = {
none: 0,
minor: 1,
"non-trivial": 2,
};
const NON_TRIVIAL_TERMS = new Set([
"accessibility",
"a11y",
"component",
"dialog",
"editor",
"form",
"layout",
"modal",
"navigation",
"onboarding",
"page",
"redesign",
"responsive",
"settings",
"sidebar",
"wizard",
]);
const NON_TRIVIAL_PATH_PARTS = [
"/+page.",
"/dialog.",
"/form.",
"/layout.",
"/modal.",
"/navigation.",
"/nav.",
"/settings.",
"/shell.",
"/sidebar.",
"/accessibility/",
"/onboarding/",
"/settings/",
];
const UI_SOURCE_SUFFIXES = [
".astro",
".css",
".html",
".jsx",
".less",
".scss",
".svelte",
".tsx",
".vue",
];
export interface UiImpactPolicyInput {
plannerImpact: UiImpact;
plannerEvidence: string[];
affectedSurfaces: string[];
facts: WorkflowFacts;
}
export interface UiImpactPolicyResult {
impact: UiImpact;
evidence: string[];
affectedSurfaces: string[];
objectiveImpact: UiImpact;
}
function objectiveEvidence(facts: WorkflowFacts) {
const evidence: string[] = [];
for (const term of facts.requestTerms) {
if (NON_TRIVIAL_TERMS.has(term.toLowerCase())) {
evidence.push(`Request includes non-trivial UI signal: ${term}`);
}
}
for (const candidatePath of facts.candidatePaths) {
const normalized = `/${candidatePath.toLowerCase().replaceAll("\\", "/")}`;
const isUiSource = UI_SOURCE_SUFFIXES.some((suffix) =>
normalized.endsWith(suffix),
);
if (
isUiSource &&
NON_TRIVIAL_PATH_PARTS.some((part) => normalized.includes(part))
) {
evidence.push(`Affected path has non-trivial UI responsibility: ${candidatePath}`);
}
}
for (const tag of [...facts.tags, ...facts.riskTags]) {
if (NON_TRIVIAL_TERMS.has(tag.toLowerCase())) {
evidence.push(`Repository evidence includes non-trivial UI signal: ${tag}`);
}
}
return evidence;
}
export function enforceUiImpact(input: UiImpactPolicyInput): UiImpactPolicyResult {
const objective = objectiveEvidence(input.facts);
const objectiveImpact: UiImpact =
objective.length > 0 ? "non-trivial" : input.plannerImpact;
const impact =
IMPACT_RANK[objectiveImpact] > IMPACT_RANK[input.plannerImpact]
? objectiveImpact
: input.plannerImpact;
return {
impact,
objectiveImpact,
evidence: [...new Set([...input.plannerEvidence, ...objective])],
affectedSurfaces: [...new Set(input.affectedSurfaces)],
};
}