/** * pi-goal-list-loop-audit — v0.2.0 * extensions/goal-loop-shield.ts * * regression_shield — pure, dependency-free enforcement logic. * * When a goal has a verification contract, an verdict is only * accepted if the auditor's report carries an section that * references every contract item. This kills the "auditor ran bash true and * approved" class of bamboozle that pi-goal-x's author explicitly documented * as a known hole. * * Kept free of pi imports so unit tests can exercise it under plain node. */ /** Split a verification contract into its individual checkable items. */ export function contractItems(contract: string): string[] { return contract .split("\n") .map((l) => l.trim()) .map((l) => l.replace(/^(?:done when|verify|verified when|verification|done)\s*:\s*/i, "")) .map((l) => l.replace(/^[-*•]\s+/, "").replace(/^\d+[.)]\s+/, "")) .filter((l) => l.length > 0); } export interface RegressionShieldResult { passed: boolean; missingItems: string[]; hasEvidenceBlock: boolean; } /** * Check an approved auditor report against the verification contract. * Rules (deliberately simple + auditable): * 1. The report must contain an ... block. * 2. Every contract item must be referenced inside the report by a * distinctive token (the item's longest word >= 5 chars, or the full * item if shorter) — a cheap, honest proxy for "the auditor addressed * this item". */ export function checkRegressionShield(report: string, contract: string): RegressionShieldResult { const hasEvidenceBlock = /[\t\n\r ]*[\s\S]*?<\/evidence>/i.test(report); const items = contractItems(contract); const missingItems: string[] = []; const reportLower = report.toLowerCase(); for (const item of items) { // Distinctive token: longest word >= 5 chars in the item; fall back to the // whole item (short items like "npm test" are matched whole). const words = item.split(/[^A-Za-z0-9_.\-/]+/).filter(Boolean); const distinctive = words.reduce((a, b) => (b.length >= 5 && b.length > a.length ? b : a), ""); const needle = (distinctive || item).toLowerCase(); if (!reportLower.includes(needle)) missingItems.push(item); } return { passed: hasEvidenceBlock && missingItems.length === 0, missingItems, hasEvidenceBlock, }; }