REGION_GATE_HARDENING_FINDINGS.md

decisions/REGION_GATE_HARDENING_FINDINGS.md

Region-page gate hardening — findings record (2026-07-22)

Produced by the bali pass-2 landing, where check_region_page.py went red and the repair ran

through three implement-then-adversarially-attack rounds. Everything CLOSED is in the gate; this

file is the record of what was found, what was deliberately ACCEPTED, and what is still OPEN.

DOC_MAP row: §12 Harness & gates. Consumer: the next person who touches check_region_page.py,

and the self-test arming work in OPEN-1.

The lesson worth keeping: a noisy gate HIDES real findings

The gate reported 42 use_class errors on bali. All 42 were false positives. Underneath them sat

THREE REAL zone-catalog errors that nobody could see, because the scorecard truncates at 20 errors

per page. The false positives were not merely annoying — they were concealing the findings that

mattered. Treat a rule that fires in bulk as a defect in the rule until proven otherwise, and clear

the noise before trusting anything below it.

The defect class: anchor-then-overscan, and its twin, anchor-then-silently-disable

Every real bug found here was one of two shapes:

use_class rule scanned every Section-16 line containing SRC_, punishing the inline citation

that cited-or-cut requires. The zone rule set its scan body to the Zone Catalog header through

END OF PAGE, so every markdown table below it was read as zone rows.

error. A one-space indent on | zone_id | disabled the entire zone rule. An H3 or indented

## Zone Catalog heading disabled the contiguity assert. Both fail OPEN and SILENT, which is the

worst failure mode a gate has.

When adding a rule to this gate, state its scope explicitly and write a fixture proving the rule

still fires when its anchor is written in every legal shape.

CLOSED this session (each mutation-proven, fixtures added to --self-test)

declared in Section 16 on a line carrying a use_class token. Strictly stronger — it gains the

never-declared-at-all class the per-line form structurally could not reach, because that form

only ever scanned inside Section 16.

a hole where a prose negation ("None of these are FRINGE-SKIP: SRC_00157, SRC_00251") made a page

with ZERO real use_class assignments pass.

CROSS-REFERENCE no longer count as declarations.

dropping every row below the break. This closes a regression the bounding fix itself introduced —

found by the adversarial pass, not by the implementer.

also accepts H3).

of the anchor-then-overscan class in this one file, and the first that had already caused real

damage. Its first repair (pure nearest-neighbour) then UNDER-scanned and was widened to

anchor-plus-coordinated-list the same day. See the section below.

Two implementation traps, recorded because both were live failures

ABOVE the header, the contiguous run terminates immediately, the bounded body comes back empty

and all four pages go red. Horizontal whitespace only: ^[ \t]*\|. An inline ANCHOR NOTE guards

this in the source; do not "simplify" it back.

corpus, and corrected it. Critics outrank authors on findings, but a critic's prescribed REMEDY

is still just a proposal — run it before adopting it.

The link_type rule: instance THREE of the defect class, and the one that cost prose

This one is worth reading in full, because the false positive did not merely annoy — it propagated

into canon. The gate reported:

link_type: LINK_0185 is 'knowledge-transmission' but the line names ['parallel-independent']

That was read as "the page mischaracterizes LINK_0185". IT DID NOT. The page already said

knowledge-transmission. The error came from a DIFFERENT physical line — a bookkeeping sentence that

co-mentioned LINK_0185 beside the token parallel-independent, which described LINK_0186 and

LINK_0187. The old rule pooled every LINK id and every hyphenated type token on one physical line

and demanded each id's registry type appear somewhere in that pool, so an id merely NAMED next to

another link's type went red. Acting on that false premise, a fixer rewrote a correct,

critic-verified sentence on sumatra_java.md and in doing so deleted the registry-endorsed

guardrail that Angkor is "an author, not a passive recipient" — a §17.1 care defect in the

OVER-CREDITING direction (shifting authorship of a Khmer achievement toward the Indic source).

That prose was reverted before this fix; grep "because it received that frame" over the pages

dir now returns nothing, and sumatra_java.md:3851-3859 carries the correct both-things-are-true

wording again. Recording it here because the lesson generalizes: a false gate error is not a

cosmetic problem, it is an instruction to break working canon.

THE FIX, ROUND 1 — the unit of judgement is the CLAIM, not the LINE. Each hyphenated type-token

occurrence binds to the nearest LINK id on its line, measured edge to edge, ties resolving to the

id that PRECEDES the token (the corpus's dominant idiom is "the <type> LINK_x"). An id is judged

only against the tokens actually bound to it. Why this scoping and not the alternatives: "only

check lines with exactly one id" would blind the rule on every real bookkeeping line and is

trivially evaded by adding a second id; "require strict adjacency" breaks the equally live

LINK_0185 (link_type knowledge-transmission) and the parallel-independent LINK_0186 shapes,

which put the token on opposite sides at different distances. Nearest-neighbour keeps both.

ROUND 2 — nearest-neighbour alone was a COVERAGE LOSS, and it is now closed

Round 1's stated price ("a plural token is checked against the NEARER id only") was not an

acceptable price, it was a regression: in the coordinated shape

LINK_0185 and LINK_0187 are both parallel-independent

the single token bound only to LINK_0187, so a mistyped LINK_0185 went UNJUDGED — and the OLD

per-line pooling had caught exactly that. The rule's whole purpose is the §17.1 case where a page

asserts independent invention over a registry-recorded transmission, so losing the far id of a

coordinated list loses the defect class itself. The idiom is live: sumatra_java.md Section 15

writes "The flying-viscera-witch LINK_0186 (…) and the hydraulic-achievement LINK_0187 (…) are both

parallel-independent." Both ARE parallel-independent, so the page is correct — but round 1 would

not have noticed if one of them were not.

Binding is now ANCHOR + COORDINATED-LIST EXPANSION (bind_types_on_line). The token still anchors

to its nearest id, then grows outward in BOTH directions across every adjacent id whose intervening

text is list GLUE, and is claimed about every id in that list. Glue is deliberately narrow — the

gap must contain a coordinator (,, and, or, &), must contain NO type token, must contain no

sentence break (., ;, :), and, after parenthetical/bracketed asides are stripped, every

remaining word must be a bare connective (and or the a an plus both) or a hyphenated descriptor.

That single whitelist is what separates the three behaviours that must coexist:

shapebindingwhy
A (aside) and the hydraulic-achievement B (aside) are both <type>A and Bgap is glue: aside stripped, only and/the/a hyphenated descriptor remain
A, B and C are <type>A, B and C, and and are glue
the knowledge-transmission A and the parallel-independent BA→kt, B→pi, separatelythe gap CONTAINS a type token, which ends the list
A (link_type <type>) / the <type> AA onlyno coordinated neighbour
B originate on the excursion page and A is received therenothing boundoriginate/on/excursion/page/received are not connectives — the walk stops, so a binding cannot run away down a sentence

WHAT IT CAN AND CANNOT SEE (the honest account):

parenthetical asides between the members.

live sumatra_java Section 15 sentence wraps — its two ids sit on one line and its "are both

parallel-independent" predicate on the next — so neither line judges it today. This is the one

residual that costs LIVE coverage. Joining continuation lines was rejected as too blunt a

remedy, but the reason first recorded here was FALSE and is corrected: it claimed that joining

the next sentence's lines would bind parallel-independent to LINK_0186 across "originate on

the excursion page and". An adversarial probe joined those exact lines and it binds NOTHING to

LINK_0186 — the glue whitelist stops at "originate". The honest reason to defer line-joining is

that it widens the binding surface without a proven need, not that it demonstrably breaks. If

wrapped claims are to be judged, that is the follow-up to build, with its own teeth proof.

version of this entry claimed that "removes nothing real"; that is FALSE and is corrected. A

real live case exists: angkor_khmer.md:537 writes an inline SET LABEL — "the

weaving-progression set: the architecture-proportion weaving LINK_0121 ... is deepened by ...

and the guardian-at-the-gate weaving LINK_0129 ..." — where the original per-line pooling judged

LINK_0129 and the current binding does not. Measured across all four pages the loss is exactly

one id (angkor_khmer 4 bound vs 5 pooled; bali, flores and sumatra_java deltas empty). Recorded

as OPEN-5 rather than papered over.

knowledge-transmission and parallel-independent respectively" binds each token to the wrong id.

The first version claimed this "fails LOUD, never silent" — FALSE, corrected: coordination

expansion gives every id in the list every token, so the actual not in named check can never

fire and the wrong pairing passes SILENTLY. It was equally silent under the original pooling, so

this is not a regression — but it is a silent hole, which is the worst kind, and it is OPEN-6.

lexical, and still the critic's.

expansion walk, but the nearest-id anchor does not, so a type token in a DIFFERENT sentence on

the same physical line binds to an id it does not describe and fires a FALSE RED. Inherited from

round 1, not introduced by round 2, and no live page triggers it — but this file's own headline

lesson is that a false gate error is an instruction to break working canon, so it is OPEN-7.

TEETH, proven not asserted:

inline characterization + the bare co-mention shape + a two-token bookkeeping line + the live

coordinated are both shape + a three-id comma list) and LINK_TYPE_BAD_FIXTURE (LINK_0185, a

knowledge-transmission link, typed parallel-independent). The self-test asserts the BAD fixture

flags ONLY LINK_0185 — flagging a co-mentioned LINK_0186/LINK_0187 would prove the rule had slid

back to judging the line.

(LINK_0188, knowledge-transmission, swept into an "are both parallel-independent" predicate).

The self-test requires the error to NAME LINK_0188, and separately requires that the correctly

typed near id LINK_0187 is NOT flagged — so the expansion cannot pass by over-binding.

characterization teeth are missing".

reproducing the ORIGINAL false error verbatim: `LINK_0185 is 'knowledge-transmission' but the

text binds ['parallel-independent'] to it`. This is the proof the GOOD fixture captures the real

defect and not a strawman — an earlier draft of the fixture used only the two-token line and

survived mutation B, i.e. it was a vacuous fixture. If you weaken this rule, that fixture is what

catches you.

forcing _is_coordination to return False) → self-test RED, exit 1, naming the far-id tooth:

"the FAR id of a coordinated list … was NOT caught". Restored and verified BYTE-IDENTICAL by

sha256 (fd8615d4afc1e2168a1522d78b238a013d903e9826d82950e34cd5be01981753; the round-1 hash

recorded above no longer applies — the file changed).

(angkor_khmer 6, bali 10, flores_island 1, sumatra_java 7), binding them to 22 distinct ids —

unchanged by round 2, because every live coordinated list is a WRAPPED one. The expansion loses

no live coverage and adds the far-id tooth for every unwrapped list. All four pages pass.

ACCEPTED residuals (deliberate; recorded in the gate docstring, do not re-engineer)

bracket declares, including an ordinary inline prose citation. The adjacency alternative was

tested and re-breaks the legal grouped-declaration shape flores_island uses (four sources bound

to one SUBSTRATE token in a single bracket). No live page reaches this.

green is not evidence the rule works.

contiguity assert. It fails LOUD, and no page has one.

This is the one residual that currently costs live coverage (sumatra_java Section 15 wraps its

coordinated are both parallel-independent sentence). Rejected remedy: joining continuation

lines, which re-opens run-away binding. The critic covers it.

OPEN — real, unreached by current data, worth doing

were unarmed; a rigorous per-rule mutation sweep (mutate each decisive condition to a no-op, run

--self-test, record green/red) found the count was UNDERSTATED — the "header checks" are three

independent string checks and the "machine-blocks check" is two, and it omitted region_id:resolves

and region_id:snake_case entirely. The definitive unarmed set was ELEVEN conditions. All that

CAN be armed now ARE (each with a minimal single-rule BAD fixture in --self-test, each

mutation-proven RED and naming its own tooth):

every prior self-test call passed region_ids=set(), so this elif branch was structurally

dead in the self-test — the reason its mutation stayed green)

branch was already armed by FK_BAD_FIXTURE, but the char_ids branch had never been

exercised; now it is.

link_type was already ARMED before this pass (see the section above); confirmed still red

under mutation.

**The ONE residual — region_id:snake_case — is UNREACHABLE DEAD CODE and cannot be armed by any

fixture.** REGION_ID_RE's capture group ([a-z0-9_]+) guarantees the captured id already

satisfies re.fullmatch(r"[a-z0-9_]+", rid), so if not re.fullmatch(...) can never be True. An

uppercase value makes the regex fail to match and trips region_id: not found instead; a hyphen

truncates to a passing prefix. This is not a missing fixture — it is a branch no input can reach.

The honest fix is a LOGIC change (widen REGION_ID_RE to capture a looser token so a malformed

slug reaches the snake_case check, or delete the dead branch), out of scope for a fixture-only

arming pass and flagged here for the next editor. A NOTE in self_test records the same.

With that one exception every rule the gate enforces is now armed and mutation-verified.

ENTIRE zone rule vanishes silently — the same shape the indent fix closed, but for the column

name.

validation if its zone_id carries any uppercase, if the cited site token is capitalized, if the

source cell names one VALID site alongside an invented one (the rule is "at least one resolves",

not "all resolve"), if the cell mentions region-ambient anywhere (a by-design exemption that

also exempts an invented site named on the same line), or if the invented token is four

characters or shorter. Verified non-vacuous on real data first: all 27 zone rows across the four

pages are single-site and fully resolve, and no live row uses the region-ambient exemption, so

the four green pages are genuinely validated rather than passing through these holes.

nowhere. Dead constant that invites a reader to think the zone scan flows through it.

inside a "weaving-progression set" label whose type token binds elsewhere, so the original

per-line pooling judged it and the current binding does not. Exactly one id across four pages

(angkor_khmer 4 bound vs 5 pooled). Either bind a set label to its whole list, or accept it in

writing — do not leave it undocumented, which is what the first draft of this file did.

every id in a list every token, so a wrong pairing cannot fire actual not in named. Equally

silent under the original pooling, so not a regression — but a silent hole is the worst kind and

it should either be caught or explicitly declared out of scope.

expansion walk; the nearest-id anchor does not, so a type token in a different sentence on the

same physical line binds to an id it does not describe and fires a FALSE RED. No live page

triggers it. This is the same false-positive class that already cost canon prose once today.

coverage today (sumatra_java Section 15). See the CANNOT-see block above for why line-joining

was deferred rather than rejected on evidence.

Generated by harness/site/structure_site.py — the URL path is the repo path. review root