UNPKG

@blundergoat/goat-flow

Version:

AI coding agent harness and local dashboard for Claude Code, OpenAI Codex, Google Antigravity, and GitHub Copilot - setup audits, guardrails, structured skills, deny hooks, and persistent learning loops.

105 lines 5.03 kB
/** * Splits a footgun bucket into its individual `## Footgun:` sections and audits * each one's structure: that a Status field exists and is canonical, that active * entries sit above the `## Resolved Entries` marker (and resolved entries below * it), that active entries carry file:line or `(search: ...)` evidence, and that * they do not cite retired-file evidence. * * This is the schema enforcer behind the footguns README contract: section order, * the active/resolved boundary, and the evidence requirement are the invariants a * reviewer relies on. It only reports diagnostic strings; it never mutates the * bucket or throws on malformed input. */ import { EVIDENCE_PATTERN, parseMarkdownFrontmatter, stripStrikethrough, } from "./learning-loop-common.js"; /** * Slice a footgun bucket body at each `## Footgun:` heading into discrete * sections, capturing each section's source offset (so callers can compare it * against the resolved marker) and its lowercased Status value. * * @param body - bucket markdown with frontmatter already stripped; section bounds run heading-to-next-heading * @returns one section per heading in document order; status is null when the section has no Status field */ export function splitFootgunSections(body) { const headings = Array.from(body.matchAll(/^##\s+Footgun:\s+(.+)$/gm), (match) => ({ title: (match[1] ?? "").trim(), start: match.index, })); return headings.map((heading, index) => { const end = headings[index + 1]?.start ?? body.length; const content = body.slice(heading.start, end); const statusMatch = content.match(/\*\*Status:\*\*\s*([^|\n]+)/i); return { title: heading.title, start: heading.start, content, status: statusMatch?.[1] !== undefined ? statusMatch[1].trim().toLowerCase() : null, }; }); } /** Detect whether a footgun section has file:line or (search: ...) evidence. */ function hasSectionEvidence(content) { return EVIDENCE_PATTERN.test(content) || /\(search:/i.test(content); } /** Check one active footgun section for placement, evidence, and retired-file patterns. */ function diagnoseActiveSection(section, path, resolvedIndex) { const out = []; if (resolvedIndex !== -1 && section.start > resolvedIndex) { out.push(`${path} has active footgun "${section.title}" below ## Resolved Entries`); } if (!hasSectionEvidence(section.content)) { out.push(`${path} active footgun "${section.title}" missing file:line or (search: ...) evidence`); } const cleaned = stripStrikethrough(section.content); if (/\(file retired/i.test(cleaned) || /\bretired in v\d/i.test(cleaned)) { out.push(`${path} active footgun "${section.title}" uses retired-file evidence`); } return out; } /** Check that resolved footguns live below the bucket's resolved marker. */ function diagnoseResolvedSection(section, path, resolvedIndex) { if (resolvedIndex === -1) { return [ `${path} has resolved footgun "${section.title}" but no ## Resolved Entries marker`, ]; } if (section.start < resolvedIndex) { return [ `${path} has resolved footgun "${section.title}" above ## Resolved Entries`, ]; } return []; } /** Check one footgun section's schema + (if active) its placement and evidence. */ function diagnoseFootgunSection(section, path, resolvedIndex) { if (section.status === null) { return [`${path} footgun "${section.title}" missing Status field`]; } // Schema: status must be exactly "active" or "resolved" (machine-simple per footguns/README.md:14) if (section.status !== "active" && section.status !== "resolved") { return [ `${path} footgun "${section.title}" has non-canonical status "${section.status}" (expected "active" or "resolved")`, ]; } if (section.status === "active") { return diagnoseActiveSection(section, path, resolvedIndex); } return diagnoseResolvedSection(section, path, resolvedIndex); } /** * Audit one footgun bucket and return every structure, schema, and evidence * violation as a human-readable diagnostic. The path is woven into each message * so a caller aggregating many buckets can attribute findings without extra state. * * @param path - bucket path used to prefix each diagnostic for attribution * @param content - raw bucket file content; frontmatter is parsed off internally * @returns one diagnostic per violation; an empty array means the bucket is well-formed */ export function collectFootgunStructureDiagnostics(path, content) { const { body } = parseMarkdownFrontmatter(content); const resolvedIndex = body.indexOf("## Resolved Entries"); const sections = splitFootgunSections(body); return sections.flatMap((section) => diagnoseFootgunSection(section, path, resolvedIndex)); } //# sourceMappingURL=learning-loop-sections.js.map