# Conflicts: # .agents/notes/archived/process/2026-07-23-browser-demo-gif-recording.i18n.yaml
87 lines
3.0 KiB
TypeScript
87 lines
3.0 KiB
TypeScript
/** Shared repository file discovery and line-oriented reference scanning. */
|
|
|
|
import { globSync, readFileSync, realpathSync } from 'node:fs'
|
|
import { relative, resolve, sep } from 'node:path'
|
|
|
|
/** One authored path plus its canonical target for symlink deduplication. */
|
|
export interface RepoFile {
|
|
/** Absolute path matched by the caller's glob. */
|
|
abs: string
|
|
/** Absolute canonical path used only for deduplication. */
|
|
real: string
|
|
}
|
|
|
|
/** A rejected line-oriented repository reference. */
|
|
export interface ReferenceViolation {
|
|
/** Repo-relative file containing the reference. */
|
|
file: string
|
|
/** 1-based line containing the reference. */
|
|
line: number
|
|
/** Normalized reference text. */
|
|
ref: string
|
|
}
|
|
|
|
/** Whether a repository path is frozen Agent Note history, not evolving source prose. */
|
|
export function isArchivedAgentNotePath(path: string): boolean {
|
|
return path.replaceAll('\\', '/').startsWith('.agents/notes/archived/')
|
|
}
|
|
|
|
/**
|
|
* Expand repository-relative globs and deduplicate symlinked files.
|
|
* @param root - absolute repository root.
|
|
* @param patterns - repository-relative glob patterns, processed in order.
|
|
* @param isExcluded - optional predicate over each matched relative path.
|
|
* @returns matched files in stable first-seen order.
|
|
*/
|
|
export function uniqueRepoFiles(
|
|
root: string,
|
|
patterns: readonly string[],
|
|
isExcluded: (relativePath: string) => boolean = () => false,
|
|
): RepoFile[] {
|
|
const seen = new Set<string>()
|
|
const files: RepoFile[] = []
|
|
for (const pattern of patterns) {
|
|
for (const match of globSync(pattern, { cwd: root })) {
|
|
const repoPath = match.split(sep).join('/')
|
|
if (isExcluded(repoPath)) continue
|
|
const abs = resolve(root, repoPath)
|
|
const real = realpathSync(abs)
|
|
if (seen.has(real)) continue
|
|
seen.add(real)
|
|
files.push({ abs, real })
|
|
}
|
|
}
|
|
return files
|
|
}
|
|
|
|
/**
|
|
* Scan regex matches line by line and return the normalized matches rejected by
|
|
* a caller predicate.
|
|
* @param root - absolute repository root used for violation paths.
|
|
* @param absPath - absolute text-file path to scan.
|
|
* @param pattern - global regex matched independently against each line.
|
|
* @param normalize - maps raw regex text to the reference the gate evaluates.
|
|
* @param isViolation - returns true when the normalized reference is invalid.
|
|
* @returns every rejected reference in source order.
|
|
*/
|
|
export function findReferenceViolations(
|
|
root: string,
|
|
absPath: string,
|
|
pattern: RegExp,
|
|
normalize: (raw: string) => string,
|
|
isViolation: (ref: string) => boolean,
|
|
): ReferenceViolation[] {
|
|
const file = relative(root, absPath).split(sep).join('/')
|
|
const out: ReferenceViolation[] = []
|
|
const lines = readFileSync(absPath, 'utf8').split('\n')
|
|
for (let i = 0; i < lines.length; i++) {
|
|
const line = lines[i]
|
|
if (line === undefined) continue
|
|
for (const match of line.matchAll(pattern)) {
|
|
const ref = normalize(match[0])
|
|
if (isViolation(ref)) out.push({ file, line: i + 1, ref })
|
|
}
|
|
}
|
|
return out
|
|
}
|