check:no-fleet-names: names in any case, the plugin agents' names, and the translation catalogues scanned too; the self-test plants each

This commit is contained in:
Doxa Maps 2026-10-01 05:42:24 +00:00
parent 2f7eaae8e3
commit 59afad5a33

View file

@ -7,9 +7,10 @@
* which request a choice answered, which QA round found it. The technical fact belongs in the comment; the person, * which request a choice answered, which QA round found it. The technical fact belongs in the comment; the person,
* the request and the date-stamped round do not. A sweep removed every such line; this gate keeps the count at 0. * the request and the date-stamped round do not. A sweep removed every such line; this gate keeps the count at 0.
* *
* SCANS every tracked text file EXCEPT research data (data/, plugin-designs/<plugin>/data/), build output * SCANS every tracked text file EXCEPT research data (data/, plugin-designs/<plugin>/data/) and build output
* (public/) and the translation catalogues (library/i18n/locales/: UI strings in 11 languages, where a short * (public/). The translation catalogues ARE scanned: a name can hide in a string as well as a comment, and a
* pattern can be an ordinary word). Three classes: * translation that ever collides with a short name is allowed by its line, not by skipping the folder. Names match
* in any case. Three classes:
* 1. a build-team member's name (the fleet's agent names); * 1. a build-team member's name (the fleet's agent names);
* 2. a per-user attribution (the words "per" and "user" in a row: a request credited to a person); * 2. a per-user attribution (the words "per" and "user" in a row: a request credited to a person);
* 3. a dated QA-round tag (a lowercase or uppercase QA label followed by a 20xx date). * 3. a dated QA-round tag (a lowercase or uppercase QA label followed by a 20xx date).
@ -25,13 +26,16 @@ import { fileURLToPath } from 'node:url'
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
// Built from parts so this file does not match itself line by line. // Built from parts so this file does not match itself line by line.
const NAMES = ['dx-' + 'shepherd', 'exp-' + 'shepherd', 'go-' + 'int', 'msg-' + 'agent', 'regions-' + 'plugin', 're' + 'ga'] const NAMES = ['dx-' + 'shepherd', 'exp-' + 'shepherd', 'go-' + 'int', 'msg-' + 'agent', 're' + 'ga']
// The plugin agents are named <plugin>-plugin; the folders and bundles never carry that suffix.
const PLUGIN_AGENTS = ['deep-research', 'prayer-progress', 'poster-design', 'country-posters', 'wall-of-people-groups',
'simpleresearch', 'regions', 'big-picture', 'bible-poster', 'upg100', 'gospel', 'cities'].map((p) => p + '-' + 'plugin')
const CLASSES = [ const CLASSES = [
['a build-team member named', new RegExp('\\b(' + NAMES.join('|') + ')\\b')], ['a build-team member named', new RegExp('\\b(' + NAMES.concat(PLUGIN_AGENTS).join('|') + ')\\b', 'i')],
['a per-user attribution', new RegExp('\\bper ' + 'user\\b', 'i')], ['a per-user attribution', new RegExp('\\bper ' + 'user\\b', 'i')],
['a dated QA tag', new RegExp('\\(q' + 'a:|\\bq' + 'a: ?20\\d\\d-|\\bQ' + 'A 20\\d\\d-\\d\\d')], ['a dated QA tag', new RegExp('\\(q' + 'a:|\\bq' + 'a: ?20\\d\\d-|\\bQ' + 'A 20\\d\\d-\\d\\d')],
] ]
const SKIP = [/^data\//, /^public\//, /^plugin-designs\/[^/]+\/data\//, /^library\/i18n\/locales\//, /\/node_modules\//] const SKIP = [/^data\//, /^public\//, /^plugin-designs\/[^/]+\/data\//, /\/node_modules\//]
const TEXT = /\.(m?js|cjs|ts|vue|html|css|json|md|sh|go|py|ya?ml|txt|svg)$|(^|\/)(Dockerfile|\.gitignore|\.gitattributes)$/ const TEXT = /\.(m?js|cjs|ts|vue|html|css|json|md|sh|go|py|ya?ml|txt|svg)$|(^|\/)(Dockerfile|\.gitignore|\.gitattributes)$/
// { file, line: RegExp matching the WHOLE allowed line, why }. A detector that must name what it detects goes here, // { file, line: RegExp matching the WHOLE allowed line, why }. A detector that must name what it detects goes here,
// one line at a time. Empty today. // one line at a time. Empty today.
@ -53,13 +57,17 @@ function trackedFiles() {
if (process.argv.includes('--self-test')) { if (process.argv.includes('--self-test')) {
const planted = [ const planted = [
['measured by ' + NAMES[5] + ' at 390', 1], ['measured by ' + NAMES[4] + ' at 390', 1],
['(' + NAMES[0] + "'s ruling)", 1], ['(' + NAMES[0] + "'s ruling)", 1],
['// Per ' + 'user 2026-05-02: drop the prefix', 1], ['// Per ' + 'user 2026-05-02: drop the prefix', 1],
['/* fits the band (q' + 'a: 2026-05-02 iter-9) */', 1], ['/* fits the band (q' + 'a: 2026-05-02 iter-9) */', 1],
['// Q' + 'A 2026-04-27: rows match desktop', 1], ['// Q' + 'A 2026-04-27: rows match desktop', 1],
['// the drawer opens from the right in ar', 0], ['// the drawer opens from the right in ar', 0],
['// a user of the map can pick a language', 0], ['// a user of the map can pick a language', 0],
['// ' + NAMES[4].toUpperCase().slice(0, 1) + NAMES[4].slice(1) + ' measured it at 390', 1],
['(' + NAMES[0].toUpperCase() + "'s call)", 1],
['// on ' + PLUGIN_AGENTS[9] + "'s argument", 1],
['// the ' + PLUGIN_AGENTS[3].replace('-' + 'plugin', '') + ' bundle', 0],
['// the irrigation (reg' + 'ar) field', 0], ['// the irrigation (reg' + 'ar) field', 0],
] ]
let bad = 0 let bad = 0
@ -69,14 +77,17 @@ if (process.argv.includes('--self-test')) {
console.log(` ${ok ? 'ok ' : 'BAD'} ${want ? 'caught' : 'clean '}: ${line}`) console.log(` ${ok ? 'ok ' : 'BAD'} ${want ? 'caught' : 'clean '}: ${line}`)
if (!ok) bad++ if (!ok) bad++
} }
// A name inside a translation catalogue is scanned like any other file.
const localeFile = 'library/i18n/locales/en/x.json'
if (SKIP.some((re) => re.test(localeFile)) || scanText('{ "note": "by ' + NAMES[0] + '" }', localeFile).length !== 1) { console.log(' BAD locales: a name in a catalogue was not caught'); bad++ }
// The allowlist is per LINE: an allowed line passes, a different flagged line in the SAME file still fails. // The allowlist is per LINE: an allowed line passes, a different flagged line in the SAME file still fails.
const allow = [{ file: 'd.mjs', line: /^const PATTERN = .*detects$/, why: 'self-test' }] const allow = [{ file: 'd.mjs', line: /^const PATTERN = .*detects$/, why: 'self-test' }]
const allowedLine = 'const PATTERN = "' + NAMES[0] + '" // detects' const allowedLine = 'const PATTERN = "' + NAMES[0] + '" // detects'
if (scanText(allowedLine, 'd.mjs', allow).length !== 0) { console.log(' BAD allowlist: an allowed line was flagged'); bad++ } if (scanText(allowedLine, 'd.mjs', allow).length !== 0) { console.log(' BAD allowlist: an allowed line was flagged'); bad++ }
if (scanText(allowedLine + '\n// measured by ' + NAMES[5], 'd.mjs', allow).length !== 1) { console.log(' BAD allowlist: a new name in an allowlisted file passed'); bad++ } if (scanText(allowedLine + '\n// measured by ' + NAMES[4], 'd.mjs', allow).length !== 1) { console.log(' BAD allowlist: a new name in an allowlisted file passed'); bad++ }
if (scanText(allowedLine, 'other.mjs', allow).length !== 1) { console.log(' BAD allowlist: the allowed line passed in another file'); bad++ } if (scanText(allowedLine, 'other.mjs', allow).length !== 1) { console.log(' BAD allowlist: the allowed line passed in another file'); bad++ }
if (bad) { console.error(`NO-FLEET-NAMES SELF-TEST: FAIL — ${bad} line(s) judged wrong.`); process.exit(1) } if (bad) { console.error(`NO-FLEET-NAMES SELF-TEST: FAIL — ${bad} line(s) judged wrong.`); process.exit(1) }
console.log(`NO-FLEET-NAMES SELF-TEST: PASS — ${planted.filter((p) => p[1]).length} planted lines caught, ${planted.filter((p) => !p[1]).length} clean lines passed, the per-line allowlist holds (3 cases).`) console.log(`NO-FLEET-NAMES SELF-TEST: PASS — ${planted.filter((p) => p[1]).length} planted lines caught, ${planted.filter((p) => !p[1]).length} clean lines passed, the per-line allowlist holds (3 cases), catalogues are scanned.`)
process.exit(0) process.exit(0)
} }