Validation Gates That Always Pass: Add a Negative Control
Part 5 of a series — a Dynamo tunnel geometry that was already finished, a 39-sheet Excel workbook it had never been wired to, the web viewer built to referee the two, and the Revit add-in at the end of it.
By the middle of the project the instructions stopped sounding like design work. They stopped being "build the tunnel section" and became "this part, this property, this authority." That shift is what let a pixel-level gate exist at all — and the gate then spent most of its life proving that its own failures were not what they looked like.
The unit of work changed
Early on, a work item was a shape. Later, a work item was a sentence with three slots in it: which part, which property, on whose authority (a spreadsheet cell, the drawing as drawn, or a derived rule).
The gate documents were written in exactly that shape. One of them reads:
The user explicitly confirms: that R2 terminates at the outer top corner of the utility-duct body, that R3 begins at the lining offset and ends at the toe of the lower-detail trim, and that the region being filled is the lining band, not the tunnel bore.
And immediately beside it, the rule that keeps a green light from meaning anything on its own:
technicalPass=truedoes not open downstream work. Only a recorded user approval flipsdownstreamAllowedto true.
That separation — machine verdict and human approval as two different variables — is the whole reason the rest of this post is not just a list of passing tests.
The raster gate, step by step
The side-detail raster gate: source, production render, overlay and distance, in one strip, with the numeric result printed underneath.
The same four-stage gate on the barrier family, with per-check pixel figures and FAIL lines listed on the sheet rather than summarised away.
Road and slab. A wide, shallow section makes the overlay panel almost unreadable at a glance — which is exactly why the numeric distance panel exists.
The tunnel family gate.
Support: R1 cycle on the top row, grouting / forepoling on the bottom. Five families in total were gated this way.
The raster gate exists to delete the sentence "it looks right to me." It compares generated geometry against the scanned source drawing in pixel space, on terms fixed in advance.
Step 1 — keep only the ink
In the source PNG, any pixel below grayscale 180 counts as dark ink. Everything else is discarded. That is a single constant, SOURCE_DARK_THRESHOLD = 180, and it is deliberately generous: the drawings are scans of scans, and a tighter threshold would start dropping the lighter linework that carries the detail.
Step 2 — draw the generated geometry into the same frame
The generated polylines are rendered into the identical image coordinate system with a 2 px stroke (TARGET_LINE_WIDTH_PX = 2). The coordinate transform comes from fixed anchors only. It is never fitted to make the result look better — the master plan spells that out as "one transform per image, holdout residuals, no per-role fitting."
Step 3 — measure the distance field in both directions
The distance transform is a hand-rolled 4-neighbour breadth-first sweep — city-block (L1) distance, seeded from every ink pixel, initialised to height + width + 10, stored as int16. Not a library call. The reason for writing it out is that the metric's definition then lives in the repository rather than in a dependency version, and the reports declare it explicitly: "distanceMetric": "cityblock_px".
It is run twice, and the two directions ask different questions:
- Forward (generated → source): how far did my line stray from the drawing's ink?
- Reverse (source → generated): how much of the drawing's ink did my line actually cover?
Only having both makes the gate non-trivial. Forward alone rewards drawing nothing at all; reverse alone rewards drawing everywhere. Each run emits six figures per role: targetPixelCount, medianDistancePx, p95DistancePx, maxDistancePx, and coverage at 2, 4 and 8 px. Reporting three coverage radii rather than one is small and useful — a role that passes at 4 px but collapses at 2 px is a different animal from one that is tight at 2 px, and the shape of that curve is diagnostic.
Step 4 — the thresholds are locked, in writing, in the file
SOURCE_DARK_THRESHOLD = 180 TARGET_LINE_WIDTH_PX = 2 P95_LIMIT_PX = 8.0 COVERAGE_DISTANCE_PX = 4.0 COVERAGE_MIN = 0.75 SOURCE_ROLE_ROI_MARGIN_PX = 3
So: bidirectional p95 ≤ 8 px AND coverage within 4 px ≥ 75 %. Directly above those constants sits the single most useful line in the whole gate:
Locked before the first run. Do not relax these after seeing a failure without recording an engineering reason and issuing a new comparison schema version.
Thresholds that can be nudged after you see the number are not thresholds; they are decoration. Making relaxation cost a written reason and a schema version bump is what makes them real. It is also enforced mechanically rather than culturally: a guard script carries its own copy of the locked policy and compares the report's declared policy against it with math.isclose(..., abs_tol=1e-9, rel_tol=0.0), raising LOCKED_THRESHOLD_DRIFT on any divergence. A tenth of a pixel of quiet loosening fails the run.
Step 5 — whatever you calibrated on is excluded from scoring
This is the step people skip, and it is the most common way a visual-diff gate silently cheats. The utility-duct body outline was used to establish scale, so it is removed from the scored set. The code says why, in place:
Its envelope defines the four calibration anchors, so it cannot be scored as a holdout.
The exclusion happens at three different granularities, which is more thorough than I expected when I went back to read it:
- Whole-role. Each role spec carries
"holdout": True|False, and the report emitsholdoutEligibleandusedForCalibrationper role. - Pixel disk. Around each calibration anchor pixel, a disk of radius 7 or 8 px (depending on the family) is zeroed — in the source mask and the target mask both. Removing it from only one side would still let the calibration region flatter the score in the other direction.
- Blocking versus non-blocking. A role can stay measured and visible while losing the power to block, tagged
"authorityClassification": "NUMERIC_AUTHORITY_DIFF_PENDING_USER_VERDICT"— capped at one such role per side, withTOO_MANY_AUTHORITY_EXEMPT_ROLESfiring if the exemption starts being used as a general amnesty.
The contact sheet says it out loud too, printed on the image: "calibration body shown but excluded from holdout score."
Step 6 — conditions for a verdict to count at all
A pass is only structurally valid with at least 4 holdout elements, at least 3 blocking targets, and all blocking targets passing. The reason for those specific numbers is recorded in a comment rather than left as folklore:
2026-07-13 contract: r23_trim_face is excluded from holdout (its source stroke is not the physical trim face; topology gates own it), so the structural minimum is 4 holdouts with ≥ 3 blocking.
That is a threshold changing for a stated reason, with the reason attached to the number. Which is exactly what the lock comment demands, and exactly the difference between a justified relaxation and an unjustified one.
How the gate is actually built
The distance map for the left side detail: green under 2 px, amber under 8 px, red over 8. The failures are located, not just counted.
The same colouring on the tunnel arch. Most of the crown is green; the haunches and the invert are where the error concentrates.
Not one script. One shared metric kernel, five scope-specific producers, five scope-specific guards.
The kernel is the side-detail comparison module — 35,929 bytes, the largest Python file in the verification folder. It exports four functions that every other raster module imports by name: the distance transform, the metric extractor, a canonical-JSON SHA-256 helper, and a finite-point check. The other four producers hardcode the same five locked constants and add one scope-specific parameter each:
| Producer | Bytes | Extra constant |
| Side detail (kernel) | 35,929 | SOURCE_ROLE_ROI_MARGIN_PX = 3 |
| Whole tunnel section | 17,509 | SOURCE_CORRIDOR_PX = 6, CALIBRATION_EXCLUSION_RADIUS_PX = 8 |
| Support | 18,960 | SOURCE_CORRIDOR_PX = 5 |
| Road / slab | 17,491 | SOURCE_CORRIDOR_PX = 5, CALIBRATION_EXCLUSION_RADIUS_PX = 8 |
| Barrier | 15,962 | CALIBRATION_ANCHOR_EXCLUSION_RADIUS_PX = 7 |
The anti-circularity discipline is stated in each producer, in a comment, at the point where the source geometry is defined. Three variants, all saying the same thing:
These rectangles were locked from the original Excel PNG before scoring. They select source roles only; target geometry never defines or moves these ROIs.
These source polylines were read from the original Excel PNG before any target render was scored. Target geometry cannot move them.
Fixed source masks were digitized from the original Excel PNG before the target render was scored.
And the tunnel producer takes this to its literal conclusion: its source geometry is typed-in pixel coordinates. A hand-digitised polyline like [[115, 267], [93, 315], [82, 370], [84, 430], [102, 455], [125, 484]], plus arcs generated from explicit centre/radius/sweep arguments. It is crude, it does not scale, and it is the only construction that makes the comparison honest — because a source outline extracted by the same code that produces the target is not an independent comparison, it is a mirror. The master plan lists that failure explicitly among its stop conditions: "a source/target comparison reads the same derived geometry on both sides."
Finally, the verdict image itself prints the terms it was judged under, formatted directly from the constants:
locked thresholds: source dark < 180; target width 2px; holdout p95 <= 8.0px and coverage within 4.0px >= 75%
Green on pass, red on fail. It is not possible to circulate the verdict without circulating its terms.
The five families
| Family | What it checks |
| Side detail | Utility opening · drain pocket · D300 perforated pipe · slab end face · R23 trim faces |
| Full tunnel section | R1 inner face · R1 outer face (lining) · R2 left/right · R3 left/right · lower closure left/right |
| Road / slab | Road surface · pavement underside · slab underside · slab end left/right |
| Barrier | Step outline · shoulder notch · y dimension datum · z level datum · diagonal reinforcement |
| Support | Five fan zones around R1: lower-left, upper-left, crown, upper-right, lower-right, plus the crown grouting ring |
The support family carries an extra rule worth noting, because it is a conditional holdout: when forepoling is active in the payload, its crown centres must appear as a scored holdout, and ACTIVE_FOREPOLING_HOLDOUT_MISSING fires if they do not. The failure mode it prevents is subtle — a part that exists in some sections and not others quietly dropping out of the scored set on the sections where it exists.
What the gate actually caught
Here is the part that changed how I read failures. Five roles failed. When I took the numbers apart, four of them were not shape errors at all.
| Role | Reverse | Forward | Diagnosis |
| Left opening | p95 11 | p95 53 px (≈ 256 mm) | Real difference — the cell value and the drawn symbol size disagree |
| Right opening | p95 4 / coverage 97 % PASS | p95 25 px | The source ink was fully covered |
| R23 trim faces, left and right | p95 3 / coverage 100 % PASS | p95 56 / 11.2 | Source line covered perfectly. The generated face is simply longer. |
| Right slab end face | p95 2 / 100 % PASS | p95 10.95 | Same pattern |
Read the reverse column down: 11, 4, 3, 2. Read the forward column: 53, 25, 56, 10.95. Every one of the last four covered the drawing's ink essentially perfectly and was penalised anyway. The forward metric could not tell "extended" apart from "misplaced." The draughtsman drew a short stroke; my generated line ran past the end of it and took the full penalty for the overhang, even though not one pixel of source ink was missed.
This is a specific and generalisable measurement bug. A symmetric distance metric assumes the two masks are meant to have the same extent. In a comparison against a hand-drafted section they are not: a drawing shows a face with as much line as is needed to be legible, while the generated geometry shows the whole face. The mismatch is in the conventions of the two artefacts, not in the geometry.
The self-contradiction
Worse, the gate was in a state of self-contradiction, and it took a reverse review — the implementing agent auditing the reviewing agent's gate — to see it. The source line being used as the comparison target for the R23 trim faces had already been ruled invalid by another document in the same project. The review states it flatly:
The result document already ruled that this line is a source transition line unrelated to the physical trim face, and must not be used as approval evidence. The gate still uses it.
Two artefacts of the same project, disagreeing, with one of them scoring the other's work. And in the same review, the freshness problem surfaces in its most embarrassing form:
Yesterday morning's overall green only held because those latest reports were stale-green.
A green dashboard, produced by reports written before the change they claimed to validate. That is the failure that the 15-minute watchdog's freshness field existed to catch (Part 4), and here is the case where it mattered.
The fix — thresholds did not move
Three things about the measurement contract changed, and nothing about the pass bar:
- Comparison targets already ruled invalid are removed from the holdout, with the reason recorded.
- Forward measurement is clipped to the extent of the source stroke, so a longer generated line is no longer scored against empty paper.
- The openings are reclassified as
NUMERIC_AUTHORITY_DIFF: the measurement and the delta stay visible, but they no longer block. A cell-versus-symbol disagreement is a decision for a human, not a defect for a script.
That is what this gate is actually worth. Not "it failed," but "here is, in numbers, why it looked like a failure."
The gate that grades the gate
Producers write evidence. Guards read it — and refuse to believe it.
The side-detail guard does not accept the pass field a producer wrote. It recomputes every claimed pass from the four raw bidirectional metrics and raises ROLE_PASS_DOES_NOT_MATCH_LOCKED_THRESHOLD if the recomputation disagrees. It carries 24 distinct failure codes, and the interesting ones are not about geometry at all:
| Code | What it refuses |
SOURCE_TARGET_HASH_NOT_DISTINCT | The source hash, the target-branch hash and the render hash must be three different values. If any two match, you are comparing something with itself. |
SOURCE_VECTOR_TRACE_REUSED_FOR_RESIDUAL | The vector trace used to build the geometry cannot also be the thing it is scored against |
CALIBRATION_ROLE_NOT_EXCLUDED | The calibration role must be declared and excluded |
HOLDOUT_REUSED_FOR_CALIBRATION | No scored role may also be a calibration role |
CALIBRATION_ROW_SCORED_AS_HOLDOUT | The same check from the other direction |
SIDE_DETAIL_SCOPE_CLAIMS_GLOBAL_APPROVAL | A narrow pass claiming to be a broad one |
THRESHOLD_NOT_LOCKED_BEFORE_RUN | The report must declare that its policy predates the run |
STALE_PAYLOAD_VERSION | The evidence must have been produced against the live payload |
MUTATION_NOT_CAUGHT | The meta-failure — see below |
Nine of the codes above are about the integrity of the comparison rather than the correctness of the shape. That ratio is the point. Most of the ways a visual gate lies to you are structural, not numerical.
The mutation self-test
Eight scripts ship a self-test that works like this: build a hand-written clean fixture that ought to pass, then derive deliberately corrupted variants of it, each expected to trip one specific failure code. A case passes only if the expected code is actually raised. If a corruption slips through undetected, the whole guard fails with MUTATION_NOT_CAUGHT.
| Mutation case | What is corrupted | Code that must fire |
clean_side_detail_evidence | nothing — the control | (must produce no violation) |
same_source_target_hash | target hash set equal to source hash | SOURCE_TARGET_HASH_NOT_DISTINCT |
source_trace_reused | flag says the trace was reused for the residual | SOURCE_VECTOR_TRACE_REUSED_FOR_RESIDUAL |
claims_global_approval | a narrow report sets the global pass flag | SIDE_DETAIL_SCOPE_CLAIMS_GLOBAL_APPROVAL |
holdout_used_for_calibration | a role appears as both holdout and calibration | HOLDOUT_REUSED_FOR_CALIBRATION |
false_role_pass | p95 set to 20.0 while still claiming pass: true | ROLE_PASS_DOES_NOT_MATCH_LOCKED_THRESHOLD |
The clean fixture uses p95 4.0 and coverage 0.90 across five roles — comfortably inside the bar, so the control is not passing by a hair. The support guard runs nine mutations rather than six, adding a one-way-distance corruption, a stale payload version, a missing holdout, and a report claiming visual approval.
What this buys is protection against the worst outcome available to a test suite: a validator that has silently stopped validating. A guard whose parsing has drifted, or whose input shape changed, will happily report zero issues forever. Feeding it known-bad input on every run is the only way to distinguish "found nothing" from "looked at nothing."
The negative control
The parametric mutation validator has the most quietly rigorous test in the project. Its purpose, in its own header:
Prove that the geometry is actually driven by Excel numeric parameters, not by hardcoded/proxy constants. Every prior validator only re-checks already-produced numbers against the same dims that produced them, so a frozen constant that happens to equal today's Excel value would still pass. This validator instead MUTATES one input parameter, re-runs the real production geometry function, and asserts the generated geometry responded by the expected delta.
That is the direct answer to Part 2's entire subject. Eleven cases run — opening width, opening height, drain diameter, centre offset, lane width, the R2/R3 wall, the R3 start offset, corner tracking, wall flattening, lower-detail following the slab end — each bumping one parameter and requiring the corresponding geometry to move by the matching amount within 2.0 mm.
And then case six, which is the one worth copying:
NEGATIVE CONTROL. Mutating the right opening width must NOT move the left opening. Proves the test is measuring real per-parameter response, not global noise.
It bumps the right opening by +100 mm, re-measures the left opening's bounding box, and requires the change to be ≤ 1e-6. Note the asymmetry: 2.0 mm of slack for "did it move", one part in a million for "did it stay still." The tolerances differ because the expected answers differ in kind — a positive response has rounding in it, while the correct negative response is exactly zero. A suite that only ever asserts "the number changed" is satisfied by noise; pairing it with "and this other number did not change at all" is what makes the response measurement mean anything.
The validator population
Validator output on the right detail — purple opening, blue pocket, red D300, orange slab, teal trim — each classified region drawn back onto the original Excel raster.
The grouting validator: production pattern in red and blue over the source in black. Source and target are visibly the same array, which is what the check is asserting.
The verification folder holds 99 scripts, 1,361,141 bytes — 93 Python and 6 JavaScript. But they are not 99 validators. The split is architectural:
- 67
validate_*files (61 Python, 6 JavaScript) — the gates. - 32 producers — 22
build_*, 3overlay_*, 2render_*, 2audit_*, and one each ofextract_,compare_,classify_.
The producers generate evidence; the gates never trust it. That separation is the reason a producer that overstates its own result gets caught rather than believed.
Over time, suites of 43, then 44, then 60 checks were run. One rule governed all of it:
No new geometry without a paired validator. If you create a branch, you write the check for that branch in the same unit of work.
A representative sample, chosen to span the families:
| Validator | Bytes | What it proves, and its key constants |
validate_section_geometry_schema.js | 87,228 | The largest file in the folder by 2.4×. Pins 18 required dimension keys and a list of required production roles, then passes only on frames.length === 66 && bySheet.size === 33 && issues.length === 0 |
validate_parametric_mutation.py | 25,839 | The anti-frozen-constant test above. Imports the real server module and re-runs the real production functions; 11 cases plus a negative control |
validate_r123_review_layer_ui.py | 17,175 | Greps the shipped front-end for exact tokens. Requires showAudit: false — audit overlays must ship off by default — and forbids six legacy branch-name prefixes from appearing in the connection display at all |
validate_r2_100_arc_topology.py | 13,867 | The most-corrected geometry in the project. Budgets: tangent break ≤ 11.0°, guide offset ≤ 700 mm, against measured worsts of 9.49° and 619 mm — about 15 % headroom, stated in the comment |
validate_independent_support_raster_evidence.py | 13,411 | Support raster guard. Nine mutation cases; enforces the conditional forepoling holdout and a live payload version |
validate_independent_raster_evidence.py | 12,639 | The side-detail guard described above. 24 codes, 6 mutations, three-way hash distinctness |
validate_production_branch_purity.py | 13,049 | Keeps source and aggregate branches audit-only. Its same_polyline accepts either winding direction — a definition of "the same geometry" that survives a reversal |
validate_minimum_internal_clearance.py | 10,495 | The clearance envelope must be exactly one closed six-sided polygon, centred on the R centre, and must not overlap R1/R2/R3. Single tolerance 0.5 mm |
validate_excel_closure_chain.py | 7,752 | Proves the whole side-wall chain from the exported artefact: fitted curvatures equal the Excel radii (2.5 mm), tangent continuity at the R2→R3 junction (3.0°), landing on the detail corner and lower foot (2.0 mm). Radius recovered by three-point circumcircle fit. "Raw trace is never a failure condition." |
validate_excavation_payline.py | 5,402 | The excavation line must equal the shotcrete outline pushed radially outward by exactly the Excel overbreak, 1.5 mm tolerance, with the bench split present wherever the full outline exists |
validate_umbrella_fan.py | 4,014 | Every support pipe must be a 2-point finite segment whose length equals the Excel length; the branch must declare its driver cells. Prints only the first 12 issues |
Two patterns run through the whole set. First, almost none of them check a number in isolation — they check a relationship (this equals that plus the Excel value; this is tangent to that; this does not overlap that). Second, several of them check things that are not geometry at all: whether the front-end ships with audit layers off, whether a legacy branch name has leaked into a display string, whether the report is fresh.
There is one notable absence. Despite the project being named for mirroring, there is no mirror validator — "mirror" appears in the codebase mainly as an anti-mirror rule: "place the source pipe centre and pocket from this side's own slab datum; do not mirror the opposite side," with the report emitting "forbidsOppositeSideMirroring": true. The two sides of the section are genuinely different, and assuming symmetry is a defect, not a shortcut.
The defect list nobody wrote: June's report families
The clearest inventory of what was actually wrong with this model is not a defect log. It is the list of when each report family was introduced and what it failed on first. A gate is created when someone discovers a way to be wrong, so the introduction times are a chronology of discoveries, and the first failure counts are the size of each discovery.
28 June was the day this happened at scale: 498 report JSONs written, twelve new report families created in a single day.
| Time | Family introduced | First result | First failure type |
| 04:24 | parametric_section_relationships | ✗ 132 | SLAB_R23_CONTACT_DATUM_BRANCH_MISSING_LOCK_LINES |
| 08:01 | parametric_dimension_bindings | ✗ 66 | FRAME_DIMENSION_KEYS_MISSING — 8 keys |
| 08:15 | dynamo_consumer_map | ✗ 112 | FRAME_REQUIRED_BRANCH_PROFILE_EMPTY (forepoling) |
| 08:30 | dynamo_profile_payload | ✗ 330 | PROFILE_POLYLINE_TOO_SHORT — polylines of length 1 |
| 13:45 | production_profile_payload | ✗ 112 | PRODUCTION_PROFILE_EMPTY (forepoling) |
| 14:35 | excel_source_overlay_contract | ✗ 924 | MISSING_DIMENSION_DRIVER |
| 15:45 | barrier_dimensions | ✗ 1,320 | BARRIER_ACTIVE_Y_VALUE_MISMATCH |
| 16:23 | production_driver_coverage | ✗ 396 | PRODUCTION_DRIVER_KEY_MISSING |
| 20:47 | viewer_production_display_contract | ✗ 14 | FORBIDDEN_REFERENCE_BRANCH_IN_PRODUCTION_DISPLAY |
Every single one failed on its first run. That is the correct outcome — a new gate that passes immediately has either found nothing or is not looking. And most of them closed the same night: driver coverage passed at 22:22, the barrier's 1,320 issues went to zero at 22:39, dimension bindings passed at 23:20.
The next day added more, including the two that mattered most:
| Time | Family | Result | What it established |
| 00:22 | source_production_shape_fidelity | ✗ 528 | Cut lines, body and pocket match the trace exactly (delta 0.0), but the opening's source delta is 109.27 mm and the raw drain trace's is 220.19 mm |
| 00:40 | utility_control_datum_manifest | ✗ 264 | Four control points formally named and locked |
| 01:55 | production_axis_linework | ✓ | 10 axis-aligned branches, 8,712 segments |
| 02:05 | excel_source_dimension_conflicts | ✓ | PASS_DIMENSION_OVERRIDES_RAW_SOURCE |
| 03:04 | orthogonality of the road step | ✓ | No diagonal segments, 66 frames × 2 sides |
| 03:14 | slab_tail_r23_split | ✗ 132 | MISSING_SOURCE_SLAB_TAIL |
The 02:05 rule is the heaviest thing established in June, and it exists precisely because of the 00:22 measurement above it:
When a dimension traced from the embedded Excel image disagrees with the Excel numeric label, production geometry follows the Excel numeric dimension, and the raw trace delta is retained for audit and reference only.
The numbers make it concrete. Raw disagreement peaked at 109.27 mm on the opening height and 178.38 mm on the drain width. After the rule, production dimension error is 4.5e-13 mm on the opening and 0.0 mm on the drain. The disagreement did not go away — it was relocated, from being a defect to being a recorded delta. That is the same manoeuvre the raster gate's NUMERIC_AUTHORITY_DIFF class performs, arrived at independently a fortnight earlier.
Gates grow from inside
The other readable pattern: a single gate's check list grows. The parametric_section_relationships family started with 7 checks and ended June with 11. The four additions are four disciplines discovered in the two days between. A validator's item count over time is a better record of what a project learned than any of its documentation.
The order in which the tools themselves were built says the same thing in miniature: vectorise the Excel image, then prove dimensions are bound to geometry, then rebuild an individual member from its dimensions and compare, then enforce shape discipline (orthogonality, closure), and finally — last — fix what gets displayed at all. The final tool of June is not a validator. It is a display contract that names the eight things to draw and, explicitly, the things to exclude.
By 13:45 on 29 June, all 25 report families were passing. June ended entirely green. Part 6 is about what that green was worth.
What works about this
A full contract checkpoint. Left, the shape under review; right, a column of measured checks with their verdicts. The gate is a report, not a boolean.
- Verdicts reproduce. "Looks about right" becomes
p95 = 11 px, and that number is still there tomorrow. - Relaxation is expensive. The constants are locked, loosening requires a written justification and a new schema version, and a drift check fires at 1e-9.
- It cannot grade itself. Anything used for calibration is dropped from the scored set, at role level and at pixel level, on both masks.
- The producers are not trusted. Guards recompute every claimed pass from raw metrics.
- There is a tamper self-test. Deliberately wrong values are injected on every run to confirm the gate fails for the intended reason, not by luck.
- There is a negative control. Something that must not move is checked for not moving, to a thousand times tighter tolerance than anything else.
The limits — and printing them on the image
INDEPENDENT_SCOPE_MISSING_AND_MISMATCH. The limit is printed on the screen the reviewer looks at: part of the scope was never checked, and part of it disagreed.
The independent-evidence panel, one row per family, each with its own MISMATCH badge. A passing total would have hidden all five of these.
The most important limit is that any given pass covers a narrow slice. So a habit developed: the scope limit gets printed into the comparison sheet itself, along the bottom edge.
Scope limit: this proves only the side-detail raster comparison path. It does not approve the whole tunnel, road/slab, barrier/support, 3D, Dynamo, or Revit outputs.
It is not a one-off. Going back through the result documents, the same sentence appears in four distinct forms, each under its own heading, plus roughly 25 lighter rewordings scattered through the corpus:
| Heading | Wording |
## Acceptance boundary | "This scope is technically complete and ready for user visual approval. It is not a claim that the separate global independent-raster comparison scope is perfect; that existing visual gate remains independent of this result." |
## Remaining visual gate | "This correction is technically coherent but is not declared globally perfect. … the mismatch remains explicit evidence and is not waived. Checkpoint A therefore remains AWAITING_USER_VISUAL_APPROVAL, downstreamAllowed=false." |
## Remaining Independent Gate | "This correction is technically verified, but the project is not globally perfect. … Those known blockers are separate from this local correction." |
## Honest boundary | "This correction establishes that the comparison linework follows the intended Excel material edges and no longer follows dimension graphics. It does not approve the dimension-driven production solids or make the project globally perfect. … final 2D/3D approval still belongs to the user." |
And the same discipline in other artefacts: the barrier contact sheet proves the barrier detail only; the dimension-authority sheet says "This proves dimension-authority reconstruction only"; the similarity report notes that the current trace and the production branch share a lineage, so its score is for audit only and cannot prove visual approval; and the defect report states outright that "closed polygons and numeric-dimension PASS do not equal visual completion."
These sentences are not there for a careful reader of the archive. They are burned into the verdict image so that when someone crops that image into a status email, the scope travels with it. A screenshot without its scope is how a narrow pass becomes a broad claim, and it is the most common way technical work gets misreported upward.
Other limits are worth stating plainly. Pixel adjudication only means something if the source drawing is accurate, and this one is not: the original has 5.81 % anisotropy between its horizontal and vertical scales, so every raster verdict inherits an error floor it cannot see. Where a cell and a drawn symbol genuinely disagree — the openings — the gate does not decide; it hands the question to a person. And most consequentially of all: a 2D closure PASS does not guarantee a 3D loft will succeed. That one cost an entire day later, and it is the subject of the next post.
The table that was inside the workbook all along
A note on the drawing said only: refer to the standard support pattern drawing. It did not say where that drawing was. So I recorded the support spacing and inclination as "not in the source material" and used other values I happened to have.
The correction came back like this:
"There must be some spacing, some inclination information. Please actually review the reference material you were told to review."
The table was inside the Excel workbook. All of it — fifteen support Types, the authoritative version, sitting on one dedicated sheet, with rock bolts arranged as three zones by two staggered cycles.
Opening it revealed that the values I had been using were wrong:
| Sheet | Pattern Type | Longitudinal spacing in use | Actual longitudinal spacing |
| T_sec_8 | B-2 | 2.0 m | 7.0 m |
| T_sec_11 | B-1 | 2.5 m | 8.0 m |
| T_sec_5 | C-1 | 1.8 m | 6.0 m |
| T_sec_18 | E-1 | 1.5 m | 24.0 m at the crown |
The cause was simple and embarrassing: I had been using the transverse spacing as the longitudinal spacing. In places the error was a factor of 16.
The mechanism deserves a moment, because it is not simple carelessness. The manifest records how that number was obtained: active_cycle_min_positive — the smallest non-zero value in the support cycle matrix. That reduction returns a spacing. It has no way of knowing whether the smallest non-zero spacing in a matrix is the longitudinal one or the transverse one. The extraction was correct and the interpretation was wrong, and nothing downstream could tell the difference, because both quantities are a plausible number of metres.
Two failures stacked here:
- I declared Excel the primary source of truth and then never searched it exhaustively. When a value was missing, I went looking outside the workbook — the one place I had written down as authoritative.
- Some sheets were hidden. That is true, and a person would have struggled to find them too. It is still not an excuse, because I had never once opened all 39 sheets.
On the same day, a full text extraction of the drawing notes from DXF turned up two more confirmations:
- "General drilling grouting: install every 6.0 m" — exactly matching the longitudinal grouting spacing in the Excel table.
- "Rock bolts to be installed perpendicular to the excavation face" — meaning the correct bolt inclination is zero, not the value in use.
The two sources could have checked each other the whole time. Because one of them was never opened, two months of work was built on the wrong numbers — and, worse, the gates were all green throughout, because no validator in the suite checked a support spacing against an independent source. Every check compared the spacing against the same extracted value that produced it. This is the exact disease the parametric mutation validator was written to catch for dimensions, and nobody had built its equivalent for the support pattern.
"Not in the source material" is not a conclusion. It is a report of how far you searched. If you do not write down where you looked, that sentence becomes a lie told to the next person.
Not verified in this section
- The name of the eighth re-paired validator from the v9 re-contract — the source document states eight files and lists seven names. I am leaving it unconfirmed rather than inventing a plausible one.
- The script that generated the 25 px grid calibration image; only the image filename survives.
- A direct measurement of "comparison was slow inside Dynamo." The viewer's 147–155 second full regeneration time is documented; the Dynamo-side comparison time was never measured.
- Any per-instruction correction rate after the mid-July self-analysis — the transcript was never re-extracted, so the measurement simply does not exist.
- The accuracy of the Korean part names in the component dictionary; the source document marked them "presumed" and the confirmation request went unanswered.
- Whether the barrier gate's scale approximation is itself sound. It compares marker deltas using a scale derived from the barrier span over the sum of active y values. The gate passes — but that scaling assumption is not itself under test by anything.
- June's passes are JSON-based. On 28 June there were 498 report JSONs and six screen captures. A wide stretch of that month's green was never cross-checked against pixels.
What's next
The next part is what happened when all of this — green gates, locked thresholds, mutation tests and all — met a Revit kernel that had never agreed to any of it.
Tunnel automation series — nine parts.
← Previous: Human-in-the-Loop AI for BIM: Make It Ask, Not Guess (not published yet)
Next: Revit TessellatedShapeBuilder Returns a Mesh, Not a Solid (not published yet) →
Start of the series: Dynamo Built the Whole Tunnel and Never Read the Excel File
댓글
댓글 쓰기