UNPKG

@phren/cli

Version:

Knowledge layer for AI agents - CLI, MCP server, and data layer

197 lines (196 loc) • 10.7 kB
/** * Consolidated metadata regex patterns and parsing helpers for HTML comment * metadata embedded in FINDINGS.md and related files. * * `phren:` prefixes are used for HTML comment metadata. */ // --------------------------------------------------------------------------- // Prefix pattern (shared across all metadata types) // --------------------------------------------------------------------------- /** Matches `phren` as a comment prefix. */ const PREFIX = `phren`; // --------------------------------------------------------------------------- // Raw regex patterns — exported for direct use when the helpers don't fit // --------------------------------------------------------------------------- export const METADATA_REGEX = { /** Matches `<!-- phren:status "active" -->` or `<!-- phren:status "superseded" -->` etc. */ status: new RegExp(`<!--\\s*${PREFIX}:status\\s+"?(active|superseded|contradicted|stale|invalid_citation|retracted)"?\\s*-->`, "i"), /** Matches `<!-- phren:status_updated "2025-01-01" -->` */ statusUpdated: new RegExp(`<!--\\s*${PREFIX}:status_updated\\s+"([^"]+)"\\s*-->`, "i"), /** Matches `<!-- phren:status_reason "superseded_by" -->` */ statusReason: new RegExp(`<!--\\s*${PREFIX}:status_reason\\s+"([^"]+)"\\s*-->`, "i"), /** Matches `<!-- phren:status_ref "some ref" -->` */ statusRef: new RegExp(`<!--\\s*${PREFIX}:status_ref\\s+"([^"]+)"\\s*-->`, "i"), /** Generic field matcher factory for status_updated / status_reason / status_ref. */ statusField(field) { return new RegExp(`<!--\\s*${PREFIX}:${field}\\s+"([^"]+)"\\s*-->`, "i"); }, /** Raw (unquoted) fallback for status fields: `<!-- phren:status_ref some text -->` */ statusFieldRaw(field) { return new RegExp(`<!--\\s*${PREFIX}:${field}\\s+([^>]+?)\\s*-->`, "i"); }, /** Matches `<!-- phren:superseded_by "text" 2025-01-01 -->` */ supersededBy: new RegExp(`<!--\\s*${PREFIX}:superseded_by\\s+"([^"]+)"(?:\\s+([0-9]{4}-[0-9]{2}-[0-9]{2}))?\\s*-->`, "i"), /** Legacy `<!-- superseded_by: "text" -->` */ supersededByLegacy: /<!--\s*superseded_by:\s*"([^"]+)"\s*-->/i, /** Matches `<!-- phren:supersedes "text" -->` */ supersedes: new RegExp(`<!--\\s*${PREFIX}:supersedes\\s+"([^"]+)"\\s*-->`, "i"), /** Matches `<!-- phren:contradicts "text" -->` */ contradicts: new RegExp(`<!--\\s*${PREFIX}:contradicts\\s+"([^"]+)"\\s*-->`, "i"), /** Global version for matchAll */ contradictsAll: new RegExp(`<!--\\s*phren:contradicts\\s+"([^"]+)"\\s*-->`, "g"), /** Legacy `<!-- conflicts_with: "text" -->` or `<!-- conflicts_with: "text" (from project: foo) -->` */ conflictsWith: /<!--\s*conflicts_with:\s*"([^"]+)"(?:\s*\(from project:\s*[^)]+\))?\s*-->/i, /** Global version for matchAll on conflicts_with */ conflictsWithAll: /<!--\s*conflicts_with:\s*"([^"]+)"(?:\s*\(from project:\s*[^)]+\))?\s*-->/g, /** Matches `<!-- phren:cite {...} -->` or `<!-- phren:cite {...} -->` on a full line. */ citation: /^\s*<!--\s*phren:cite\s+\{.*\}\s*-->\s*$/, /** Matches the opening marker (not line-anchored) for extracting JSON payload. */ citationMarker: /<!--\s*phren:cite\s+/, /** Matches `<!-- phren:archive:start -->` or `<!-- phren:archive:start -->` */ archiveStart: new RegExp(`<!--\\s*${PREFIX}:archive:start\\s*-->`), /** Matches `<!-- phren:archive:end -->` or `<!-- phren:archive:end -->` */ archiveEnd: new RegExp(`<!--\\s*${PREFIX}:archive:end\\s*-->`), /** Matches `<!-- fid:abcd1234 -->` */ findingId: /<!--\s*fid:([a-z0-9]{8})\s*-->/i, /** Matches `<!-- created: 2025-01-01 -->` */ createdDate: /<!--\s*created:\s*([0-9]{4}-[0-9]{2}-[0-9]{2})\s*-->/i, /** Matches any lifecycle annotation: status, status_updated, status_reason, status_ref */ lifecycleAnnotation: new RegExp(`<!--\\s*${PREFIX}:status(?:_updated|_reason|_ref)?\\b[^>]*-->`, "i"), /** Matches `<!-- source:... -->` */ source: /<!--\s*source:\s*(.*?)\s*-->/, /** Matches any HTML comment `<!-- ... -->` (non-greedy). */ anyComment: /<!--.*?-->/g, // Strip patterns (with leading optional whitespace for clean removal) /** Strip status comment */ stripStatus: new RegExp(`\\s*<!--\\s*${PREFIX}:status\\s+"?(?:active|superseded|contradicted|stale|invalid_citation|retracted)"?\\s*-->`, "gi"), /** Strip status_updated comment */ stripStatusUpdated: new RegExp(`\\s*<!--\\s*${PREFIX}:status_updated\\s+"[^"]+"\\s*-->`, "gi"), /** Strip status_reason comment */ stripStatusReason: new RegExp(`\\s*<!--\\s*${PREFIX}:status_reason\\s+"[^"]+"\\s*-->`, "gi"), /** Strip status_ref comment */ stripStatusRef: new RegExp(`\\s*<!--\\s*${PREFIX}:status_ref\\s+"[^"]+"\\s*-->`, "gi"), /** Strip legacy `<!-- superseded_by: "..." -->` */ stripSupersededByLegacy: /\s*<!--\s*superseded_by:\s*"[^"]+"\s*-->/gi, /** Strip `<!-- phren:superseded_by "..." ... -->` */ stripSupersededBy: new RegExp(`\\s*<!--\\s*${PREFIX}:superseded_by\\s+"[^"]+"(?:\\s+[0-9]{4}-[0-9]{2}-[0-9]{2})?\\s*-->`, "gi"), /** Strip `<!-- phren:supersedes "..." -->` */ stripSupersedes: new RegExp(`\\s*<!--\\s*${PREFIX}:supersedes\\s+"[^"]+"\\s*-->`, "gi"), /** Strip legacy `<!-- conflicts_with: "..." -->` */ stripConflictsWith: /\s*<!--\s*conflicts_with:\s*"[^"]+"(?:\s*\(from project:\s*[^)]+\))?\s*-->/gi, /** Strip `<!-- phren:contradicts "..." -->` */ stripContradicts: new RegExp(`\\s*<!--\\s*${PREFIX}:contradicts\\s+"[^"]+"\\s*-->`, "gi"), }; // --------------------------------------------------------------------------- // Parsing helpers // --------------------------------------------------------------------------- /** Parse `<!-- phren:status "active" -->` from a line. Returns the status string or undefined. */ export function parseStatus(line) { return line.match(METADATA_REGEX.status)?.[1]?.toLowerCase(); } /** Parse a quoted status field (status_updated, status_reason, status_ref) from a line. */ export function parseStatusField(line, field) { const quoted = line.match(METADATA_REGEX.statusField(field))?.[1]; if (quoted) return quoted.replace(/\s+/g, " ").trim(); const raw = line.match(METADATA_REGEX.statusFieldRaw(field))?.[1]; return raw ? raw.replace(/\s+/g, " ").trim() : undefined; } /** Parse supersession metadata: returns `{ ref, date }` or null. Checks both prefixed and legacy forms. */ export function parseSupersession(line) { const prefixed = line.match(METADATA_REGEX.supersededBy); if (prefixed) return { ref: prefixed[1], date: prefixed[2] }; const legacy = line.match(METADATA_REGEX.supersededByLegacy); if (legacy) return { ref: legacy[1] }; return null; } /** Parse `<!-- phren:supersedes "..." -->` from a line. Returns the ref or undefined. */ export function parseSupersedesRef(line) { return line.match(METADATA_REGEX.supersedes)?.[1]; } /** Parse contradiction metadata. Checks both prefixed `contradicts` and legacy `conflicts_with`. */ export function parseContradiction(line) { const prefixed = line.match(METADATA_REGEX.contradicts); if (prefixed) return prefixed[1]; const legacy = line.match(METADATA_REGEX.conflictsWith); if (legacy) return legacy[1]; return null; } /** Parse all contradiction refs from a line using matchAll. */ export function parseAllContradictions(line) { return [...line.matchAll(METADATA_REGEX.contradictsAll)].map((m) => m[1]); } /** Parse `<!-- fid:XXXXXXXX -->` from a line. Returns the 8-char hex ID or undefined. */ export function parseFindingId(line) { return line.match(METADATA_REGEX.findingId)?.[1]; } /** Parse `<!-- created: YYYY-MM-DD -->` from a line. Returns the date string or undefined. */ export function parseCreatedDate(line) { return line.match(METADATA_REGEX.createdDate)?.[1]; } /** Check if a line (or next line) contains a citation comment. */ export function isCitationLine(line) { return METADATA_REGEX.citation.test(line.trim()); } /** Check if a line marks the start of an archive block. */ export function isArchiveStart(line) { return METADATA_REGEX.archiveStart.test(line) || /^<details(?:\s|>)/i.test(line.trim()); } /** Check if a line marks the end of an archive block. */ export function isArchiveEnd(line) { return METADATA_REGEX.archiveEnd.test(line) || /^<\/details>/i.test(line.trim()); } // --------------------------------------------------------------------------- // Strip helpers — remove metadata comments from a line // --------------------------------------------------------------------------- /** Strip all lifecycle status comments (status, status_updated, status_reason, status_ref). */ export function stripLifecycleMetadata(line) { return line .replace(METADATA_REGEX.stripStatus, "") .replace(METADATA_REGEX.stripStatusUpdated, "") .replace(METADATA_REGEX.stripStatusReason, "") .replace(METADATA_REGEX.stripStatusRef, ""); } /** Strip all relation comments (superseded_by, supersedes, conflicts_with, contradicts). */ export function stripRelationMetadata(line) { return line .replace(METADATA_REGEX.stripSupersededByLegacy, "") .replace(METADATA_REGEX.stripSupersededBy, "") .replace(METADATA_REGEX.stripSupersedes, "") .replace(METADATA_REGEX.stripConflictsWith, "") .replace(METADATA_REGEX.stripContradicts, ""); } /** Strip all phren/phren metadata comments from a line. */ export function stripAllMetadata(line) { return stripRelationMetadata(stripLifecycleMetadata(line)); } /** Strip all HTML comments from text. */ export function stripComments(text) { return text.replace(METADATA_REGEX.anyComment, "").trim(); } /** Normalize finding text for comparison: strips bullet prefix, HTML comments, confidence tags, normalizes whitespace, lowercases. */ export function normalizeFindingText(raw) { return raw .replace(/^-\s+/, "") .replace(/<!--.*?-->/g, " ") .replace(/\[confidence\s+[01](?:\.\d+)?\]/gi, " ") .replace(/\s+/g, " ") .trim() .toLowerCase(); } // --------------------------------------------------------------------------- // Add helpers — append metadata comments to a line // --------------------------------------------------------------------------- /** Build a metadata comment string. */ export function addMetadata(type, value, extra) { const prefix = "phren"; const escaped = value.replace(/"/g, "'"); if (extra) { return `<!-- ${prefix}:${type} "${escaped}" ${extra} -->`; } return `<!-- ${prefix}:${type} "${escaped}" -->`; }