| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586 |
- /** 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
- }
|