doxa-maps/internal/check-dependency-names.mjs
doxa-maps ec93f8aa1a
Some checks are pending
build-gate / build (push) Waiting to run
Gate: every declared dependency must be a real package name
Makes the cheap half of last tick's P0 automatic. A folder rename had
rewritten three npm package names, so bun install failed in a fresh clone
while everything here passed -- the real packages were already in
node_modules from before the rename, making the breakage invisible to every
build and every gate.

Checks two things, offline, in milliseconds: the declared name resolves under
node_modules/, AND the installed package's own name field equals the declared
key. The first alone catches the capacitor case; the second catches a name
that resolves to a DIFFERENT package, which a path-only check would miss.
Together they detect the corrupted-name class without a network round-trip,
which is what lets it run on every build rather than in a nightly job nobody
watches.

It does not replace the clone test. A gate can only see what is IN the repo;
only a clone sees what the repo fails to do on a clean machine.

Negative-tested four ways: recreating the EXACT P0 (the three
@deployment/capacitor/* names) FAILS naming all three; a name that resolves
to a package whose own name differs FAILS; an absent node_modules SKIPs
honestly rather than passing or failing; an empty dependency list FAILS via
the vacuous guard. Wired into build:unlocked and confirmed running inside a
full build (17 declared, 17 verified, exit 0).
2026-09-22 06:49:21 +00:00

105 lines
5 KiB
JavaScript

#!/usr/bin/env node
/**
* check-dependency-names.mjs — every declared dependency must be a REAL package name.
*
* WHY (a P0 found 2026-09-22 by cloning the repo and running the documented first command)
* `bun install` FAILED in a fresh clone:
*
* error: GET https://registry.npmjs.org/@deployment%2fcapacitor%2fcli - 405
* error: @deployment/capacitor/cli@7 failed to resolve
*
* The rename of `capacitor/` → `deployment/capacitor/` had done a blind string replace that also
* rewrote three NPM PACKAGE NAMES: @capacitor/{android,cli,core} became @deployment/capacitor/*,
* which do not exist on npm. A folder rename reached into a namespace it had no business touching.
*
* ★ THE SHAPE THIS GUARDS — AN INHERITOR-ONLY FAILURE. The local tree worked PERFECTLY, because
* node_modules/@capacitor/* had been installed BEFORE the rename. Every build passed, every gate
* passed, `check:no-abs-paths` scanned 783 files and passed. The breakage lived ONLY in the
* DECLARED names, so it was invisible to everyone except someone installing from scratch — which
* is precisely the person the north star is about ("the path Doxa inherits").
*
* WHAT IT CHECKS, and why this is enough to catch that class OFFLINE:
* 1. the declared name resolves under node_modules/, and
* 2. the installed package's OWN `name` field equals the declared key.
* A corrupted name fails (1) because nothing was ever installed under it. A name that resolves to
* a DIFFERENT package fails (2) — the case a path-based check alone would miss.
*
* WHY NOT JUST RUN `bun install` IN CI. That needs a network and a clean cache to be meaningful,
* and a warm lockfile hides exactly this defect. This runs in milliseconds, offline, against the
* tree as it stands. It does NOT replace the clone test — see the note at the foot of the
* standalone-gate table — it makes the cheap half of it automatic.
*
* NEGATIVE-TESTED: renaming a declared dependency to a nonexistent scope makes this FAIL naming
* the key; restoring it returns PASS. A package.json with no dependencies FAILS (vacuous guard).
*/
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)), '..')
let pkg
try {
pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
} catch (e) {
console.error(`DEPENDENCY-NAMES: FAIL — cannot read package.json (${String(e.message).slice(0, 60)}).`)
process.exit(1)
}
const declared = []
for (const section of ['dependencies', 'devDependencies']) {
for (const [name, range] of Object.entries(pkg[section] || {})) declared.push({ section, name, range })
}
// Vacuous guard: "0 dependencies" must never read as "0 problems".
const MIN_DECLARED = 5
if (declared.length < MIN_DECLARED) {
console.error(`DEPENDENCY-NAMES: FAIL — only ${declared.length} declared dependency(ies) ` +
`(expected >= ${MIN_DECLARED}). package.json is empty or unreadable; refusing to pass vacuously.`)
process.exit(1)
}
const problems = []
let verified = 0
let uninstalled = 0
for (const { section, name, range } of declared) {
const pj = join(ROOT, 'node_modules', name, 'package.json')
if (!existsSync(pj)) {
// Not installed. That is the CORRUPTED-NAME signature when node_modules is otherwise populated:
// nothing could ever have been installed under a name that does not exist upstream.
uninstalled++
problems.push({ section, name, range, why: 'declared but NOT INSTALLED — is this a real package name?' })
continue
}
let real
try { real = JSON.parse(readFileSync(pj, 'utf8')).name } catch { real = null }
if (real !== name) {
problems.push({ section, name, range, why: `resolves to a package whose own name is ${JSON.stringify(real)}` })
continue
}
verified++
}
console.log(`DEPENDENCY-NAMES: ${declared.length} declared, ${verified} verified against node_modules.`)
// If node_modules is absent entirely, this gate cannot judge — say so rather than fail or pass.
if (uninstalled === declared.length) {
console.log('DEPENDENCY-NAMES: SKIP — node_modules/ is empty or absent, so nothing can be cross-checked.')
console.log(' Run `bun install` first. An absent tree is not evidence either way.')
process.exit(0)
}
if (problems.length) {
console.error(`\nDEPENDENCY-NAMES: FAIL — ${problems.length} declared dependency(ies) do not check out:`)
for (const p of problems) console.error(` ✗ ${p.section}: ${p.name}@${p.range}\n ${p.why}`)
console.error(
'\n A declared name that does not exist upstream breaks `bun install` in a FRESH CLONE while\n' +
' working perfectly here, because the real package is already in node_modules from before the\n' +
' change. Check whether a rename or a search-and-replace rewrote a package name: that is how\n' +
' @capacitor/cli became @deployment/capacitor/cli (2026-09-22).'
)
process.exit(1)
}
console.log('DEPENDENCY-NAMES: PASS — every declared dependency is a real, correctly-named package.')