diff --git a/documentation/README-SECTION-3-cli-commands.md b/documentation/README-SECTION-3-cli-commands.md index 89a914514..77bbfc60b 100644 --- a/documentation/README-SECTION-3-cli-commands.md +++ b/documentation/README-SECTION-3-cli-commands.md @@ -25,7 +25,7 @@ The scripts these commands are grounded in (from this branch's `package.json`): // claiming to be ground truth, and this one had silently stopped being it. "dev": "bun x vite", "build": "bash internal/build-lock.sh", -"build:unlocked": "bun run check:template-literal && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run check:descriptions && bun run gallery", +"build:unlocked": "bun run check:template-literal && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run check:descriptions && bun run check:no-abs-paths && bun run gallery", "preview": "bun x vite preview", "serve": "python3 -m http.server 4173 --bind 0.0.0.0 --directory public", "cap:sync": "./node_modules/.bin/cap sync android", diff --git a/documentation/reference.md b/documentation/reference.md index 25d47489c..b0495d975 100644 --- a/documentation/reference.md +++ b/documentation/reference.md @@ -234,6 +234,7 @@ so running it inside the build would make it unfalsifiable. Run them before publ | `bun run verify:5-caps` | a capability tier that no longer has a working exemplar — the 5×5 product claim going stale | needs a browser + a Mapbox token | | `bun run check:quoted-scripts` | a **doc that quotes a command which has since changed** — an omitted step in a quoted chain is invisible: there is no wrong character to notice, just a check the reader believes is in the chain | compares docs against `package.json`; cheap, but it guards documentation rather than build output | | `bun run check:capability-exemplars` | `documentation/reference-capability-exemplars.md` drifting from the manifest's real tiers (regenerate with `bun run capability-exemplars`) | the table is generated *from* the manifest, so running it mid-build would compare a file against the thing that just wrote it | +| `bun run check:no-abs-paths` | a MACHINE-SPECIFIC PATH in tracked source — a `/home//` prefix, its macOS equivalent, or the Windows user-profile form. It is in `build:unlocked`, listed here because the failure it prevents is a HAND-OFF failure: 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. Scans `git ls-files` (the set a clone receives), which excludes node_modules and build output without an ignore list. Measured 2026-09-22: the repo was clean BY ACCIDENT — a planted `/home/…` line passed check:public-hygiene, check:parse and check:quoted-scripts, because public-hygiene scans the SERVED tree, not source. | | `bun run check:settled` | measuring or publishing a tree **mid-build** — a build's `clean` step `rm -rf`s `public/doxa-maps`, so samples taken seconds apart give opposite answers | it exists to be run *around* builds, not inside one | ### Before you run the list — three gates need a PRECONDITION, or they look broken diff --git a/internal/check-no-absolute-paths.mjs b/internal/check-no-absolute-paths.mjs new file mode 100644 index 000000000..3bb48c051 --- /dev/null +++ b/internal/check-no-absolute-paths.mjs @@ -0,0 +1,103 @@ +#!/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.') diff --git a/package.json b/package.json index 5f3095775..9eedf8415 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "check:public-hygiene": "node internal/public-hygiene-check.mjs", "check:label-joins": "node internal/check-label-joins.mjs", "check:dxlife": "node internal/check-dxlife-untouched.mjs", - "build:unlocked": "bun run check:template-literal && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run check:descriptions && bun run gallery", + "build:unlocked": "bun run check:template-literal && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run check:descriptions && bun run check:no-abs-paths && bun run gallery", "check:template-literal": "node internal/template-literal-check.mjs", "verify:map-paint": "node internal/verify-map-paint.mjs", "check:sri": "node internal/sri-consistency-check.mjs", @@ -70,7 +70,8 @@ "check:settled": "node internal/settled-tree-gate.mjs", "verify:5-caps": "node internal/verify-5-capabilities.mjs", "verify:gate1": "node internal/acceptance/big-picture-acceptance.mjs # the Gate-1 chain end-to-end against LIVE data: header emits research-filter -> composite passes props down -> the REAL cap-2 grid re-filters -> count reflected back up. Asserts the NEGATIVE (a filter that must return 0 does) and RESET. Was an ORPHAN — present in internal/acceptance/ but invoked by NO script, so it could never report anything.", - "check:descriptions": "node internal/check-profile-descriptions.mjs # every shipped profile must say what it is (reads the BUILT manifest)" + "check:descriptions": "node internal/check-profile-descriptions.mjs # every shipped profile must say what it is (reads the BUILT manifest)", + "check:no-abs-paths": "node internal/check-no-absolute-paths.mjs # no /home/… or /Users/… in any TRACKED file (a clone must work on any machine)" }, "devDependencies": { "@deployment/capacitor/android": "7", diff --git a/public/.build.lock b/public/.build.lock index ecc9ca921..30aabd6b9 100644 --- a/public/.build.lock +++ b/public/.build.lock @@ -1 +1 @@ -pid=1234302 started=2026-09-22T04:51:59Z by=lk10x +pid=1763935 started=2026-09-22T06:28:44Z by=lk10x