@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.
162 lines • 8.6 kB
TypeScript
/**
* Shared parsing and reference-validation primitives for the learning-loop fact
* extractors (footguns, lessons, patterns, decisions). Owns the markdown reading,
* frontmatter parsing, freshness computation, and the evidence-reference checks
* that flag stale paths, out-of-bounds line numbers, and broken `(search: ...)`
* anchors.
*
* Reference validation is intentionally conservative: ambiguous shorthand (bare
* source filenames, gitignored task paths, URLs/hostnames) is skipped rather than
* reported, because a false "stale" finding on a clean checkout erodes trust in the
* whole audit. The regexes here are the canonical evidence grammar - footgun and
* lesson extractors must reuse them so the same string is judged identically
* everywhere. ADR-024 governs the line-number-versus-semantic-anchor policy these
* checks enforce.
*/
import type { BucketFreshness, ReadonlyFS } from "../../types.js";
/** Strict YYYY-MM-DD format - rejects full ISO 8601 timestamps in `last_reviewed`. */
export declare const ISO_DATE_REGEX: RegExp;
/**
* Matches file path evidence in multiple formats:
* - `src/auth.ts` (backtick-wrapped file path)
* - `src/auth.ts:42` (backtick-wrapped with line number)
* - `src/auth.ts:42-50` (backtick-wrapped with line range)
* - (lines 866-880) or (line 52) (prose-style)
* Line numbers are discouraged per ADR-024; flagged for cleanup when found alongside a semantic anchor.
* File paths alone remain valid evidence.
*/
export declare const EVIDENCE_PATTERN: RegExp;
/** Regex to extract file paths from backtick-wrapped references (with optional line numbers). */
export declare const FILE_REF_REGEX: RegExp;
/** One markdown file read from a learning-loop directory. */
export interface MarkdownEntry {
path: string;
content: string;
}
/** A learning-loop directory with its existence flag and contained markdown entries. */
export interface EntryDir {
path: string;
exists: boolean;
files: MarkdownEntry[];
}
/** Aggregated file-reference validation results for footgun entries. */
export interface FootgunRefSummary {
staleRefs: string[];
invalidLineRefs: string[];
totalRefs: number;
validRefs: number;
}
/**
* Decide whether a backtick-wrapped reference names a real file path rather than a
* URL or hostname (which share the `host:port` shape). Used to gate staleness
* checks so a `localhost:3000`-style token is never treated as a missing file.
*
* @param filePath - candidate reference text with any trailing `:line` already split off
* @returns true for paths with a slash or a root-level filename extension; false for URLs, hostnames, and bare extensionless names
*/
export declare function isFileRef(filePath: string): boolean;
/**
* Find learning-loop artifact surfaces that exist on disk but sit outside the
* configured canonical location - the signal that a project is splitting one
* concern across two directories. Returns nothing unless a canonical path is
* actually present, so a project that simply hasn't adopted the surface yet is
* not flagged. Trailing slashes are normalized before comparison.
*
* @param fs - read-only filesystem adapter for the target project
* @param canonicalPaths - the configured/blessed locations; at least one must exist or the result is empty
* @param knownPaths - candidate surfaces to test against the canonical set
* @returns existing non-canonical paths, sorted lexicographically for deterministic output; empty when none compete
*/
export declare function findCompetingArtifactSurfaces(fs: ReadonlyFS, canonicalPaths: string[], knownPaths: string[]): string[];
/**
* Read a learning-loop location into a stable, sorted set of markdown entries.
* Handles both config shapes uniformly: a directory (every `.md` except the
* README.md/INDEX.md metadata files, sorted lexicographically) and a single flat
* `.md` file (one entry). INDEX.md is generated bucket metadata (`goat-flow index`),
* not entry content - including it would count phantom legacy entries and force
* entry frontmatter onto a generated file. The sort is load-bearing - downstream
* entry ordering and report output must be deterministic across machines, so
* directory listing order is never trusted.
*
* @param fs - read-only filesystem adapter for the target project
* @param dir - directory path, or a single `.md` file path for flat-file config mode
* @returns the location with its existence flag and entries; files is empty when the location is absent or unreadable
*/
export declare function listMarkdownEntries(fs: ReadonlyFS, dir: string): EntryDir;
/**
* Separate a leading `---`-delimited YAML frontmatter block from the markdown body.
* Recognizes frontmatter only at the very start of the content; a `---` later in
* the document is left in the body untouched.
*
* @param content - raw markdown file content
* @returns the frontmatter text without its `---` fences (null when there is none) and the remaining body
*/
export declare function parseMarkdownFrontmatter(content: string): {
frontmatter: string | null;
body: string;
};
/**
* Parse simple `key: value` pairs from a YAML frontmatter block.
* Only handles flat scalar fields (sufficient for goat-flow's single-level frontmatter);
* nested structures, arrays, and multi-line scalars are intentionally unsupported.
*
* @param frontmatter - YAML frontmatter body without the surrounding `---` markers
* @returns flat key/value fields parsed from the frontmatter block
*/
export declare function parseFrontmatterFields(frontmatter: string): Record<string, string>;
/**
* Compute days-since-review and a coarse freshness band for a bucket file.
* Returns `unknown` for missing or non-YYYY-MM-DD values so callers can flag them.
*
* @param lastReviewed - ISO date from bucket frontmatter, or null when absent
* @param now - comparison clock for deterministic tests and reports
*/
export declare function computeFreshness(lastReviewed: string | null, now?: Date): {
days: number | null;
band: BucketFreshness["freshnessBand"];
};
/**
* Count how many times a pattern matches across a string. Pass a global (`/g`)
* regex - `matchAll` requires it, and without the flag the match count is not what
* a caller expects.
*
* @param content - text to scan
* @param pattern - global regular expression; non-global patterns will throw under matchAll
* @returns the total number of non-overlapping matches; 0 when none match
*/
export declare function countMatches(content: string, pattern: RegExp): number;
/**
* Remove `~~...~~` strikethrough spans before evidence is scanned, so a reference
* an author has struck through (marked as historical) is not counted as live
* evidence. Run this first in every reference check; otherwise retired anchors
* resurface as findings.
*
* @param content - markdown that may contain strikethrough spans, including multi-line ones
* @returns the content with all strikethrough spans removed
*/
export declare function stripStrikethrough(content: string): string;
/**
* Validate every file reference in one footgun section and tally the result.
* Reports a path as stale when the file no longer exists, and flags a `file:line`
* reference when the line is out of bounds, lacks a semantic anchor, or carries a
* line number made redundant by an anchor (the ADR-024 anchor-over-line-number
* contract). Strikethrough is stripped first so struck evidence is ignored.
*
* @param fs - read-only filesystem adapter used to resolve and line-count referenced files
* @param content - the footgun section's markdown
* @returns counts plus the stale-path and invalid-line-reference lists; all empty when every reference is valid
*/
export declare function summarizeFootgunRefs(fs: ReadonlyFS, content: string): FootgunRefSummary;
/**
* Validate the file references in one lesson or pattern section, sharing the same
* staleness and ADR-024 line-reference rules as footguns. Lessons cite full
* project-rooted paths (src/, lib/, docs/, .goat-flow/, ...), so this matches that
* prefix grammar and skips glob-like or `...`-elided tokens that cannot be resolved
* to a single file.
*
* @param fs - read-only filesystem adapter used to resolve and line-count referenced files
* @param content - the lesson or pattern section's markdown
* @returns counts plus the stale-path and invalid-line-reference lists; all empty when every reference is valid
*/
export declare function summarizeLessonRefs(fs: ReadonlyFS, content: string): FootgunRefSummary;
//# sourceMappingURL=learning-loop-common.d.ts.map