Comments in library/ and internal/ that quoted a person, attributed a decision to one, used first person,
named fleet agents or carried private host names now state the same technical fact in neutral words:
poster-render (including the comments it writes into generated posters), packet-render, the poster and
research scripts, PosterWall, DataTable, the legends, the layouts and the gates. The network-policy note
states the policy itself. Behaviour is unchanged: a few printed gate messages and reason labels drop an
agent name, the poster-render integrity pin follows its comment-only edit, and series.json changes only in
its _meta and _why notes. The historical poster design snapshots (library/scripts/posters/snapshots/),
which nothing reads, are removed.
studio's judgement on the nineteen REGION_OF findings: the English values are
identities, not reader text. Node ids derive from them, and every surface
that shows one translates it at render — upg-100-list.vue:440 and
poster-wall.vue:129 both read REGION_KEY[english] and call t(`poster.${…}`).
The exemption is conditional, and the condition is the whole rule. It holds
only for a name that IS a key of REGION_KEY. A region added to REGION_OF
without a matching key falls through the `|| english` arm and renders English
in all eleven locales, which is the defect upg100 already shipped once with
Americas and Europe. So the gate keeps working as the table grows rather than
blessing the file.
Deliberately not "object values" and not "legend-tree.js", as studio asked:
either would exempt the next table nobody has checked.
The parser had no newline requirement in the end, and finding out why is the
part worth keeping. The first version anchored on a closing brace at the
start of a line, so it never matched a REGION_KEY written on ONE line —
regionKeyNames returned null, the rule exited early, and BOTH two-sided cases
passed while the exemption never ran once. Poisoning the membership test
produced no failure, which is what exposed it. With the anchor gone the
poison fails, so the conditional is now proven load-bearing.
.js findings 41 -> 34.
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.
`throw new Error(`head ${n} tail` + detail)` puts a BinaryExpression between
the literal and the NewExpression, so the parent-only check saw the `+` and
not the throw, and reported a thrown message as a reader-visible leak.
Found while measuring the .js scan on legend-tree.js:392, and my first
report of it was wrong in a way worth recording: I told dx-shepherd the
developer-site exemption "is not firing on the .js path". It fires fine.
The gap is concatenation, and a .vue file concatenating inside a throw hits
it identically — the .js scan only surfaced a shape that is rarer in
templates. Three .vue findings cleared when this landed, which is the proof
it was never .js-specific: 92 -> 89.
Only `+` chains are walked. A string built with + is still the same argument
to the same call; any other expression type between the two is a different
construct and should be judged on its own site.
Two-sided: a thrown message built by concatenation goes quiet, and a
concatenated sentence that is NOT at a developer site still counts.
The .js scan itself is NOT in this commit. It needs the data-map exemption
first, or it hands two owners ~60 findings that are lookup keys they have
already classified.
(cherry picked from commit f96381481b)
exp's last experimental seam. A print page assembled by concatenation —
'<html lang="' + locale + '" dir="' + dir + '">' — leaves the quasi '" dir="',
which is the seam between two attributes rather than between two CSS
declarations, so the seam shape added for exp does not cover it.
The cause was smaller than the symptom: dir and lang were the only standard
attributes missing from the HTML-fragment list that already holds class,
href, src, alt, loading, width, height, style, id, target and rel. They are
attribute names, not text a reader sees, and every RTL-aware print page
writes both.
Asserted on the shape rather than through a fixture, for the reason the CSS
seam commit records: this gate judges a split template literal as one string,
so no .vue fixture can produce a seam, and a fixture-based test passes
whether or not the rule exists. Removing dir|lang fails the assertion.
prayer-walk-planner.vue now has ZERO findings. The same shape in
library/packet/packet-render.js:1460 is not reachable yet — the scan walks
.vue only — and this rule will cover it when .js coverage lands.
exp found that splitting `width:${w}px;height:${h}px` leaves the quasi
"px;height:" — a seam between two declarations, carrying the tail of one
value and the head of the next property. It has a colon but no value and no
brace, so every CSS shape here missed it. Their regex is used as written.
The semicolon is what keeps prose out, and it is load-bearing: "12 people:"
has the colon but no declaration before it, and "Pools checked; nothing
found:" has both a semicolon and a colon and still counts, because the
segments either side are not property:value pairs.
Tested on the SHAPE, not through a .vue fixture, and the first version was
wrong in a way worth recording. A seam only exists as a separate string if a
quasi is judged alone, and this gate deliberately judges a split template
literal as one string — so no fixture can produce one. The fixture version I
wrote first passed with the rule DISABLED, which is exactly the vacuous green
this file exists to refuse. Asserting at the shape fails when the rule is
removed.
Measured before adding it: exp's four upg-neighbor findings and the CSS half
of prayer-walk-planner's are already gone, fixed by the two CSS shapes that
landed after their measurement. The only seam left in either bundle is an
HTML attribute one (`" dir="`), which this does not cover. So the rule
guards against a recurrence and against callers that do judge quasis
separately, rather than clearing a live count.
The legend's region sections, its Deaf row and its one-country zones, the
wall's card names, the print progress and error messages, the map's status
badge and the Population column all rendered English in every locale. None
of them was visible to check:hardcoded-english: they were single words, .js
modules, Error() arguments, library components or number formatting.
- Legend: region rows through one REGION_KEY table shared with the wall
(poster.region_*, new region_deaf); zones through zoneTitle (tier A by
country, the Deaf row by its bloc name); node ids stay English-derived.
- Wall: card labels through zoneTitle, so the Show-on-map name, alt text,
tooltip, HUD title and printed title agree with the legend and packet.
- Errors a reader sees are keyedError(key, English, params);
errorText(e, t) shows them translated, and an unkeyed error still shows
its own message. Developer detail for a failed data load goes to the
console; the badge says upgMap.mapDataFailed.
- Population: Intl compact notation in the app's locale, same precision.
- Country Posters: the unsupported card's reason is
messages.countryPosterTooMany, not the data file's English note.
- Strings only the UPG-100 map shows move to a new upgMap area, so other
bundles do not carry them (country-posters 1,490,907 bytes).
- check:hardcoded-english judges keyedError's English as a fallback
(quiet when the key resolves everywhere, a finding when it does not);
check:label-key-coverage follows REGION_KEY to legend-tree.js and covers
the legend's table and its regionFor producer. Both self-tests two-sided.
gospel found 12 real defects — including a search box's aria-label, which is
screen-reader text — inside 88 findings they had already described as mostly
deliberate. None were visible from the count. They classified every remaining
finding by hand and named the two shapes the gate miscounts.
A string compared with === against table data is a value, not a label.
`scope === 'Full Bible'` tests a cell of the shipped resource table; the
reader never sees that literal, and the row it selects renders through a key.
Translating it makes the comparison miss and the feature stop, while the
count falls and it looks like progress — the same inversion as a lookup-table
key, which is why gospel classed them together.
A separator followed by a brand is not a sentence. `{{ t('…') }} · GRN`
splits into a keyed half and the text node "· GRN", and only the second
reaches this scan.
The names are LISTED, not patterned. "Punctuation then a capitalised word"
would swallow "· Not found in any pool", a real caveat, and that is the class
gospel's 12 defects were hiding in. A name earns its place by being a
publisher or standard that is the same word in every language.
Each quiet case is paired in the self-test with the nearest thing that must
stay loud: a caveat after the same separator, prose that merely begins with a
brand, a real English sentence used as a table key, and an .includes()
argument. Poisoning the name list into a punctuation rule fails.
Poisoning the operator check did NOT fail, which is why there is now a
concatenation case: `greeting + 'Welcome to the prayer map'` is a
BinaryExpression too, so accepting any operator would silence a sentence
assembled for the reader. Only an equality test means "compared against
data".
Also the last finding in this owner's plugins. All seven main tabs resolve
through tabs.<id> in all eleven locales, so their English labels could never
render — a second copy of each name that could only drift. The sub-tab tables
lost theirs the same way.
Fleet 152 -> 143. This owner's plugins: 1 -> 0.
gospel's resources-list maps the resource titles that arrive in the data to
the i18n keys that render them:
const RES_KEY = { 'Bible video': 'gospel.res.bibleVideo', ... }
title: RES_KEY[r.title] ? tr(RES_KEY[r.title]) : r.title
The English on the left is matched against a value from the data; what the
reader sees is the key on the right. Wrapping the left side in t() would
make every lookup miss and fall through to the raw data value, so the count
would drop while the display broke — the worst shape a gate finding can
have. Found by gospel, who triaged it rather than acting on it.
Exempt only when the mapped key really resolves in every locale, the same
way a fallback has to earn its exemption. A table pointing at a key nobody
wrote still counts: the lookup succeeds and hands t() a key the catalogue
has never heard of. A value that is a plain category rather than a key
counts too — the gate can only vouch for what it can resolve.
Self-test covers all three arms, using the real RES_KEY case. Poisoning the
exemption to skip the resolution check trips it.
gospel 153 -> 146; fleet 159 -> 152.
Eighteen keys across eleven locales for text that was written in English in
the components: the rail and filter toggles, the selection count, the CSV
export toast, the drawer's prayer and information links, the dashboard
title, the map's failure message, and the four search placeholders that name
the tiers of whichever tab is open.
The export toast is interpolated rather than concatenated. Its counts keep
going through fmt(), which groups digits the way each locale does, and only
the words around them come from the catalogue — the order of "rows",
"fields" and the outcome is not the same in every language, so a sentence
built by joining fragments cannot be translated at all. It uses the shared
fillParams, which leaves a placeholder visible when a value is missing
instead of rendering the word "null".
The map's failure message no longer names the token and the container: that
is for the console line, which stays English for whoever is debugging. What
the reader sees says what to do.
Three more shapes the scan could not see, each of them correct code: tStatic
called directly rather than through a wrapper; `config || t(key) || English`,
where the translator is not the immediate left operand because || is
left-associative; and the licence credits, media queries and product name
that must not be translated at all.
Own leaks 91 -> 1, the last being a tab label behind a key that resolves in
all eleven locales. Fleet 249 -> 159. check:i18n-parity,
never-translated, treeshake and stopped-scope all pass.
tStatic is the library's translator for code outside a component's setup, and
it was missing from the set of names the scan knows. Anywhere it was called
directly rather than through a wrapper, correct guarded code read as a leak.
The two-sided rule still applies to it: a tStatic fallback is exempt only
when its key resolves, and the self-test asserts both halves.
Three allow shapes, each for a string that must not be translated:
A matchMedia query. Rendering "(prefers-reduced-motion: reduce)" in another
language matches nothing, so motion safety would switch off for exactly the
readers who asked for it, and nobody would see an error.
Tile licence attribution. The credit a data licence asks for is the one
string that stays as the licensor wrote it in every language. These render
only when the live sources cannot be read, which is the case a licence never
allows to come out empty.
The product name, and bare CSS position keywords.
Each shape is anchored rather than fuzzy: prose that resembles them —
"Start end of the list", "center the map on your region", "Doxa Maps is a
prayer tool for the unreached", a copyright line with a real sentence in it —
is still reported. 190 -> 178.
Every reference to the Driver in internal/ is reworded the same way as in
library/: the reason stays, the speaker and the quote go, an open decision
reads "provisional, pending a product decision", and agent names and decision-
sheet rows are dropped. Where a gate printed the word, its message now names
the reviewer or the ruling instead.
Three spots were code, not comments, and changed with their callers:
- the element census's allowlist field `driver` is now `ruling`, in the gate
and both presets (same validation: match, ruling, date and card required);
- check:i18n-never-translated's DRIVER-GATED marker is DECISION-PENDING, in
the entries and in the regex that counts them (its output is otherwise
byte-identical);
- the package.json description of the census says "ruling".
public-hygiene-check keeps its two patterns for the word: they are what keep
it out of the served folder.
T('dashboard.map_loading', 'Research map loading…') is the idiom these
components use: the English is what shows if the catalogue ever loses the
key, not what the reader gets. Forty-four findings were code of exactly
that shape, and triaging them meant reading catalogue entries that were
already complete in all eleven locales.
Two things were wrong. Wrapper aliases were not recognised as translators
at all — only `const X = t` was, never `const T = (key, en) => ...` — so
every fallback in those components read as a leak. And the rule that
exempted translator arguments exempted all of them, including the fallback,
which left the gate structurally unable to see a typo'd key: the English
then renders forever while the call site still looks correct.
So the key argument stays exempt and the fallback argument has to earn it,
by resolving to a real value in every shipped locale. A missing key still
counts. An identical rendering counts only when the English has three or
more translatable words: measured against this catalogue, the identical
entries are cognates (Normal in de, Auto in fr), proper nouns, and
format-only strings like "{label} · {count} · {pct}%", and failing on those
would report correct translations as leaks.
Bound expressions are parsed on their own, so they could not see the
translator declared in the script block; the declarations are carried in as
stubs. The stubs deliberately contain no || — written the natural way they
match the te()-guard shape and exempt every fallback in the file, resolved
or not.
Self-test covers both sides: a resolved fallback is quiet in template and
script, an unresolved one counts in both, and the catalogue rule is
asserted directly. 249 -> 205 across the fleet, all 44 in this owner's
plugins; detection elsewhere is unchanged.
Triage meant grepping the reported string to find its site, and the first
textual match is very often a comment that discusses the string rather than
the code that renders it. Two findings were sited to prose that way, and a
count built on those sites was wrong.
Each finding now carries the line. <script> and <script setup> are scanned
separately rather than joined, because concatenating them made every line in
the second block wrong by the length of the first — a confident wrong site
sends the reader further off than no site at all. A bound expression is
re-parsed on its own, so its findings take the attribute's line instead of
the fragment's own numbering.
The self-test reads the reported line back out of the fixture and requires
it to contain the string, so an offset error fails rather than passing with
a plausible number.
Detection is unchanged: 249 before and after.
check-hardcoded-english judges each quasi of a template literal on its own, which
is correct -- an allow-listed fragment must not drag a whole literal in with it.
But splitting `url(${base}/images/worldmap.svg)` leaves "url(" and
"/images/worldmap.svg)", and `repeat(${n}, minmax(0, 1fr))` leaves "repeat(" and
", minmax(0, 1fr))". The whole string is obviously CSS and the existing CSS shape
matches it; no individual fragment carries a brace or a colon-value pair, so each
one fell through and was reported as user-visible English.
Colon-terminated prefixes failed the same way. The id rule accepts a trailing
hyphen, because `id="country-${code}"` leaves "country-", but a langTree id is
`fam:${x}` / `dial:${x}` and a localStorage key is `pgg-cards-per-row:${profile}`.
Those leave "dial:" and "pgg-cards-per-row:", which no shape described.
Two shapes, both narrow: a CSS function fragment, and a prefix that is one
lowercase token ending at a colon. The fleet total goes 257 to 249 -- five of
mine, three in other owners' plugins -- and every real string still flags, which
I checked in the same breath rather than afterwards: "Search people groups…",
"Show filters", "selected" and "Research map loading…" are all still caught.
Worth saying plainly because the number was about to be routed as work: these
eight were never translation defects. Wrapping "dial:" in t() would have been a
bug, and someone would have done it on the strength of a count.
The module said a cycle terminates rather than hanging. That was prose. The
manifest has six self-composing edges today, through the bundle/profile form,
and upg100 measured that a naive recursive walk hangs on it -- so the claim was
load-bearing and untested, which is the same gap as an untested fallback.
The fixture now carries a self-composing bundle and a mutual two-cycle. If the
closure ever regresses to recursion this test does not fail, it hangs, which is
a louder signal than a red assertion.
A gate scoped by a bundle's own declared stage misses what a production reader
sees. Two production leaves compose three bundles that declare themselves
experimental, so the code on those pages was held to the experimental bar --
not by anyone's decision, but because the scope was computed from the wrong end
of the composition.
Effective stage: production if it declares production, or if anything
production-for-gating composes it, transitively. The graph's edges come in two
shapes, a bare bundle name and a bundle/profile pair, and reading only one
silently halves it.
It lives in its own module because check:frozen-labels needs the same rule.
Two gates with their own idea of "production" would disagree by passing a file
the other fails, with neither obviously wrong.
The scan goes from seven plugins to ten, nineteen files to twenty-nine, and
from 178 to 274. The 96 are not new: they are English on production pages that
no gate was measuring. The report names which plugins are in scope only by
composition, and why, since that is the first question a reader will have.
The deliberate bucket was one flat number, and it is the one place where
over-claiming costs nothing and shows nothing: a marker covering seventy-odd
lines and a one-line span read identically in a total. gospel went looking for
an over-broad island on their own card, suspected it, and found twenty-seven
strings behind a single attribute -- including the prose that says the figures
mean "the pools searched" rather than "nothing exists".
Each suppressed string now names the island that suppressed it, and the report
groups by island, largest first, in strings and in lines. A large island is not
automatically wrong, but it is the shape an over-broad one has, and nobody
should have to suspect it first.
It finds the one gospel measured by hand, and a larger one they had not: eighty
-four lines and twenty strings behind another single attribute in the same file.
Inheritance down the subtree is unchanged; it is correct. What changes is that
the output can now be read.
A literal string passed to a child that renders it is invisible to every scan
we have: in the parent it is an attribute value, in the child an interpolation
of a variable. The English and the render are in different files, so no
single-file rule can join them. One plugin hands a component a unit and a noun
that it prints as visible text, and a shared component derives the plural by
appending an s -- an English rule in library code, with no English string at
the site to find.
Reported as suspect, never counted. Whether the child renders the prop is not
knowable from here; it may be a key, an id, or an event name. Calling a suspect
a defect would make the count untrustworthy in the other direction, so this is
a question for a human rather than a verdict.
Only component tags, and only prop names that suggest rendered text, so an
event name is left alone. Plain elements keep going through the existing
visible-attribute rule.
Canary mode now triages an unplantable file too. "No text surface" and "hands
literal English to a child" are opposite risks, and reporting them together
buried the second inside the first -- which is exactly how this class stayed
hidden inside a bland count.
Both serious bugs in this gate made it quieter, and quiet reads as good news.
A zero is only worth what the detector is worth on that file, so canary mode
plants a known leak in each production SFC and requires it to be caught. A
plugin that cannot catch a planted leak cannot be trusted to report zero.
In memory rather than a temp directory: there is no cleanup to get wrong and no
chance of scanning a stale copy.
The canary goes inside the root element, located by the parser. Two earlier
attempts were wrong the same way. Injecting after the template tag made it a
sibling of the root, inheriting none of the root's attributes -- and a
root-level attribute is exactly what blinded a plugin, so that canary would
have passed the plugin it could not see. Finding the root tag by pattern then
broke on an attribute containing an arrow function, whose greater-than ends the
match mid-attribute. A pattern cannot tokenise markup any more than it can
tokenise JavaScript.
A skip is reported, not counted as a pass: a file with no plantable first child
has an unproven zero, and hiding that would repeat the mistake the mode exists
to prevent. Two such files are named in the output.
The island rule also accepted a bound :dir whose expression merely contained
the text 'ltr'. That matches the locale-aware switch every RTL-ready component
puts on its root, so such a component's entire template was treated as
deliberate English and every leak inside it was silenced.
regions-dashboard read zero for exactly that reason. Not clean: blind. Three
production plugins use the same pattern, so their counts were understated too,
and the failure is the dangerous direction -- a scan that under-reports looks
identical to code that is fine.
It surfaced from a one-count disagreement with gospel that we both expected to
be a stale tree. It was not: their run counted a mis-marked label as deliberate
where mine had already fixed it, and chasing the single-digit gap found the
bug.
A dynamic direction switch is the opposite of "this stays English" -- it is the
component promising to follow the reader. The fixture root now carries one, so
every assertion runs inside such a component and the blindness cannot return
without the whole self-test failing.
The stamp sat on the header. gospel pointed out they only investigated our
mismatched counts because the arithmetic would not close -- an eight-hit gap
was too large to rationalise -- and that a gap of one or two would have been
shrugged off as drift.
So the failure that still gets through is a discrepancy small enough to explain
away, and provenance on a line the quoter skips does nothing for it. The
verdict line is what gets pasted into a report, so the tree travels with the
number instead of three lines above it.
It was added for the writer who forgets to name a sha. Within the hour it
earned itself on a different case: gospel and I compared counts, disagreed,
and spent the exchange assuming one set was stale. Neither was. Same script,
different source trees, twelve scanned files apart -- two honest measurements
of different bytes.
"19 SFC(s) scanned" describes the shape of a run and says nothing about which
bytes, so two correct runs are indistinguishable in a report. The stamp is what
lets a reader tell them apart, which is the better argument for the line and is
now written beside it.
gospel's diff of a hand triage against the checker found this: a selector
listing two custom elements was reported as user-visible English, because it
reads as English only where the tag names happen to be English words. The
single-tag rule matched neither, since it is a comma-separated list.
Exempt by call site rather than by content, as with developer strings. No
content rule can reliably tell a tag list from a sentence, and the site is a
fact about what the string is for. Covers querySelector, querySelectorAll,
closest, matches and the getElementBy* family.
The self-test plants gospel's own case and a second list of custom elements
from another plugin, since the pattern is not theirs alone.
A namespaced translator qualifies every key with its own area, so a key from
another area can never resolve and the English fallback becomes the normal path
in every locale. The reader sees English exactly as if it had been hardcoded,
which is why it belongs here rather than in a separate check.
Nothing caught it before. The existing key check reads the key as written,
which does exist; it is the key as qualified that misses. exp-shepherd hit it
twice in one day, in two different plugins.
Detected by shape -- qualified key absent while the unqualified one is present
-- so it cannot fire on an ordinary missing key, which is the other check's job.
The first version found only one of the two real cases, because the other
declares its translator through a wrapper that takes no argument and carries
the namespace one indirection away. Wrappers are now resolved from source, and
both historical defects are reproduced from the code as it stood before each
fix, with the qualified key named in the message.
Self-test now makes twenty-eight assertions, including a planted wrapper.
gospel, upg100 and exp-shepherd triaged 300-odd hits between them, and most of
what they found was the gate being wrong rather than the code.
False positives removed, each from a real hit: class and id suffixes built by
interpolation, relative URLs, custom-element tag names, and empty matches.
Developer strings are now exempt by CALL SITE rather than by content, because
the site is the fact and the wording is not. A split template literal is one
finding reported as its whole string with its holes marked, instead of one hit
per fragment -- that alone was turning single defects into three.
Text inside a dir="ltr" island is DELIBERATE English, the marker these plugins
already use for content ruled to stay English. It is listed but not counted, so
nobody is asked to undo a decision.
The header gains a second blind spot, found the same day by exp-shepherd: a
guarded fallback is judged by its shape, not by whether the lookup it guards
works. A key read with the wrong prefix misses every time, making the English
arm the normal path in every locale while this stays quiet. I had triaged
exactly that line as "not a leak" because the key existed -- existing and being
found are different things.
Self-test is twenty-three assertions: every leak shape seen, every correct form
silent, both blind spots pinned.
Interpolation expressions carry the same defect as bound attributes: a literal
the reader sees, assembled in an expression a template walker skips. Added
before the gate lands, so the count each owner triages is the real one. It rose
by twenty-nine.
Two more shapes are fallbacks rather than leaks, both found by triaging real
hits. A proxy translator whose member read misses, and the idiom that asks
whether the translator handed the key back, which is what a missing key looks
like in vue-i18n. The second is matched on the key's shape -- dotted, no spaces
-- so an ordinary comparison against a word is not mistaken for one.
Both directions are planted in the self-test, which now makes seventeen
assertions: every leak shape must be seen, and every correct form must be
silent.
sr, upg100, gospel and regions each wrote a scanner for user-visible English
that never reaches a translator, and each found real defects. Four copies means
four sets of blind spots and four allow-lists drifting apart, so this merges
them. Every rule earns its place from a defect one of those scanners caught.
Templates and script blocks are parsed, never matched with a regex: a regex for
string literals matches from the closing quote of one to the opening quote of
the next, which produced 137 phantom findings before this was parsed properly.
Translator aliases are read out of each file, because `const t2 = t` defeats a
check that knows only the base names. Guarded fallbacks are not leaks, in both
shapes that exist here: an existence check around the call, and a proxy whose
member read misses. Bound attributes are parsed too, in either quote style,
which is the blind spot every one of the four scanners shared.
The self-test plants a leak of each kind and requires the gate to see it, then
plants the correct forms and requires silence. It also asserts the fixtures are
valid Vue first, because a malformed fixture and a blind scanner produce the
same silence.
The header records what this cannot see: single-word strings. A bare 'Other'
used as a fallback label is user-visible English and will not be reported. That
trade keeps the signal readable, and the self-test pins it so that closing the
gap forces the header to be corrected rather than left stale.