Some checks are pending
build-gate / build (push) Waiting to run
120 keys across three locales: legend.json (12 each), search.json kinds (10
each) and tabs.json (18 each, previously absent entirely). Each locale follows
its own established vocabulary rather than a parallel one — Popolo/Popor/民族,
matching the terms already used in that catalog's detail and legend strings.
zh stays Simplified here, as the rest of this tree is, even though the prayer
packet's zh block is Traditional: a reader sees 民族 next to 祷告进度, never
the two files side by side.
Ceilings drop 94 -> 54 per locale. regions.json is the only i18n debt left in
the app: 54 keys of full sentences, three locales.
WHY THE NEW GATE. check:i18n-parity compares catalogs TO EACH OTHER, so it
certifies a file no t() call ever reaches — perfect symmetry, zero consumers,
green forever. Grepping for t('tabs. found ZERO call sites and tabs.json was
nearly recorded as dead. It is not: the only consumer builds the key
dynamically (SemanticTreeLegend.vue:40, 'tabs.' + (tab.i18nKey || tab.id)),
which no literal-prefix grep can see. The reverse error appeared in the same
pass — regions. and options. matched only poster-regions.mjs and a JS options
parameter. Static reachability is unavailable in both directions, so
check:tabs-localized renders the labels and reads them.
It asserts DIFFERENCE from English, not mere presence: te() falls back to the
hardcoded English label, so "shows some text" passes on the exact failure
being tested. Mutation-tested three ways — missing catalog FAILs, an all-English
catalog FAILs (first draft PASSed it, taking a per-pair exemption seven times),
one legitimate collision still passes with a note.
Both i18n gates now run in gates:post; neither was wired into the build before.
Also fixed, found by the build rather than sought:
- check-i18n-parity hardcoded an absolute path, so the studio copy silently
graded wt-packet's locales and reported PASS on a tree it never read. Now
repo-relative. That exposed the studio's own locale tree as a strict subset,
103 keys behind including buttons.json:share — the very orphan this gate's
header says it was written to catch, reappeared unseen in a second repo.
Synced forward: zero studio-only keys, zero text conflicts, lossless.
- index.json still declared field-access-visas with rows:1146, a leftover from
the commit that removed that table as private. The generator already filters
private tables; the committed artifact predated the filter. Removing the
declaration also removes the row-count leak. Copying the file in would have
reopened the leak.
- check:sfc-parse needs an installed node_modules and is the first pre-gate to
fail without one, so a dependency-less worktree reads as a parse error.
Documented, with the reason switching it to bun only appears to fix it.
146 lines
7.6 KiB
JavaScript
146 lines
7.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-sfc-script-parses.mjs — every .vue script block must actually PARSE.
|
|
*
|
|
* ⚠ IT IMPORTS `@vue/compiler-sfc`, so it needs an INSTALLED node_modules. Unlike its ~20 sibling
|
|
* check:* gates, which use only Node built-ins and therefore run in a bare worktree, this one
|
|
* dies with ERR_MODULE_NOT_FOUND if deps are missing — and it is the FIRST pre-gate to do so, so
|
|
* its failure is what a dependency-less worktree looks like from the build log. Measured
|
|
* 2026-09-24 in wt-packet: `node_modules/` was entirely absent, `bun run build` reported
|
|
* "6/7 pre gates passed — check:sfc-parse FAILED" on a tree with ZERO parse errors, and the real
|
|
* error (`Cannot find package '@vue/compiler-sfc'`) was hidden by the parallel runner.
|
|
* If you see this gate fail with ERR_MODULE_NOT_FOUND, run `bun install --frozen-lockfile` —
|
|
* do NOT chase a parse bug, and do NOT switch the shebang to bun. Switching runtimes only
|
|
* APPEARS to fix it: bun falls back to a global module cache and resolves the import on a tree
|
|
* that has no node_modules at all, so this gate goes green while all 23 bundle builds still fail
|
|
* on `Cannot find package 'vite'`. That is a masked environment fault, not a fix.
|
|
*
|
|
* THE GAP THIS CLOSES. `check:template-literal` catches a stray backtick inside a CSS *comment*
|
|
* within a template literal, but NOT one in LIVE CSS — and a live-CSS backtick breaks the build the
|
|
* same way. Proven both directions on 2026-09-21 (see
|
|
* BUG-TEMPLATE-LITERAL-GATE-MISSES-BACKTICKS-IN-LIVE-CSS): planting `content: \`x\`;` into
|
|
* doxa-simple-map's shadow CSS left check:template-literal at PASS/EXIT=0, while
|
|
* `BUNDLE=doxa-simple-map bun x vite build` died EXIT=1 with a Babel parseSubscripts stack trace
|
|
* pointing far from the edit — the exact misleading error that gate exists to replace.
|
|
*
|
|
* ★ WHY "NOT ACHIEVABLE" WAS ONE STEP TOO STRONG. That card concluded live-CSS detection was not
|
|
* achievable, on solid evidence: THREE lookahead heuristics were tried and reverted, scoring 98 and
|
|
* then 136 false positives on a HEALTHY tree (the real terminator of a template literal is also a
|
|
* backtick, so no window of surrounding characters can separate the two). The evidence was right
|
|
* and the inference over-generalised — "no heuristic works" is not "nothing works." The card itself
|
|
* named the answer in its last line: *needs a tokenizer*. A parser does not guess; it knows where
|
|
* the literal ends, because ending it is what parsing means.
|
|
*
|
|
* ★ MEASURED BEFORE BEING BUILT, against the same tree the heuristics failed on:
|
|
* lookahead heuristic (pre-fix) -> 98 false positives
|
|
* lookahead heuristic (post-fix) -> 136 false positives
|
|
* THIS (parse the script block) -> 0 false positives across 83 .vue files
|
|
*
|
|
* WHY @babel/parser SPECIFICALLY: it is the parser Vite's own pipeline reports errors from, so its
|
|
* verdict matches what actually breaks the build rather than approximating it. @vue/compiler-sfc
|
|
* splits the SFC first, so the check sees exactly the `<script>` / `<script setup>` text the build
|
|
* compiles — no regex extraction, no guessing where a block starts.
|
|
*
|
|
* WHAT IT DOES NOT DO: it is not a linter and takes no view on style. A file either parses as the
|
|
* build will parse it, or it does not. That narrowness is the point — it can never cry wolf, which
|
|
* is what disqualified every heuristic attempt.
|
|
*
|
|
* NEGATIVE-TESTED: planting a backtick in LIVE shadow CSS (the exact case check:template-literal
|
|
* misses) makes this FAIL naming the file and the line; removing it returns PASS. A run that finds
|
|
* fewer than MIN_FILES .vue files FAILS rather than passing vacuously.
|
|
*/
|
|
import { readFileSync, readdirSync, existsSync } from 'node:fs'
|
|
import { resolve, dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { parse as sfcParse } from '@vue/compiler-sfc'
|
|
import { parse as babelParse } from '@babel/parser'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
// The two authored zones. internal/ and deployment/ hold no .vue sources.
|
|
const SCAN = ['plugin-designs', 'library']
|
|
|
|
function walk(dir, out = []) {
|
|
if (!existsSync(dir)) return out
|
|
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
if (e.name === 'node_modules' || e.name.startsWith('.')) continue
|
|
const p = join(dir, e.name)
|
|
if (e.isDirectory()) walk(p, out)
|
|
else if (e.name.endsWith('.vue')) out.push(p)
|
|
}
|
|
return out
|
|
}
|
|
|
|
const files = SCAN.flatMap(d => walk(join(ROOT, d)))
|
|
|
|
// Vacuous guard: "0 files scanned" must never read as "0 problems".
|
|
const MIN_FILES = 40
|
|
if (files.length < MIN_FILES) {
|
|
console.error(`SFC-PARSE: FAIL — only ${files.length} .vue file(s) found (expected >= ${MIN_FILES}).`)
|
|
console.error(' The walk is broken or the tree is empty. Refusing to pass vacuously.')
|
|
process.exit(1)
|
|
}
|
|
|
|
const broken = []
|
|
let parsed = 0, blocks = 0
|
|
|
|
for (const f of files) {
|
|
const rel = f.replace(ROOT + '/', '')
|
|
const src = readFileSync(f, 'utf8')
|
|
|
|
let descriptor, errors
|
|
try { ({ descriptor, errors } = sfcParse(src, { filename: f })) }
|
|
catch (e) { broken.push({ rel, where: 'SFC', msg: String(e.message).split('\n')[0].slice(0, 90) }); continue }
|
|
|
|
if (errors && errors.length) {
|
|
broken.push({ rel, where: 'SFC', msg: String(errors[0].message).split('\n')[0].slice(0, 90) })
|
|
continue
|
|
}
|
|
|
|
// Both block forms compile in a real build, so both are checked.
|
|
const scripts = [descriptor.script, descriptor.scriptSetup].filter(Boolean)
|
|
let failed = false
|
|
for (const b of scripts) {
|
|
blocks++
|
|
try {
|
|
// jsx is enabled because a .vue script may legitimately contain it; enabling a plugin cannot
|
|
// make invalid code parse, only widen what counts as valid.
|
|
babelParse(b.content, { sourceType: 'module', plugins: ['jsx'] })
|
|
} catch (e) {
|
|
// Babel reports a line within the BLOCK; offset it to the file line so the message is
|
|
// actionable. b.loc.start.line is the line the block's content begins on.
|
|
const m = /\((\d+):(\d+)\)\s*$/.exec(e.message.split('\n')[0])
|
|
const fileLine = m ? (b.loc.start.line + Number(m[1]) - 1) : null
|
|
broken.push({
|
|
rel, where: b === descriptor.scriptSetup ? '<script setup>' : '<script>',
|
|
msg: e.message.split('\n')[0].replace(/\(\d+:\d+\)\s*$/, '').trim().slice(0, 80),
|
|
line: fileLine,
|
|
})
|
|
failed = true
|
|
break
|
|
}
|
|
}
|
|
if (!failed) parsed++
|
|
}
|
|
|
|
console.log(`SFC-PARSE: ${files.length} .vue file(s), ${blocks} script block(s) parsed.`)
|
|
|
|
if (broken.length) {
|
|
console.error(`\nSFC-PARSE: FAIL — ${broken.length} file(s) do not parse as the build will parse them:`)
|
|
for (const b of broken) {
|
|
console.error(` ✗ ${b.rel}${b.line ? ':' + b.line : ''} (${b.where})`)
|
|
console.error(` ${b.msg}`)
|
|
}
|
|
console.error(
|
|
'\n The build would fail on these too, but with a stack trace pointing FAR FROM the edit —\n' +
|
|
' that misleading error is what this gate exists to replace. The commonest cause in this repo\n' +
|
|
' is a stray backtick inside a shadow-CSS template literal: a backtick ENDS the literal, so\n' +
|
|
' everything after it is read as code. Write `display:block`, never a backticked form, and\n' +
|
|
' keep both synced CSS copies identical.'
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log('SFC-PARSE: PASS — every .vue script block parses exactly as the build parses it.')
|
|
console.log(' This covers the LIVE-CSS backtick that check:template-literal cannot see (it reads')
|
|
console.log(' CSS comments only). Three lookahead heuristics scored 98 and 136 false positives on')
|
|
console.log(' this same tree; parsing scores 0, because it never guesses where a literal ends.')
|