Exempt the keys of a pure data map, and scan .js

territory-counts.js is `{ 'Andhra Pradesh': 81, 'Assam': 15, … }`, and those
keys are matched against props.shapeName off the map data. Translating one
breaks the join silently: the count goes missing and nothing errors. Same
inversion as a lookup table whose values are i18n keys, except the values are
numbers, so there is no key to resolve and that rule cannot vouch for it.

Judged on the whole object, not one property. A single numeric value proves
nothing — a map of sentences with one count in it would exempt the sentences.
Every value must be a plain non-key literal, there must be at least six of
them, and the key itself must read as a name: anything with sentence
punctuation stays loud whatever its value is.

Two refinements found by running it rather than reasoning about it. The word
limit is eight, not six, because "Dādra and Nagar Haveli and Damān and Diu"
is one real union territory and the longest key in a 46-entry table; the
punctuation test is what separates a name from a sentence, not the length.
And a bare key is now tried against every area, because legend-tree's
REGION_KEY maps a region to 'region_middle_east' and the call site reads
t(`poster.${…}`) — a suffix table is exempt on the same terms as a dotted
one: the key it names must resolve.

Four two-sided cases, and one of them exists because poisoning found the
first three did not exercise the whole rule. Removing the all-values-are-data
check left every test passing, because the rule only ever exempts KEYS and
those fixtures caught their prose as VALUES. The case that bites is a table
whose keys are names and whose values are prose: a label table, not a join
table, where the keys must still count.

With the exemption in, .js files are scanned. 220 .js files were invisible
against 142 .vue. The .js findings fall from 67 to 41.
This commit is contained in:
Doxa Maps 2026-09-29 10:01:36 +00:00
parent 510832b23d
commit a3aa7dc839

View file

@ -335,6 +335,48 @@ function isDataComparison(node, parent) {
return parent.left === node || parent.right === node
}
// ★ A PURE DATA MAP'S KEYS ARE JOIN VALUES, NOT LABELS. territory-counts.js is
// export const ADM1_COUNTS = { 'Andhra Pradesh': 81, 'Assam': 15, … }
// and those keys are matched against `props.shapeName` off the map data. Translating one breaks the
// join silently — the count goes missing and nothing errors. Same inversion as a lookup table whose
// values are i18n keys, which isResolvedLookupKey already covers; this is the case where the values
// are NUMBERS, so there is no key to resolve and that rule cannot vouch for it.
//
// ⚠ JUDGED ON THE WHOLE OBJECT, NOT ON ONE PROPERTY. A single numeric value proves nothing — a map
// of sentences with one count in it would exempt the sentences. Every value must be a non-key
// literal, and there must be enough of them that the shape is a table rather than a couple of
// options. Below that floor the object is judged property by property, as before.
//
// ⚠ AND THE KEYS THEMSELVES MUST LOOK LIKE IDENTIFIERS, NOT PROSE. "Andhra Pradesh" is a name; "A
// resource not found means NOT FOUND in the pools checked" is a sentence that happens to be used as
// a key, and that is a real leak — gospel found twelve defects hiding in exactly that kind of
// already-classified bucket. Anything with sentence punctuation stays loud whatever its value is.
const DATA_MAP_MIN_ENTRIES = 6
function isPureDataMapKey(node, parent, grand) {
if (!parent || parent.type !== 'ObjectProperty' || parent.key !== node) return false
if (!grand || grand.type !== 'ObjectExpression') return false
const props = grand.properties || []
if (props.length < DATA_MAP_MIN_ENTRIES) return false
// every value a plain non-key literal: a number, a boolean, or a short bare token
const allData = props.every((p) => {
if (p.type !== 'ObjectProperty') return false
const v = p.value
if (v?.type === 'NumericLiteral' || v?.type === 'BooleanLiteral' || v?.type === 'NullLiteral') return true
if (v?.type === 'UnaryExpression' && v.argument?.type === 'NumericLiteral') return true // -1
// a short lowercase token ('scripture', 'film') is an identifier, not a sentence
if (v?.type === 'StringLiteral') return /^[a-z][\w-]{0,24}$/.test(v.value)
return false
})
if (!allData) return false
// the KEY must read as a name or an identifier, never as a sentence
const key = node.value
if (/[.!?;:]|\s—\s|\bthe\b.*\bthe\b/i.test(key)) return false
// Eight, not six: "Dādra and Nagar Haveli and Damān and Diu" is one real Indian union territory
// and the longest key in a 46-entry join table. A place name is still not a sentence — the
// punctuation test above is what separates them, not the length.
return key.split(/\s+/).length <= 8
}
function isResolvedLookupKey(node, parent, grand) {
// the literal must BE the property's key, not its value
if (!parent || parent.type !== 'ObjectProperty' || parent.key !== node) return false
@ -342,8 +384,17 @@ function isResolvedLookupKey(node, parent, grand) {
const v = parent.value
if (v?.type !== 'StringLiteral') return false
// a dotted key shape, and it has to resolve — a bare category like 'scripture' is not a key
if (!/^[a-z][\w-]*(\.[\w-]+)+$/i.test(v.value)) return false
return fallbackFullyResolved(v.value)
// A dotted key resolves directly. A BARE key is a SUFFIX the caller prefixes — legend-tree's
// REGION_KEY maps 'Middle East' -> 'region_middle_east' and the call site reads
// t(`poster.${REGION_KEY[english]}`). Try the bare value against every area rather than
// rejecting it, so a suffix table is exempt on the same terms: the key it names must resolve.
if (/^[a-z][\w-]*(\.[\w-]+)+$/i.test(v.value)) return fallbackFullyResolved(v.value)
if (/^[a-z][\w]*(_[\w]+)*$/i.test(v.value)) {
for (const area of Object.keys(catalogue())) {
if (fallbackFullyResolved(`${area}.${v.value}`)) return true
}
}
return false
}
// The `T('a.key', 'English')` / `L('key', 'English')` / `tStatic('a.key') || 'English'` shapes, seen
@ -549,6 +600,8 @@ function scanScript(code, file, out, lineBase = 0) {
// against, never a string the reader sees.
// stack's last entry IS the parent, so the enclosing object is one further back.
if (isResolvedLookupKey(node, parent, stack[stack.length - 2])) return
// A pure data map's keys are join values — see isPureDataMapKey.
if (isPureDataMapKey(node, parent, stack[stack.length - 2])) return
// A literal tested against data with === is a value the reader never sees.
if (isDataComparison(node, parent)) return
@ -716,7 +769,7 @@ const sfcsIn = (dir) => {
const walk = (d) => { for (const e of readdirSync(d, { withFileTypes: true })) {
const p = join(d, e.name)
if (e.isDirectory()) { if (!/node_modules|dist|snapshots|fixtures/.test(e.name)) walk(p) }
else if (e.name.endsWith('.vue')) out.push(p)
else if (e.name.endsWith('.vue') || e.name.endsWith('.js')) out.push(p)
} }
walk(dir); return out
}
@ -905,6 +958,35 @@ const KIND = { 'Bible Stories': 'scripture' }
'COUNTS a lookup-table key whose mapped i18n key does not exist')
ok(lk.some((f) => f.text === 'Bible Stories'),
"and COUNTS one whose value is a category, not a key — the gate can only vouch for what it can resolve")
// ★ PURE DATA MAPS. Keys joined against map data (territory-counts.js) are join VALUES; a
// sentence used as a key, or rendered as a value, is still a leak.
const DM = `<template><p>{{ x }}</p></template>
<script setup>
const ADM1_COUNTS = { 'Andhra Pradesh': 81, 'Assam': 15, 'Bihar': 22, 'Goa': 3, 'Kerala': 9, 'Punjab': 7 }
const PROSE_KEYS = { 'A resource not found means it was not in the pools checked': 1, 'Another whole sentence here': 2, 'Third sentence for the floor': 3, 'Fourth sentence for the floor': 4, 'Fifth sentence for the floor': 5, 'Sixth sentence for the floor': 6 }
const RENDERED = { africa: 'A sentence the reader actually sees here', asia: 'Another sentence the reader sees', c: 'x', d: 'y', e: 'z', f: 'w' }
</script>`
const dm = scanSfc(DM, 'datamap.vue').map((f) => f.text)
ok(!dm.includes('Andhra Pradesh') && !dm.includes('Kerala'),
'QUIET on the keys of a pure data map whose values are all numbers')
ok(dm.some((t) => /A resource not found means/.test(t)),
'COUNTS a real SENTENCE used as a data-map key — a name is not a sentence')
ok(dm.some((t) => /A sentence the reader actually sees/.test(t)),
'COUNTS a sentence used as a data-map VALUE — only the keys are join values')
// ⚠ THE allData CHECK IS WHAT STOPS A LABEL MAP BEING READ AS A DATA MAP. Keys that look like
// names, values that are prose a reader sees: the keys must STILL count, because this is a
// label table, not a join table. Without this case the allData guard can be removed and every
// test still passes — measured by poisoning it.
ok(scanSfc(`<template><p>{{ x }}</p></template>
<script setup>
const REGION_LABELS = { 'Andhra Pradesh': 'Southern India, sixth largest', 'Assam': 'North-east, tea country here', 'Bihar': 'Northern plains along the Ganges', 'Goa': 'West coast, smallest by area', 'Kerala': 'South-west, high literacy rate', 'Punjab': 'North-west, the five rivers' }
</script>`, 'labels.vue').map((f) => f.text).includes('Andhra Pradesh'),
'COUNTS a data-map key when the VALUES are prose — that is a label table, not a join table')
ok(scanSfc(`<template><p>{{ x }}</p></template>
<script setup>
const TOO_FEW = { 'Tamil Nadu': 81, 'Assam': 15 }
</script>`, 'few.vue').map((f) => f.text).includes('Tamil Nadu'),
'COUNTS the same key in a THREE-entry object — two options are not a table')
// ★ THE TWO CLASSES gospel ASKED FOR, both sides each. Their 12 real defects were hiding inside 88
// findings they had already called "mostly deliberate", so the danger in teaching the gate a class
// is that it silences the next 12 too. Each quiet case is paired with the nearest thing that must
@ -1170,7 +1252,12 @@ const dirs = productionPluginDirs('.')
const findings = []
let files = 0
try {
for (const d of dirs) for (const f of sfcsIn(d)) { files++; findings.push(...scanSfc(readFileSync(f, 'utf8'), f)) }
for (const d of dirs) for (const f of sfcsIn(d)) {
files++
const src = readFileSync(f, 'utf8')
if (f.endsWith('.js')) { const out = []; scanScript(src, f, out); findings.push(...out) }
else findings.push(...scanSfc(src, f))
}
} catch (e) { console.error('HARDCODED-ENGLISH: parse error —', e.message); process.exit(2) }
const deliberate = findings.filter((f) => f.deliberate && !f.suspect)