doxa-maps/internal/check-no-internal-role-nouns.mjs
Doxa Maps 2eaea5cedf P (repo): neutral wording in the gates and docs, and no private-process checks
Comments, messages and docs only; no check logic changes.
- The role-word gate is renamed check-no-internal-role-nouns (and its baseline fixture). Same
  logic: it builds the word from parts so the file does not carry it, and a planted line in
  title case and in capitals still fails it while WebDriver and "GPU driver" do not.
- public-hygiene-check: the two role patterns are built from parts (the regexes are byte-
  identical), the header loses the forge and date wording, and the prayer comment loses its
  decision date. Its output on the tree is identical in normal and --strict modes.
- landing-preconditions, check-no-public-binds, check-published-tree-weight: no fleet tool
  name, no "this box", no forge name in comments and messages.
- Removed: check-backlog-refs, check-backlog-status-enum, check-done-evidence,
  check-verification-tags and check-dxlife-untouched (they police a private backlog and
  workspace), with their scripts, docs, gate-list entries and orphan-baseline entry, and the
  committed needs-eyes pages (check-embedded-rtl --emit writes them on demand).
- package.json docs: no agent names. Docs: the stale check mentions and the snapshot
  references are gone (the snapshots themselves leave in the studio P commit).
gates:pre: PASS 115, FAIL 0, NOT MEASURED 1 (browser seat).
2026-10-01 00:25:38 +00:00

119 lines
6.5 KiB
JavaScript

#!/usr/bin/env node
/**
* check-no-internal-role-nouns — shipped text names no internal process role, and the references already
* in it may only go away.
*
* The rule: a comment, doc, UI string or commit says what the code does and why, never who said what.
* An open decision reads "provisional, pending a product decision". The gate holds the line
* FORWARD-ONLY against a baseline:
* - a line that uses the role noun and is not in the baseline FAILS (a new reference);
* - a baselined line that is gone is fine (owners sweep in parallel); --shrink tidies the baseline.
*
* WHAT COUNTS: the role noun as a whole word, in title case or in capitals (not a GPU or device driver,
* not WebDriver, not an env name with the word in it). The word is assembled at runtime below, so this
* file does not itself carry it. KEYED by file plus the line's normalised text, with a count, not by
* line number, so edits elsewhere in a file never trip it; a new line with the same text in the same
* file raises the count and fails.
*
* ⚠ THE CAPITALS FORM WAS MISSING AT FIRST. The first pattern matched only the title-case form, so lines
* written in capitals passed unseen, and a sweep measured by this gate read as finished while they
* stayed. The baseline was re-measured once with the wider pattern, the one time it has grown; it only
* shrinks from here.
*
* node internal/check-no-internal-role-nouns.mjs check
* node internal/check-no-internal-role-nouns.mjs --shrink drop what is gone (never adds)
* node internal/check-no-internal-role-nouns.mjs --init write the baseline, only if none exists
*
* Node only. Exit 0 pass · 1 fail.
*/
import { readFileSync, writeFileSync, existsSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { join, resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const SELF = 'internal/check-no-internal-role-nouns.mjs'
// Files whose JOB is to name the word: a checker's pattern list is not a citation. Exempt by name, each with its
// reason, so no rule wider than one file is ever needed. Anything else in them is still that file's owner's to keep clean.
const PATTERN_FILES = new Set([
'internal/public-hygiene-check.mjs', // its egress patterns match the role noun
])
const BASELINE_REL = 'internal/fixtures/internal-role-nouns-baseline.json'
const BASELINE = join(ROOT, BASELINE_REL)
// The role noun, assembled from parts so this file does not carry it; title case and capitals.
const W = ['Dri', 'ver'].join('')
const WU = W.toUpperCase()
const ROLE = new RegExp(`\\b(?:${W}|${WU})\\b`)
const AREAS = ['library', 'plugin-designs', 'internal', 'documentation', 'skills', 'README.md']
export function referencesIn(text) {
const out = []
text.split('\n').forEach((line, i) => {
if (ROLE.test(line)) out.push({ line: i + 1, norm: line.replace(/\s+/g, ' ').trim() })
})
return out
}
const keyOf = (file, norm) => `${file}\u0000${norm}`
function shippedFiles() {
const listed = execFileSync('git', ['-C', ROOT, 'ls-files', ...AREAS], { encoding: 'utf8' })
return listed.split('\n').filter((f) => /\.(vue|js|mjs|md|json|html|css)$/.test(f) && !f.includes('/data/')
&& f !== SELF && f !== BASELINE_REL && !PATTERN_FILES.has(f))
}
function tally(files, read) {
const counts = new Map(); const where = new Map()
for (const f of files) {
let text
try { text = read(f) } catch { continue }
for (const r of referencesIn(text)) {
const k = keyOf(f, r.norm)
counts.set(k, (counts.get(k) || 0) + 1)
if (!where.has(k)) where.set(k, { file: f, line: r.line, text: r.norm })
}
}
return { counts, where }
}
// ── self-test: the role noun is found, a device driver is not, and a new line beside a baselined one is isolated
{
const found = referencesIn(`// the ${W} ruled that numerals stay en-US\n// THE ${WU}'S ASK\n// the GPU driver and WebDriver and ${WU}_MERGE\n`)
if (found.length !== 2) { console.error('ROLE-NOUNS: SELF-TEST FAILED — the role noun (either case) is not told apart from other drivers'); process.exit(1) }
const { counts } = tally(['a.vue'], () => `// ${W}: old\n// ${W}: old\n// ${W}: new\n`)
const base = new Map([[keyOf('a.vue', `// ${W}: old`), 2]])
const fresh = [...counts].filter(([k, n]) => n > (base.get(k) || 0))
if (fresh.length !== 1 || !fresh[0][0].endsWith('new')) { console.error('ROLE-NOUNS: SELF-TEST FAILED — a new reference beside baselined ones was not isolated'); process.exit(1) }
}
const { counts, where } = tally(shippedFiles(), (f) => readFileSync(join(ROOT, f), 'utf8'))
const args = process.argv.slice(2)
const toObj = (m) => Object.fromEntries([...m].sort(([a], [b]) => (a < b ? -1 : 1)))
if (args.includes('--init')) {
if (existsSync(BASELINE)) { console.error(`ROLE-NOUNS: --init refused, ${BASELINE_REL} exists (use --shrink)`); process.exit(1) }
writeFileSync(BASELINE, JSON.stringify({ about: 'Lines in shipped text that used the internal role noun when this gate began, keyed by file and text. Owners remove them; the list only shrinks (--shrink).', counts: toObj(counts) }, null, 1) + '\n')
console.log(`ROLE-NOUNS: baseline written: ${counts.size} line(s) in ${new Set([...where.values()].map((w) => w.file)).size} file(s)`)
process.exit(0)
}
const baseline = existsSync(BASELINE) ? JSON.parse(readFileSync(BASELINE, 'utf8')) : { counts: {} }
const base = new Map(Object.entries(baseline.counts || {}))
if (args.includes('--shrink')) {
const kept = new Map()
for (const [k, n] of base) { const now = counts.get(k) || 0; if (now > 0) kept.set(k, Math.min(n, now)) }
writeFileSync(BASELINE, JSON.stringify({ ...baseline, counts: toObj(kept) }, null, 1) + '\n')
console.log(`ROLE-NOUNS: baseline shrunk ${base.size} -> ${kept.size} line(s) (it never grows)`)
process.exit(0)
}
const fresh = [...counts].filter(([k, n]) => n > (base.get(k) || 0)).map(([k]) => where.get(k))
const gone = [...base.keys()].filter((k) => !counts.has(k)).length
if (fresh.length) {
console.error(`ROLE-NOUNS: FAIL — ${fresh.length} new use(s) of the internal role noun in shipped text:`)
for (const w of fresh.slice(0, 20)) console.error(` ✗ ${w.file}:${w.line} ${w.text.slice(0, 120)}`)
console.error(' Keep the engineering reason and drop the speaker; an open decision reads "provisional, pending a product decision".')
process.exit(1)
}
console.log(`ROLE-NOUNS: PASS — no new use of the internal role noun; ${base.size - gone} older line(s) still baselined for the sweep`
+ (gone ? `, ${gone} already swept (tidy with --shrink)` : '') + '.')