check:one-palette sees a bundle's private src/colors/ palettes; Rudder's offline hint states Go 1.22+ and the setup

Two findings from upg100's census. check-one-palette read strategy palettes from
library/colors/ only, so a bundle's own src/colors/ strategy (which overrides the
library one for that bundle) was never checked; it now reads both, the bundle's
over the library's, and --self-test plants a private palette. Rudder's
engine-offline notice gave a go build line without saying Go 1.22+ is needed or
that setup-runtime.sh downloads the model; it now says both and points at
bun run desktop:build and the desktop HOWTO.
This commit is contained in:
Doxa Maps 2026-09-28 06:49:22 +00:00
parent 4ce8604d42
commit 35955a4db6
2 changed files with 36 additions and 12 deletions

View file

@ -47,8 +47,9 @@
* NEGATIVE-TESTED: reintroducing a private `const CATEGORICAL = ['#...']` into the aggregations
* composable fails assertion 2 by name; an `[i % CATEGORICAL.length]` lookup fails assertion 3.
*/
import { readFileSync, existsSync, readdirSync } from 'node:fs'
import { readFileSync, existsSync, readdirSync, mkdtempSync, writeFileSync, rmSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
import { TREE_NAME } from './_tree-name.mjs'
@ -96,7 +97,32 @@ export function paletteHexes(src) {
return [...new Set(strings.flatMap((t) => [...t.matchAll(/#[0-9a-fA-F]{6}\b/g)].map((m) => m[0].toLowerCase())))]
}
/**
* The strategy palettes in one colors folder: library/colors/, or a bundle's own
* plugin-designs/<bundle>/src/colors/ (bundle-private strategies merge OVER the library set for that
* bundle — see that folder's README). Filename → key as the registry names it is not needed here; the
* file stem is the key both places use.
*/
export function collectStrategies(dir) {
const out = {}
if (!existsSync(dir)) return out
for (const f of readdirSync(dir)) {
if (!f.endsWith('.js') || f.startsWith('_') || f.startsWith('.') || f === 'index.js') continue
const hexes = paletteHexes(readFileSync(join(dir, f), 'utf8'))
if (hexes && hexes.length) out[f.replace(/\.js$/, '')] = hexes
}
return out
}
if (process.argv.includes('--self-test')) {
// A bundle's PRIVATE palette (src/colors/<mode>.js) must be seen, not only library/colors/ (the gate was
// blind to it: upg100's census, BUG-CHECK-ONE-PALETTE-BLIND-TO-PRIVATE-PALETTES).
const tmp = mkdtempSync(join(tmpdir(), 'one-palette-'))
writeFileSync(join(tmp, 'religion.js'), "export const PALETTE = { islam: '#010203', hinduism: '#040506' }\n")
writeFileSync(join(tmp, '_skip.js'), "export const PALETTE = { x: '#999999' }\n")
const seen = JSON.stringify(collectStrategies(tmp))
rmSync(tmp, { recursive: true, force: true })
if (seen !== JSON.stringify({ religion: ['#010203', '#040506'] })) { console.error(`ONE-PALETTE: SELF-TEST FAIL — a planted private palette read as ${seen}`); process.exit(1) }
const src = [
"export const PALETTE = {",
" a: '#112233', // not #445566 (a comment inside the export)",
@ -107,18 +133,14 @@ if (process.argv.includes('--self-test')) {
].join('\n')
const got = paletteHexes(src).join(',')
if (got !== '#112233,#aabbcc') { console.error(`ONE-PALETTE: SELF-TEST FAIL — read ${got}, want #112233,#aabbcc`); process.exit(1) }
console.log('ONE-PALETTE: SELF-TEST PASS — only the PALETTE export\'s string values are read; comments and later code are not.')
console.log('ONE-PALETTE: SELF-TEST PASS — only the PALETTE export\'s string values are read; comments and later code are not; a bundle\'s private src/colors/ palette is seen.')
process.exit(0)
}
// ── the expected palettes, read from the strategy modules the MAP uses ────────────────────────
const strategies = {}
for (const f of readdirSync(COLORS)) {
if (!f.endsWith('.js') || f.startsWith('_') || f.startsWith('.')) continue
const src = readFileSync(join(COLORS, f), 'utf8')
const hexes = paletteHexes(src)
if (hexes && hexes.length) strategies[f.replace(/\.js$/, '')] = hexes
}
// The library set, and per bundle its own src/colors/ strategies merged over it (same key overrides).
const strategies = collectStrategies(COLORS)
const privateOf = (bundle) => collectStrategies(join(ROOT, 'plugin-designs', bundle, 'src', 'colors'))
// ── the consumers that must agree ─────────────────────────────────────────────────────────────
const CONSUMERS = [
@ -142,7 +164,7 @@ for (const c of CONSUMERS) {
const p = join(BUILT, c.bundle)
if (!existsSync(p)) { errs.push(`bundle missing: ${c.bundle} — build is incomplete, so this gate cannot judge it.`); continue }
const js = readFileSync(p, 'utf8').toLowerCase()
for (const [strat, hexes] of Object.entries(strategies)) {
for (const [strat, hexes] of Object.entries({ ...strategies, ...privateOf(c.name) })) {
const missing = hexes.filter((h) => !js.includes(h))
checkedPairs++
// A consumer need not use EVERY strategy, but if it uses one at all it must use the whole

View file

@ -707,8 +707,10 @@ const statusText = computed(() => ({
the engine runs on. Open the Plugin Studio on your computer (<code>bun run dev</code>) or use the Rudder desktop app.
</p>
<p v-else-if="engine === 'offline'" class="notice" data-kind="warn">
The local speech engine isn't reachable at <code>{{ apiBase }}</code>. In the Plugin Studio it starts with
<code>bun run dev</code> once built: <code>cd backend/rudder-engine &amp;&amp; go build -o ../bin/rudder-engine .</code>
The local speech engine isn't reachable at <code>{{ apiBase }}</code>. It needs <b>Go 1.22 or newer</b> and a
one-time setup that downloads the speech model (about 750 MB): <code>bash backend/rudder-engine/setup-runtime.sh</code>.
Then <code>bun run dev</code> starts it here. For the desktop app, <code>bun run desktop:build rudder-voice</code>;
both are described in <code>documentation/HOWTO-build-the-desktop-app.md</code>.
</p>
<section class="compose">