import { createHash } from "node:crypto"; import { lstat, readdir, readFile, realpath } from "node:fs/promises"; import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path"; export const FACT_KINDS = [ "command", "reference", "layout", "constraint", "scope-boundary", ] as const; export const DEFAULT_SCAN_LIMITS: Readonly = Object.freeze({ maxDepth: 3, maxEvidenceFiles: 256, maxBoundaryFiles: 32, maxEvidenceBytes: 256 * 1024, }); export const DEFAULT_SCOPE_LIMITS: Readonly = Object.freeze({ maxDepth: 3, maxFiles: 24, maxGeneratedBytes: 192 * 1024, }); export const DEFAULT_DRAFT_BUDGET: Readonly = Object.freeze({ targetWords: 220, hardWords: 260, targetLines: 28, hardLines: 32, targetBytes: 8 * 1024, }); const IGNORED_DIRECTORIES = new Set([ ".git", ".hg", ".svn", ".pi", "node_modules", "vendor", "dist", "build", "coverage", ".next", ".turbo", ]); const EVIDENCE_KINDS = [ "manifest", "lockfile", "task-runner", "config", "ci", "document", "instruction", "boundary", ] as const; export type FactKind = (typeof FACT_KINDS)[number]; export type EvidenceKind = | "manifest" | "lockfile" | "task-runner" | "config" | "ci" | "document" | "instruction" | "boundary"; type PackageManager = "npm" | "pnpm" | "yarn" | "bun"; export interface ScanLimits { readonly maxDepth: number; readonly maxEvidenceFiles: number; readonly maxBoundaryFiles: number; readonly maxEvidenceBytes: number; } export interface ScopeLimits { readonly maxDepth: number; readonly maxFiles: number; readonly maxGeneratedBytes: number; } export interface EvidenceLocation { readonly path: string; readonly startLine: number; readonly endLine: number; readonly kind: EvidenceKind; } export interface RepositoryFact { readonly id: string; readonly kind: FactKind; readonly scope: string; readonly priority: number; readonly value: string; readonly source: EvidenceLocation; } export interface EvidenceFile { readonly path: string; readonly kind: EvidenceKind; } export interface ScanDiagnostic { readonly path: string; readonly message: string; } export interface RepositoryScan { readonly root: string; readonly facts: readonly RepositoryFact[]; readonly evidenceFiles: readonly EvidenceFile[]; readonly humanPolicyFiles: readonly string[]; readonly diagnostics: readonly ScanDiagnostic[]; readonly bounded: boolean; } export interface ScopeDecision { readonly scope: string; readonly selected: boolean; readonly reason: string; readonly factIds: readonly string[]; readonly inheritedFactIds: readonly string[]; } export interface ScopePlan { readonly root: string; readonly decisions: readonly ScopeDecision[]; readonly generatedBytesBudget: number; } export interface DraftBudget { readonly targetWords: number; readonly hardWords: number; readonly targetLines: number; readonly hardLines: number; readonly targetBytes: number; } export interface FactPack { readonly version: 1; readonly root: string; readonly scope: string; readonly targetPath: string; readonly facts: readonly RepositoryFact[]; readonly inheritedFactIds: readonly string[]; readonly inheritedLineCount: number; readonly budget: DraftBudget; } export interface DraftTraceEntry { readonly line: number; readonly factIds: readonly string[]; } export interface Draft { readonly markdown: string; readonly trace?: readonly DraftTraceEntry[]; } export interface ValidationIssue { readonly code: string; readonly severity: "error" | "warning"; readonly message: string; readonly line?: number; } export interface FactPackValidation { readonly valid: boolean; readonly issues: readonly ValidationIssue[]; } export interface DraftValidation { readonly valid: boolean; readonly retryAllowed: boolean; readonly issues: readonly ValidationIssue[]; readonly words: number; readonly lines: number; readonly bytes: number; } interface DiscoveryState { readonly root: string; readonly limits: ScanLimits; readonly files: EvidenceFile[]; readonly policies: string[]; readonly diagnostics: ScanDiagnostic[]; readonly seen: Set; boundaryFiles: number; bounded: boolean; } interface JsonObject { readonly [key: string]: unknown; } export async function collectRepositoryFacts( rootPath: string, limits: Partial = {}, ): Promise { const root = await realpath(rootPath); const rootInfo = await lstat(root); if (!rootInfo.isDirectory()) throw new Error("Repository root must be a directory."); const state: DiscoveryState = { root, limits: { ...DEFAULT_SCAN_LIMITS, ...limits }, files: [], policies: [], diagnostics: [], seen: new Set(), boundaryFiles: 0, bounded: false, }; await discoverEvidence(root, 0, state); const facts = await factsFromEvidence(state); return Object.freeze({ root, facts: Object.freeze(facts), evidenceFiles: Object.freeze([...state.files]), humanPolicyFiles: Object.freeze([...state.policies]), diagnostics: Object.freeze([...state.diagnostics]), bounded: state.bounded, }); } export function planScopes( scan: RepositoryScan, limits: Partial = {}, ): ScopePlan { const resolvedLimits = { ...DEFAULT_SCOPE_LIMITS, ...limits }; const factsByScope = new Map(); for (const fact of scan.facts) { const group = factsByScope.get(fact.scope) ?? []; group.push(fact); factsByScope.set(fact.scope, group); } const decisions: ScopeDecision[] = []; const selected = new Map(); const rootFacts = sortedFacts(factsByScope.get(".") ?? []); decisions.push( Object.freeze({ scope: ".", selected: true, reason: "root is always evaluated", factIds: Object.freeze(rootFacts.map((fact) => fact.id)), inheritedFactIds: Object.freeze([]), }), ); selected.set(".", rootFacts); const candidates = [...factsByScope.keys()] .filter((scope) => scope !== ".") .filter((scope) => hasScopeBoundary(factsByScope.get(scope) ?? [])) .sort(compareScopes); for (const scope of candidates) { const localFacts = sortedFacts(factsByScope.get(scope) ?? []); const parentScopes = [...selected.keys()] .filter( (candidate) => candidate !== scope && isScopeAncestor(candidate, scope), ) .sort(compareScopes); const inheritedFacts = parentScopes.flatMap( (parent) => selected.get(parent) ?? [], ); const inheritedKeys = new Set(inheritedFacts.map(factIdentity)); const deltaFacts = localFacts.filter( (fact) => !inheritedKeys.has(factIdentity(fact)), ); const independentlyEvidenced = uniqueValues( deltaFacts .filter((fact) => fact.kind !== "scope-boundary") .map(factIdentity), ).length >= 2; if (scopeDepth(scope) > resolvedLimits.maxDepth) { decisions.push( rejectedScope( scope, "scope depth exceeds configured limit", inheritedFacts, ), ); continue; } if (!independentlyEvidenced) { decisions.push( rejectedScope( scope, "scope lacks two non-duplicative local facts", inheritedFacts, ), ); continue; } if (selected.size >= resolvedLimits.maxFiles) { decisions.push( rejectedScope(scope, "generated file limit reached", inheritedFacts), ); continue; } const selectedFacts = localFacts.filter( (fact) => fact.kind === "scope-boundary" || deltaFacts.includes(fact), ); decisions.push( Object.freeze({ scope, selected: true, reason: "independently evidenced local delta", factIds: Object.freeze(selectedFacts.map((fact) => fact.id)), inheritedFactIds: Object.freeze(inheritedFacts.map((fact) => fact.id)), }), ); selected.set(scope, selectedFacts); } return Object.freeze({ root: scan.root, decisions: Object.freeze(decisions), generatedBytesBudget: resolvedLimits.maxGeneratedBytes, }); } export function createFactPack( scan: RepositoryScan, plan: ScopePlan, scope: string, options: { readonly inheritedLineCount?: number; readonly budget?: Partial; } = {}, ): FactPack { const decision = plan.decisions.find( (candidate) => candidate.scope === scope && candidate.selected, ); if (!decision) throw new Error("Scope is not selected in this plan."); const factsById = new Map(scan.facts.map((fact) => [fact.id, fact])); const facts = decision.factIds .map((id) => factsById.get(id)) .filter(isPresent); const targetPath = scope === "." ? "AGENTS.md" : `${scope}/AGENTS.md`; return Object.freeze({ version: 1, root: scan.root, scope, targetPath, facts: Object.freeze(sortedFacts(facts)), inheritedFactIds: Object.freeze([...decision.inheritedFactIds]), inheritedLineCount: options.inheritedLineCount ?? 0, budget: Object.freeze({ ...DEFAULT_DRAFT_BUDGET, ...options.budget }), }); } export function validateFactPack(pack: unknown): FactPackValidation { const issues: ValidationIssue[] = []; if (!isRecord(pack)) { return invalidFactPack("pack.shape", "Fact pack must be an object."); } if (pack.version !== 1) issues.push(error("pack.version", "Fact pack version must be 1.")); if (typeof pack.root !== "string" || !isAbsolute(pack.root)) { issues.push(error("pack.root", "Fact pack root must be an absolute path.")); } if (typeof pack.scope !== "string" || !isSafeRepoPath(pack.scope, true)) { issues.push( error("pack.scope", "Fact pack scope must be a contained relative path."), ); } if (typeof pack.targetPath !== "string" || !isSafeRepoPath(pack.targetPath)) { issues.push( error( "pack.target", "Fact pack target must be a contained relative path.", ), ); } if (typeof pack.scope === "string" && typeof pack.targetPath === "string") { const expectedTarget = pack.scope === "." ? "AGENTS.md" : `${pack.scope}/AGENTS.md`; if (toPortablePath(pack.targetPath) !== expectedTarget) { issues.push( error( "pack.target_name", "Fact pack may target only its scope's AGENTS.md.", ), ); } } if (!Array.isArray(pack.facts)) { issues.push(error("pack.facts", "Fact pack facts must be an array.")); } if (Array.isArray(pack.inheritedFactIds)) { const inherited = new Set(); for (const id of pack.inheritedFactIds) { if (typeof id !== "string" || id.length === 0 || inherited.has(id)) { issues.push( error( "pack.inherited_id", "Inherited fact IDs must be unique non-empty strings.", ), ); break; } inherited.add(id); } } else { issues.push( error("pack.inherited", "Fact pack inherited fact IDs must be an array."), ); } if (!isNonNegativeInteger(pack.inheritedLineCount)) { issues.push( error( "pack.inherited_lines", "Inherited line count must be a non-negative integer.", ), ); } validateBudget(pack.budget, issues); if ( !isRecord(pack) || typeof pack.root !== "string" || typeof pack.scope !== "string" || !Array.isArray(pack.facts) ) { return Object.freeze({ valid: false, issues: Object.freeze(issues) }); } const ids = new Set(); for (const candidate of pack.facts) { validateFact(candidate, pack.root, pack.scope, ids, issues); } if (pack.scope !== ".") { const localDeltas = new Set( pack.facts .filter(isRecord) .filter((fact) => fact.kind !== "scope-boundary") .map((fact) => `${String(fact.kind)}\u0000${String(fact.value)}`), ); if (localDeltas.size < 2) { issues.push( error( "pack.child_facts", "Child fact packs require two non-duplicative local facts.", ), ); } } return Object.freeze({ valid: !issues.some((issue) => issue.severity === "error"), issues: Object.freeze(issues), }); } export function validateDraft( pack: unknown, draft: unknown, retryAttempt: number = 0, ): DraftValidation { const packValidation = validateFactPack(pack); const issues = [...packValidation.issues]; if (!packValidation.valid || !isFactPack(pack) || !isDraft(draft)) { if (!isDraft(draft)) { issues.push(error("draft.shape", "Draft must contain Markdown.")); } return freezeDraftValidation(issues, 0, 0, 0, retryAttempt); } const nonBlankLines = draft.markdown .split(/\r?\n/) .filter((line) => line.trim().length > 0); const words = countWords(draft.markdown); const bytes = new TextEncoder().encode(draft.markdown).byteLength; if (nonBlankLines.length === 0) { issues.push(error("draft.empty", "Draft must not be empty.")); } if (words > pack.budget.hardWords) { issues.push( error("budget.words_hard", "Draft exceeds the hard word limit."), ); } else if (words > pack.budget.targetWords) { issues.push( warning("budget.words_target", "Draft exceeds the target word limit."), ); } if (nonBlankLines.length > pack.budget.hardLines) { issues.push( error("budget.lines_hard", "Draft exceeds the hard line limit."), ); } else if (nonBlankLines.length > pack.budget.targetLines) { issues.push( warning("budget.lines_target", "Draft exceeds the target line limit."), ); } if (bytes > pack.budget.targetBytes) { issues.push(error("budget.bytes", "Draft exceeds the byte limit.")); } // OMO-style initialization lets the model write natural repository guidance. // Trace metadata remains accepted for callers that provide it, but it is not a gate. return freezeDraftValidation( issues, words, nonBlankLines.length, bytes, retryAttempt, ); } async function discoverEvidence( directory: string, depth: number, state: DiscoveryState, ): Promise { let entries; try { entries = await readdir(directory, { withFileTypes: true }); } catch { state.diagnostics.push({ path: toPortablePath(relative(state.root, directory)) || ".", message: "Could not read directory.", }); return; } entries.sort((left, right) => left.name.localeCompare(right.name)); for (const entry of entries) { if (state.files.length >= state.limits.maxEvidenceFiles) { state.bounded = true; return; } if (entry.isSymbolicLink()) continue; const absolutePath = join(directory, entry.name); const relativePath = toPortablePath(relative(state.root, absolutePath)); if (!isSafeRepoPath(relativePath)) continue; if (entry.isDirectory()) { if (depth >= state.limits.maxDepth || IGNORED_DIRECTORIES.has(entry.name)) continue; await discoverEvidence(absolutePath, depth + 1, state); continue; } if (!entry.isFile()) continue; const kind = classifyEvidence(relativePath, entry.name); if (!kind) continue; if ( kind === "boundary" && state.boundaryFiles >= state.limits.maxBoundaryFiles ) { state.bounded = true; continue; } if (kind === "boundary") state.boundaryFiles += 1; if (kind === "instruction") { state.policies.push(relativePath); continue; } if (state.seen.has(relativePath)) continue; try { const info = await lstat(absolutePath); if (info.isSymbolicLink() || !info.isFile()) continue; } catch { state.diagnostics.push({ path: relativePath, message: "Could not inspect evidence file.", }); continue; } state.seen.add(relativePath); state.files.push(Object.freeze({ path: relativePath, kind })); } } async function factsFromEvidence( state: DiscoveryState, ): Promise { const rootPackage = state.files.find((file) => file.path === "package.json"); const rootManager = rootPackage ? await packageManagerFromFile(rootPackage, state) : detectLockfileManager(state.files, "."); const facts = new Map(); for (const file of state.files) { const scope = scopeForPath(file.path); if (file.kind === "manifest") { addFact(facts, makeFact("scope-boundary", scope, file.path, file, 1, 90)); if (basename(file.path) === "package.json") { const packageFactsForFile = await packageFacts( file, state, rootManager, ); for (const fact of packageFactsForFile) addFact(facts, fact); } else { addFact(facts, makeFact("reference", scope, file.path, file, 1, 80)); } continue; } if (file.kind === "task-runner") { const taskFacts = await taskRunnerFacts(file, state); for (const fact of taskFacts) addFact(facts, fact); continue; } if (file.kind === "boundary") { addFact(facts, makeFact("layout", scope, file.path, file, 1, 70)); continue; } if (file.kind === "config") { addFact(facts, makeFact("reference", scope, file.path, file, 1, 85)); continue; } if (file.kind === "ci") { addFact(facts, makeFact("reference", scope, file.path, file, 1, 85)); continue; } if (file.kind === "document") { addFact(facts, makeFact("reference", scope, file.path, file, 1, 60)); continue; } if (file.kind === "lockfile") { addFact(facts, makeFact("layout", scope, file.path, file, 1, 75)); } } return sortedFacts([...facts.values()]); } async function packageManagerFromFile( file: EvidenceFile, state: DiscoveryState, ): Promise { const parsed = await readJsonEvidence(file, state); if (!parsed) return detectLockfileManager(state.files, scopeForPath(file.path)); return ( packageManagerFromJson(parsed) ?? detectLockfileManager(state.files, scopeForPath(file.path)) ); } async function packageFacts( file: EvidenceFile, state: DiscoveryState, rootManager: PackageManager | undefined, ): Promise { const parsed = await readJsonEvidence(file, state); if (!parsed) return []; const scope = scopeForPath(file.path); const sourceText = await readEvidenceText(file, state); const explicitManager = packageManagerFromJson(parsed); const manager = explicitManager ?? rootManager ?? detectLockfileManager(state.files, scope); const facts: RepositoryFact[] = []; if (explicitManager) { facts.push( makeFact( "constraint", scope, `Use ${explicitManager} for package scripts.`, file, jsonPropertyLine(sourceText, "packageManager"), 95, ), ); } const scripts = parsed.scripts; if (!manager || !isRecord(scripts)) return facts; for (const [name, value] of Object.entries(scripts).sort(([left], [right]) => left.localeCompare(right), )) { if (typeof value !== "string" || value.trim().length === 0) continue; facts.push( makeFact( "command", scope, commandForScript(manager, name), file, jsonPropertyLine(sourceText, name), 100, ), ); } return facts; } async function taskRunnerFacts( file: EvidenceFile, state: DiscoveryState, ): Promise { const scope = scopeForPath(file.path); const name = basename(file.path).toLowerCase(); if (name !== "makefile" && name !== "justfile") { return [makeFact("reference", scope, file.path, file, 1, 85)]; } const text = await readEvidenceText(file, state); if (text === undefined) return []; const command = name === "makefile" ? "make" : "just"; const facts: RepositoryFact[] = []; for (const [index, line] of text.split(/\r?\n/).entries()) { const match = line.match(/^([A-Za-z0-9][A-Za-z0-9_.-]*):/); if (!match) continue; facts.push( makeFact( "command", scope, `${command} ${match[1]}`, file, index + 1, 100, ), ); } return facts.length > 0 ? facts : [makeFact("reference", scope, file.path, file, 1, 85)]; } async function readJsonEvidence( file: EvidenceFile, state: DiscoveryState, ): Promise { const text = await readEvidenceText(file, state); if (text === undefined) return undefined; try { const parsed: unknown = JSON.parse(text); if (!isRecord(parsed)) { state.diagnostics.push({ path: file.path, message: "Manifest must contain a JSON object.", }); return undefined; } return parsed; } catch { state.diagnostics.push({ path: file.path, message: "Manifest is not valid JSON.", }); return undefined; } } async function readEvidenceText( file: EvidenceFile, state: DiscoveryState, ): Promise { const path = join(state.root, fromPortablePath(file.path)); try { const info = await lstat(path); if ( info.isSymbolicLink() || !info.isFile() || info.size > state.limits.maxEvidenceBytes ) { state.diagnostics.push({ path: file.path, message: "Evidence file is not a bounded regular file.", }); return undefined; } return await readFile(path, "utf8"); } catch { state.diagnostics.push({ path: file.path, message: "Could not read evidence file.", }); return undefined; } } function classifyEvidence( relativePath: string, name: string, ): EvidenceKind | undefined { const lower = name.toLowerCase(); if (lower === "agents.md" || lower === "agents.override.md") return "instruction"; if (relativePath.startsWith(".github/workflows/") && /\.ya?ml$/i.test(name)) return "ci"; if ( lower === "package.json" || [ "pyproject.toml", "cargo.toml", "go.mod", "pom.xml", "build.gradle", "build.gradle.kts", "composer.json", "gemfile", ].includes(lower) ) { return "manifest"; } if ( [ "pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock", "bun.lockb", "cargo.lock", "poetry.lock", "composer.lock", ].includes(lower) ) { return "lockfile"; } if (["makefile", "justfile", "taskfile.yml", "taskfile.yaml"].includes(lower)) return "task-runner"; if (isConfigFile(lower)) return "config"; if (isCanonicalDocument(relativePath, lower)) return "document"; if (isBoundaryFile(name)) return "boundary"; return undefined; } function isConfigFile(name: string): boolean { return ( name === "biome.json" || name === "deno.json" || name === "turbo.json" || name === "nx.json" || name === "ruff.toml" || name === "pytest.ini" || (name.startsWith("tsconfig") && name.endsWith(".json")) || name.startsWith("eslint.config.") || name.startsWith("prettier.config.") || name.startsWith("vitest.config.") || name.startsWith("jest.config.") || name.startsWith("playwright.config.") || name.startsWith("vite.config.") || name.startsWith("webpack.config.") || name.startsWith(".eslintrc") || name.startsWith(".prettierrc") ); } function isCanonicalDocument(relativePath: string, name: string): boolean { if ( /^(readme|contributing|architecture|security|development|testing)(\.[a-z0-9]+)?$/i.test( name, ) ) return true; return relativePath.startsWith("docs/") && /\.(md|mdx|rst)$/i.test(name); } function isBoundaryFile(name: string): boolean { return /^(index|main|app|server|cli|mod)\.[A-Za-z0-9]+$/.test(name); } function detectLockfileManager( files: readonly EvidenceFile[], scope: string, ): PackageManager | undefined { const names = new Set( files .filter((file) => scopeForPath(file.path) === scope) .map((file) => basename(file.path).toLowerCase()), ); if (names.has("pnpm-lock.yaml")) return "pnpm"; if (names.has("yarn.lock")) return "yarn"; if (names.has("bun.lock") || names.has("bun.lockb")) return "bun"; if (names.has("package-lock.json")) return "npm"; return undefined; } function packageManagerFromJson(json: JsonObject): PackageManager | undefined { if (typeof json.packageManager !== "string") return undefined; const name = json.packageManager.split("@", 1)[0]; return name === "npm" || name === "pnpm" || name === "yarn" || name === "bun" ? name : undefined; } function commandForScript(manager: PackageManager, name: string): string { if (manager === "npm") return `npm run ${name}`; if (manager === "pnpm") return `pnpm run ${name}`; if (manager === "yarn") return `yarn ${name}`; return `bun run ${name}`; } function makeFact( kind: FactKind, scope: string, value: string, file: EvidenceFile, line: number, priority: number, ): RepositoryFact { const source = Object.freeze({ path: file.path, startLine: Math.max(1, line), endLine: Math.max(1, line), kind: file.kind, }); const id = `${kind}:${sha256(`${scope}\u0000${value}\u0000${file.path}\u0000${source.startLine}`)}`; return Object.freeze({ id, kind, scope, priority, value, source }); } function addFact( facts: Map, fact: RepositoryFact, ): void { const key = `${fact.kind}\u0000${fact.scope}\u0000${fact.value}`; const existing = facts.get(key); if (!existing || fact.priority > existing.priority) facts.set(key, fact); } function rejectedScope( scope: string, reason: string, inherited: readonly RepositoryFact[], ): ScopeDecision { return Object.freeze({ scope, selected: false, reason, factIds: Object.freeze([]), inheritedFactIds: Object.freeze(inherited.map((fact) => fact.id)), }); } function validateFact( candidate: unknown, root: string, scope: string, ids: Set, issues: ValidationIssue[], ): void { if (!isRecord(candidate)) { issues.push(error("fact.shape", "Fact must be an object.")); return; } if (typeof candidate.id !== "string" || candidate.id.length === 0) { issues.push(error("fact.id", "Fact ID must be a non-empty string.")); } else if (ids.has(candidate.id)) { issues.push(error("fact.duplicate_id", "Fact IDs must be unique.")); } else { ids.add(candidate.id); } if (!FACT_KINDS.includes(candidate.kind as FactKind)) { issues.push(error("fact.kind", "Fact kind is not allowed.")); } if (candidate.scope !== scope) { issues.push( error("fact.scope", "Fact scope must match the fact pack scope."), ); } if ( typeof candidate.value !== "string" || candidate.value.length === 0 || candidate.value.includes("\n") ) { issues.push(error("fact.value", "Fact value must be one non-empty line.")); } if (!isNonNegativeInteger(candidate.priority)) { issues.push( error("fact.priority", "Fact priority must be a non-negative integer."), ); } if (!isRecord(candidate.source)) { issues.push(error("fact.source", "Fact source is required.")); return; } const source = candidate.source; if (typeof source.path !== "string" || !isSafeRepoPath(source.path)) { issues.push( error( "fact.source_path", "Fact source must be a contained relative path.", ), ); } else if (!isContained(root, resolve(root, fromPortablePath(source.path)))) { issues.push( error( "fact.source_containment", "Fact source escapes the repository root.", ), ); } else if ( !isContained( resolve(root, fromPortablePath(scope)), resolve(root, fromPortablePath(source.path)), ) ) { issues.push( error("fact.source_scope", "Fact source escapes the fact pack scope."), ); } if ( !isPositiveInteger(source.startLine) || !isPositiveInteger(source.endLine) || source.endLine < source.startLine ) { issues.push( error("fact.source_lines", "Fact source lines must be a valid range."), ); } if (!EVIDENCE_KINDS.includes(source.kind as EvidenceKind)) { issues.push(error("fact.source_kind", "Fact source kind is not allowed.")); } } function validateBudget(value: unknown, issues: ValidationIssue[]): void { if (!isRecord(value)) { issues.push(error("pack.budget", "Fact pack budget must be an object.")); return; } const targetWords = value.targetWords; const hardWords = value.hardWords; const targetLines = value.targetLines; const hardLines = value.hardLines; const targetBytes = value.targetBytes; if ( !isPositiveInteger(targetWords) || !isPositiveInteger(hardWords) || targetWords > hardWords || hardWords > 260 ) { issues.push(error("pack.budget_words", "Word budget is invalid.")); } if ( !isPositiveInteger(targetLines) || !isPositiveInteger(hardLines) || targetLines > hardLines || hardLines > 32 ) { issues.push(error("pack.budget_lines", "Line budget is invalid.")); } if (!isPositiveInteger(targetBytes) || targetBytes > 8 * 1024) { issues.push(error("pack.budget_bytes", "Byte budget is invalid.")); } } function freezeDraftValidation( issues: ValidationIssue[], words: number, lines: number, bytes: number, retryAttempt: number, ): DraftValidation { const errors = issues.filter((issue) => issue.severity === "error"); const retryAllowed = retryAttempt === 0 && errors.length > 0 && errors.every(isStructuralIssue); return Object.freeze({ valid: errors.length === 0, retryAllowed, issues: Object.freeze(issues), words, lines, bytes, }); } function isStructuralIssue(issue: ValidationIssue): boolean { return ( issue.code === "draft.empty" || issue.code === "heading.allowlist" || issue.code === "structure.bullet" ); } function sortedFacts(facts: readonly RepositoryFact[]): RepositoryFact[] { return [...facts].sort( (left, right) => right.priority - left.priority || left.scope.localeCompare(right.scope) || left.id.localeCompare(right.id), ); } function compareScopes(left: string, right: string): number { return scopeDepth(left) - scopeDepth(right) || left.localeCompare(right); } function scopeForPath(path: string): string { const parent = dirname(fromPortablePath(path)); return parent === "." ? "." : toPortablePath(parent); } function scopeDepth(scope: string): number { return scope === "." ? 0 : scope.split("/").length; } function hasScopeBoundary(facts: readonly RepositoryFact[]): boolean { return facts.some((fact) => fact.kind === "scope-boundary"); } function isScopeAncestor(ancestor: string, scope: string): boolean { return ( ancestor === "." || scope === ancestor || scope.startsWith(`${ancestor}/`) ); } function factIdentity(fact: RepositoryFact): string { return `${fact.kind}\u0000${fact.value}`; } function uniqueValues(values: readonly string[]): string[] { return [...new Set(values)]; } function jsonPropertyLine(text: string | undefined, key: string): number { if (!text) return 1; const expression = new RegExp(`^\\s*"${escapeRegex(key)}"\\s*:`, "m"); const match = expression.exec(text); if (!match || match.index === undefined) return 1; return text.slice(0, match.index).split(/\r?\n/).length; } function countWords(value: string): number { return value.trim().match(/\S+/g)?.length ?? 0; } function sha256(value: string): string { return createHash("sha256").update(value).digest("hex").slice(0, 20); } function toPortablePath(path: string): string { return path.split(sep).join("/"); } function fromPortablePath(path: string): string { return path.split("/").join(sep); } function isSafeRepoPath(path: string, allowRoot: boolean = false): boolean { const portable = toPortablePath(path); if (allowRoot && portable === ".") return true; if ( portable.length === 0 || portable.startsWith("/") || portable.includes("//") ) return false; return portable .split("/") .every((part) => part.length > 0 && part !== "." && part !== ".."); } function isContained(root: string, target: string): boolean { const child = relative(root, target); return ( child === "" || (!isAbsolute(child) && child !== ".." && !child.startsWith(`..${sep}`)) ); } function isPositiveInteger(value: unknown): value is number { return typeof value === "number" && Number.isSafeInteger(value) && value > 0; } function isNonNegativeInteger(value: unknown): value is number { return typeof value === "number" && Number.isSafeInteger(value) && value >= 0; } function isRecord(value: unknown): value is JsonObject { return typeof value === "object" && value !== null && !Array.isArray(value); } function isFactPack(value: unknown): value is FactPack { return ( isRecord(value) && value.version === 1 && typeof value.root === "string" && typeof value.scope === "string" && typeof value.targetPath === "string" && Array.isArray(value.facts) && Array.isArray(value.inheritedFactIds) && isNonNegativeInteger(value.inheritedLineCount) && isRecord(value.budget) ); } function isDraft(value: unknown): value is Draft { return ( isRecord(value) && typeof value.markdown === "string" && (value.trace === undefined || (Array.isArray(value.trace) && value.trace.every( (entry) => isRecord(entry) && isNonNegativeInteger(entry.line) && Array.isArray(entry.factIds), ))) ); } function isPresent(value: T | undefined): value is T { return value !== undefined; } function error(code: string, message: string, line?: number): ValidationIssue { return line === undefined ? { code, severity: "error", message } : { code, severity: "error", message, line }; } function warning( code: string, message: string, line?: number, ): ValidationIssue { return line === undefined ? { code, severity: "warning", message } : { code, severity: "warning", message, line }; } function invalidFactPack(code: string, message: string): FactPackValidation { return Object.freeze({ valid: false, issues: Object.freeze([error(code, message)]), }); } function escapeRegex(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }