| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697 |
- /** Reject one ambiguous origin label from maintained tracked files. */
- import { execFileSync } from 'node:child_process'
- import { lstatSync, readFileSync, readlinkSync } from 'node:fs'
- import { resolve } from 'node:path'
- import { pathToFileURL } from 'node:url'
- const root = resolve(import.meta.dirname, '..')
- const blockedTerm = 'prove' + 'nance'
- const excludedPrefixes = ['vendor/', '.agents/notes/archived/'] as const
- /** One blocked term occurrence in a tracked path or text line. */
- export interface ConcreteTermViolation {
- /** Repository-relative tracked path. */
- file: string
- /** One-based source line, or null when the path contains the term. */
- line: number | null
- }
- function isExcluded(file: string): boolean {
- return excludedPrefixes.some(prefix => file.startsWith(prefix))
- }
- function containsBlockedTerm(value: string): boolean {
- return value.normalize('NFKC').toLowerCase().includes(blockedTerm)
- }
- /**
- * Find the blocked term in one maintained tracked file.
- * @param file - repository-relative tracked path.
- * @param source - text contents or symlink target.
- * @returns path and line violations; vendored sources and frozen Agent Notes are excluded.
- */
- export function findConcreteTermViolations(file: string, source: string): ConcreteTermViolation[] {
- if (isExcluded(file)) return []
- const violations: ConcreteTermViolation[] = []
- if (containsBlockedTerm(file)) violations.push({ file, line: null })
- for (const [index, line] of source.split('\n').entries()) {
- if (containsBlockedTerm(line)) violations.push({ file, line: index + 1 })
- }
- return violations
- }
- function trackedFiles(repoRoot: string): string[] {
- const files = execFileSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf8' })
- .split('\0')
- .filter(file => file !== '')
- if (!files.includes('AGENTS.md')
- || !files.some(file => file.startsWith('packages/'))
- || !files.some(file => file.startsWith('docs/'))
- || !files.some(file => file.startsWith('vendor/'))
- || !files.some(file => file.startsWith('.agents/notes/archived/'))) {
- throw new Error('verify-concrete-terms: tracked-file discovery omitted a required repository area')
- }
- return files
- }
- /**
- * Read one tracked file without following a symlink to its target.
- * @param repoRoot - Repository root containing the tracked path.
- * @param file - Repository-relative tracked path.
- * @returns File text, the symlink target, or undefined when the path is absent or not a file.
- */
- export function readTrackedSource(repoRoot: string, file: string): string | undefined {
- const path = resolve(repoRoot, file)
- const stat = lstatSync(path, { throwIfNoEntry: false })
- if (stat === undefined) return undefined
- if (stat.isSymbolicLink()) return readlinkSync(path)
- return stat.isFile() ? readFileSync(path, 'utf8') : undefined
- }
- function scanRepository(repoRoot: string): ConcreteTermViolation[] {
- const violations: ConcreteTermViolation[] = []
- for (const file of trackedFiles(repoRoot)) {
- const source = readTrackedSource(repoRoot, file)
- if (source === undefined) continue
- violations.push(...findConcreteTermViolations(file, source))
- }
- return violations
- }
- const invokedPath = process.argv[1]
- const isMain = invokedPath !== undefined && import.meta.url === pathToFileURL(resolve(invokedPath)).href
- if (isMain) {
- const violations = scanRepository(root)
- if (violations.length === 0) {
- console.log(`verify-concrete-terms: maintained tracked files contain no ${blockedTerm}.`)
- } else {
- console.error(`verify-concrete-terms: ${blockedTerm} is forbidden; name the exact source, field, identity, or evidence:`)
- for (const violation of violations) {
- console.error(violation.line === null
- ? ` ${violation.file} (path)`
- : ` ${violation.file}:${String(violation.line)}`)
- }
- process.exitCode = 1
- }
- }
|