Some checks are pending
build-gate / build (push) Waiting to run
The repo is clean -- git grep over every tracked file returns zero /home/ paths. That is a real hand-off property: a clone works on any machine, which is what "the path Doxa inherits" requires. But nothing enforced it. Proven by experiment: a /home/lk10x/... line planted in internal/settled-tree-gate.mjs passed check:public-hygiene, check:parse AND check:quoted-scripts. The tree was clean by accident, not by construction. This is the worst failure shape for a hand-off. An absolute path works perfectly for whoever wrote it and breaks only on the inheritor's machine, so no test run by its author can catch it. The sibling ideation tree already paid that cost: two docs cite a workspace root that no longer exists, so a reader following them silently finds nothing. Not a flag on check:public-hygiene, which already detects these patterns but walks a SERVED tree where node_modules never exists -- pointed at the repo root it produced 1,427 findings, nearly all from node_modules/.vite. Its scope is right for its job. This scans `git ls-files`, the set a clone actually receives, which excludes node_modules and build output with no ignore list to maintain. Negative-tested three ways: the exact planted path that beat three other gates now FAILS with file:line; a /Users/ path in a doc FAILS; a checkout with too few tracked files FAILS rather than passing vacuously. Wired into build:unlocked and confirmed running inside a full build (783 files, exit 0). It then caught its own documentation -- my reference.md row quoted the literal patterns as examples. Rewrote the example rather than allow-listing the file, which would have blinded the gate to a real path in the repo's largest doc.
103 lines
5.2 KiB
JavaScript
103 lines
5.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-no-absolute-paths.mjs — no machine-specific path may enter the SOURCE tree.
|
|
*
|
|
* WHY (measured 2026-09-22)
|
|
* The repo is currently clean: `git grep -lE '/home/[a-z0-9_-]+/'` over every tracked file returns
|
|
* ZERO. That is a real hand-off property — a clone works on any machine, which is exactly what
|
|
* "the path Doxa inherits" requires. But nothing enforced it. Proven by experiment: a
|
|
* `/home/lk10x/...` line planted in internal/settled-tree-gate.mjs passed check:public-hygiene,
|
|
* check:parse and check:quoted-scripts. The tree was clean BY ACCIDENT, not by construction.
|
|
*
|
|
* ★ WHY THIS FAILURE IS THE WORST SHAPE FOR A HAND-OFF. An absolute path works PERFECTLY for the
|
|
* person who wrote it and breaks only on someone else's machine — the inheritor, i.e. precisely the
|
|
* person the north star is about. It is invisible to every test run by its author. The sibling
|
|
* ideation tree already paid this cost: two docs cite `~/Workspace/dx/root-doxa-maps/...`, a root
|
|
* that no longer exists, so a reader following them silently finds nothing.
|
|
*
|
|
* WHY NOT REUSE check:public-hygiene. It already detects /home/…, /Users/… and C:\Users\… — but it
|
|
* walks a SERVED TREE, where node_modules never exists. Pointed at the repo root it produced 1,427
|
|
* findings, essentially all from node_modules/.vite. Its scope is correct for its job; this is a
|
|
* different job, so it gets a different (tiny) tool rather than a flag that weakens the first.
|
|
*
|
|
* SCOPE: TRACKED FILES ONLY, via `git ls-files`. That is the set a clone actually receives, and it
|
|
* excludes node_modules, build output and local scratch without needing an ignore list to maintain.
|
|
*
|
|
* NEGATIVE-TESTED: planting a /home/… line in a tracked source file makes this FAIL naming the file
|
|
* and line, EXIT=1; removing it returns PASS. A repo with no tracked files FAILS (vacuous guard).
|
|
*/
|
|
import { execSync } from 'node:child_process'
|
|
import { readFileSync, existsSync } from 'node:fs'
|
|
import { resolve, dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
|
|
// The patterns are the same three check:public-hygiene uses, kept deliberately identical so the two
|
|
// gates cannot disagree about what counts as a machine-specific path.
|
|
const PATTERNS = [
|
|
[/\/home\/[a-z0-9_-]+\//i, 'local path (/home/…)'],
|
|
[/\/Users\/[A-Za-z0-9_-]+\//, 'local path (/Users/…)'],
|
|
[/[A-Z]:\\Users\\/, 'local path (C:\\Users\\…)'],
|
|
]
|
|
|
|
// Binary and generated files a path could appear in harmlessly or unavoidably.
|
|
const SKIP_EXT = /\.(png|jpe?g|webp|gif|ico|woff2?|ttf|eot|pdf|zip|gz|mbtiles|pmtiles|lock)$/i
|
|
|
|
// A tracked file may legitimately DISCUSS an absolute path — the anti-pattern log and this file's
|
|
// own header both quote one to explain the rule. An allow-list by path keeps the gate honest
|
|
// without letting the exemption spread: it names files, never patterns.
|
|
const ALLOW = new Set([
|
|
'internal/check-no-absolute-paths.mjs', // this file quotes the patterns it forbids
|
|
])
|
|
|
|
let tracked = []
|
|
try {
|
|
tracked = execSync('git ls-files', { cwd: ROOT, encoding: 'utf8' }).split('\n').filter(Boolean)
|
|
} catch (e) {
|
|
console.error(`ABSOLUTE-PATHS: FAIL — could not list tracked files (${String(e.message).slice(0, 60)}).`)
|
|
console.error(' This gate scans `git ls-files`, the set a clone actually receives.')
|
|
process.exit(1)
|
|
}
|
|
|
|
// Vacuous guard: "0 tracked files" must never read as "0 problems".
|
|
const MIN_TRACKED = 50
|
|
if (tracked.length < MIN_TRACKED) {
|
|
console.error(`ABSOLUTE-PATHS: FAIL — only ${tracked.length} tracked file(s) (expected >= ${MIN_TRACKED}).`)
|
|
console.error(' Not a git checkout, or the listing is broken. Refusing to pass vacuously.')
|
|
process.exit(1)
|
|
}
|
|
|
|
const hits = []
|
|
let scanned = 0
|
|
for (const rel of tracked) {
|
|
if (SKIP_EXT.test(rel) || ALLOW.has(rel)) continue
|
|
const abs = join(ROOT, rel)
|
|
if (!existsSync(abs)) continue // staged-deleted file
|
|
let text
|
|
try { text = readFileSync(abs, 'utf8') } catch { continue } // binary
|
|
scanned++
|
|
const lines = text.split('\n')
|
|
for (let i = 0; i < lines.length; i++) {
|
|
for (const [re, label] of PATTERNS) {
|
|
if (re.test(lines[i])) { hits.push({ rel, line: i + 1, label, text: lines[i].trim().slice(0, 100) }); break }
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`ABSOLUTE-PATHS: ${scanned} tracked text file(s) scanned.`)
|
|
|
|
if (hits.length) {
|
|
console.error(`\nABSOLUTE-PATHS: FAIL — ${hits.length} machine-specific path(s) in tracked source:`)
|
|
for (const h of hits.slice(0, 20)) console.error(` ✗ ${h.rel}:${h.line} ${h.label}\n ${h.text}`)
|
|
if (hits.length > 20) console.error(` … +${hits.length - 20} more`)
|
|
console.error(
|
|
'\n An absolute path works perfectly for whoever wrote it and breaks only on the inheritor\'s\n' +
|
|
' machine, so no test run by its author will catch it. Use a path relative to the repo root\n' +
|
|
' (resolve(dirname(fileURLToPath(import.meta.url)), \'..\')) or an env var. If a file must QUOTE\n' +
|
|
' a path to explain something, add it to ALLOW in this gate — by filename, never by pattern.'
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log('ABSOLUTE-PATHS: PASS — no machine-specific path in any tracked file.')
|