Skip to content

fix(gate-19): the finding count was returned as an exit status, and a byte only holds 255 - #209

Merged
rubenvdlinde merged 1 commit into
mainfrom
fix/gate-19-exit-code-overflow
Aug 7, 2026
Merged

fix(gate-19): the finding count was returned as an exit status, and a byte only holds 255#209
rubenvdlinde merged 1 commit into
mainfrom
fix/gate-19-exit-code-overflow

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

run_gate returned count, and the bash gate reported $?. An exit status is one byte:

findings exits as gate reported
266 10 "10 scenario(s) missing @e2e"
256 0 PASS

Measured on openconnector: the helper printed "266 scenario(s) without a running e2e test" on the same run the gate summarised as 10.

Every count above 255 was under-reported by a factor nobody could see, and any multiple of 256 was a silent green — the failure mode this whole package exists to prevent.

Two halves

  • the helper clamps its status into the byte and never lets it wrap to zero while findings exist
  • the bash gate reports the number the helper printed, falling back to the status only when the summary line is missing

Tests

Two assertions, one of them the control:

  • 256 findings must exit non-zero and still say 256 in the summary
  • a clean spec must still exit 0, so the clamp cannot invent a failure

Suite: 50 passed. Full helper-suite run: 27 passed, 2 quarantined as documented, 0 failed.

… byte only holds 255

`run_gate` returned `count`, and the bash gate reported `$?`. An exit status
is one byte, so:

  266 findings  ->  exits 10   ->  "10 scenario(s) missing @e2e"
  256 findings  ->  exits 0    ->  PASS

Measured on openconnector: the helper printed "266 scenario(s) without a
running e2e test" on the same run the gate summarised as 10. Every count above
255 was under-reported by a factor nobody could see, and any multiple of 256
was a silent green — the failure mode this whole package exists to prevent.

Two halves:

  * the helper clamps its status into the byte and never lets it wrap to zero
    while findings exist;
  * the bash gate reports the number the helper PRINTED, falling back to the
    status only when the summary line is missing.

Two assertions, one of them the control: 256 findings must exit non-zero AND
still say 256 in the summary; a clean spec must still exit 0, so the clamp
cannot invent a failure.
@rubenvdlinde
rubenvdlinde merged commit 5561e68 into main Aug 7, 2026
28 checks passed
rubenvdlinde added a commit that referenced this pull request Aug 8, 2026
…ation (#200) (#215)

gate-57 reported `pipelinq LeadService::createLead` — a curated, spec'd MCP
write tool — as an orphaned write capability. Acting on that verdict would
have DELETED live code, which is the failure mode this module's own docstring
already records from hydra#106 and the reason it says false positives are the
primary risk of this gate.

`createLead` has no syntactic `->createLead(` call site and never will. It is
reached by attribute reflection (ADR-063): an `#[McpTool]` attribute on the
method, the class listed by an `IMcpScannableServices` implementation, and a
`registerServiceAlias('OCA\OpenRegister\Mcp\IMcpScannableServices::pipelinq',
PipelinqScannableServices::class)` binding in Application.php. OpenRegister's
AttributeToolScanner reflects the class and invokes the method. The caller
index cannot see any of that.

THE SEAM REQUIRES ALL THREE PIECES, mirroring what the register.d-handler and
event-listener seams already demand, and it is applied PER METHOD:

  * a bare `#[McpTool]` on a class nobody registers stays RED
  * a registered class's write methods that carry no attribute stay RED
  * an alias naming a class that does not implement the interface grants
    nothing
  * an implementation nobody aliases grants nothing

`LeadService::createInvoice` in the fixture is precisely that control: same
class, same scannable registration, no attribute, still reported.

THE gate-64 SHAPE, CHECKED AS ASKED, AND PRESENT

gate-64's `has_prelude()` grepped a quoted string literal, so it missed every
constant and matched inside comments — failing in both directions at once.
The attribute seam would have had the comment half of that for free:
pipelinq's real LeadService.php opens with

    * Both public entry points are annotated `#[McpTool]` (OpenRegister ADR-063

on line 7, seven lines above anything executable. A raw-text search matches
that sentence. So the seam reads a comment-blanked copy of the file, and the
walk-up from the method declaration only ever sees executable lines.

`_blank_php_comments()` preserves length and line numbers, is string-aware,
and knows that `#` opens a line comment in PHP while `#[` opens an ATTRIBUTE —
blanking those would delete the thing being looked for.

The two PRE-EXISTING seams had the same latent shape: a commented-out
`registerEventListener(...)` or a `<!-- <job>…</job> -->` exempts a WHOLE
class, which is a false GREEN. Both now strip comments first. MEASURED before
changing: a sweep of the eight repos under this gate found ZERO comment-only
matches, so this changes no verdict today — it closes the hole rather than
reacting to it. Both directions are tested: a commented registration must not
exempt, a live one must still exempt.

THE COUNT-AS-EXIT-STATUS BUG (#209) IS NOT PRESENT HERE

This helper prints one line per finding and always returns 0; the bash gate
counts the printed lines. There is no count anywhere near the exit status.
Asserted explicitly now so it cannot drift into one.

MEASURED, OLD vs NEW, over lib/Service/** in the eight repos at
origin/development:

  openconnector   1 ->  1      scholiq          0 -> 0
  pipelinq        5 ->  4      nldesign         0 -> 0
  shillinq       19 -> 19      softwarecatalog  0 -> 0
  launchpad       2 ->  2      portaliq         0 -> 0

Exactly one finding removed — the documented false positive — and every other
line byte-identical. The gate did not go quiet.

22 new tests alongside the suite's existing 21 (43 total, all green). Ten
mutations of the changed predicates were each killed: the seam exempting the
whole class, the seam removed, comment blanking disabled, the
implements-interface requirement dropped, the attribute boundary reverted to
the word-based form that `#[McpTool(handler: Foo::class)]` defeats, the alias
requirement dropped, the attribute searched file-wide, the interface name
blanked so any alias counts, the XML comment strip removed, and `#[` treated
as a comment.

Also records in the module docstring what the issue's second point asks for:
a zero-caller write capability has TWO remedies, and the finding text only
suggests one. softwarecatalog's `publishEntryForFederation()` had zero callers
while `PublicationService::publish()` served the same capability through a
live route — wiring the orphan would have duplicated a live endpoint and
widened the auth surface. Deletion was correct (softwarecatalog#447).

Closes #200

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde added a commit that referenced this pull request Aug 8, 2026
…, #239, #244) (#249)

Gate-19 is the highest-volume gate in the fleet. Its three open false-positive
issues were three symptoms of one decision — reading JavaScript with regular
expressions — and all three surfaced as the same sentence, "referenced only by
a test that never runs", about tests that ran and PASSED in the same CI run.

#234 A TRAILING COMMA before the closing paren. The body was located by
     stepping back from `)` over whitespace and requiring a `}`. Prettier's
     default and ESLint's `comma-dangle: always-multiline` put a `,` at
     exactly that index, so the body read as "" and the empty-body rule fired
     on a real, asserting test.

#239 A CONDITIONAL `test.skip(true, reason)` inside an `if` guard. The
     discriminator was the ARGUMENT alone, but `true` is just Playwright's
     "skip from this point" shape — the CALL SITE carries the condition. 111
     guarded call sites in the fleet against 4 genuinely unconditional ones.
     Worse, the remedy the gate prints is "replace the tag with @e2e exclude",
     so complying DELETED a true coverage claim.

#244 A TAG WRITTEN INSIDE THE `test(` ARGUMENT LIST. Tag resolution only ever
     searched FORWARD, so a tag between the open paren and the title bound to
     the NEXT test in the file. On nldesign that mis-binding then met #234 on
     whichever test it landed on, and 34 of 190 findings came out. Two
     defects, one symptom — which is why the fixture asserts the BINDING and
     not only the count.

So the file is tokenised once (comments, string contents, template contents
and regex literals blanked; string delimiters kept, because "is the first
argument a string literal" is the whole difference between `test.skip('t', fn)`
and `test.skip(cond, 'reason')`), and a real tree of test/describe calls is
built with header and body ranges. Structure questions are answered from that
tree. Everything the old regexes had earned is kept and re-asserted:
`rx.test(` is not Playwright, `latest(` merely ends in a name,
`test.describe.skip(` must match (#212 — NOT undone), `.only`/`.serial` are
not switched-off markers, and `test.beforeEach(`/`test.use(`/`test.step(`/
`test.describe.configure(` are not declarations at all.

SIGNALLING. This gate returned its finding COUNT as an exit status — a byte —
so 266 findings left as 10 and 256 would have left as 0, i.e. PASS (#209).
The clamp that fixed the wrap made the byte carry NEITHER: a 404-finding run
exited 255 while stdout said 404 (#242). The byte is now a status and nothing
else — 0 pass, 1 fail, 2 error — and the count is on stdout, where the runner
already reads it. A crash now reports SKIPPED (wiring), visible to
--require-full-coverage, instead of a fabricated verdict; the runner also
stops discarding the helper's stderr.

NOT TOUCHED: the empty-diff `_pass` branch, which is #242's subject and is
being fixed separately.

MEASURED, root-commit-scoped, across 24 local checkouts: 8790 -> 8698
findings, -92, and every one of the 92 is a false positive removed. Not one
finding was added anywhere. nldesign 190 -> 156 (exactly the 34 in #244);
decidesk 991 -> 984; procest 1181 -> 1166; softwarecatalog 312 -> 291;
openregister 799 -> 794; opencatalogi 51 -> 46; shillinq 284 -> 279.
Unchanged where the dead findings are genuine: openconnector 412 (6 real
`test.describe.skip`), openbuild 187 (36 real `test.skip('title', …)`),
larpingapp 101 (23 real `test.fixme`), scholiq 102.

PLANTED TRUE POSITIVES, against nldesign's real spec + real e2e suite after
the fix: a scenario with no test at all, a scenario tagged only by a skipped
test, and a scenario tagged only by an empty-bodied test are all still caught
(156 -> 159), while a fourth planted scenario tagged by a real test in the
nldesign trailing-comma layout is correctly not flagged.

TESTS: 75 -> 107, all green, plus the 27 other helper suites and the 59
entry-point tests. Mutation-checked: reinstating the trailing-comma bug, the
argument-only skip rule, the forward-only tag resolution, the header branch,
and the count-as-exit-status each turn the right tests red — and a mutant that
calls every ref live turns 25 tests red, which is the control that this fix
did not simply widen the gate. One earlier mutant SURVIVED (deleting the
header branch), proving that fixture could not see the branch it was meant to
cover; a describe-header case was added that kills it.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde added a commit that referenced this pull request Aug 9, 2026
…or, and gate-20 had never fired at all

Every gate in the 12-22 band was given one textbook true positive in a real
fleet repo and asked to catch it. Measured against package sha cdfbd7a, and
re-measured on top of fef032b (#272), across openregister (207 .vue, 794 spec
scenarios), pipelinq (202 .vue, type:"dashboard" pages) and shillinq (an ADR-040
AppHost adopter that keeps its own SettingsController).

Seven fired, named the plant, and returned to their exact prior verdict when it
was removed: 12, 13, 14 (both invariants), 15, 16, 18, 19, 21, 22. Four did not.

1. GATE-20 HAD NEVER FIRED. NOT RARELY - NEVER.
   Its search was `grep -nE "->${_pat//(/\\(}" "${_file}"`, and the expanded
   pattern `->findObjects\(` begins with `-`, so grep parses it as OPTIONS:
   `grep: invalid option -- '>'`, exit 2, no output. `2>/dev/null || true`
   discarded the message and the status, every file came back with zero hits,
   and the gate printed PASS. Planting `$this->objectService->findObjects(...)`
   in openregister's ActionsController: PASS.

   Repairing the grep ALONE is not the fix. The receiver test was "the FILE
   mentions ObjectService somewhere", and with the grep working that yields 14
   findings on openregister and 5 on shillinq, ALL FALSE - `createFromArray()`
   is a real method on OpenRegister's *Mappers*. The receiver is now part of the
   pattern. Fleet measurement across the three repos afterwards: ONE finding,
   and it is real - shillinq's BookingNotificationController resolves
   OCA\OpenRegister\Service\ObjectService from the container inside its
   non-admin authorisation guard and calls findObject(), which does not exist
   on it (real surface: find/findAll/saveObject/createObject/updateObject/
   deleteObject). That is a BadMethodCallException in an auth guard, shipped.

2. GATE-17 WAS WRONG ABOUT THE SAME API, IN THE OTHER DIRECTION.
   Four of the six names in OBJECT_SERVICE_CRUD - findObjects, createFromArray,
   updateFromArray, deleteFromId - do not exist on ObjectService; they are
   precisely what gate-20 flags as fabricated. findAll/createObject/
   updateObject/deleteObject were absent. And `^\s*return\s+new\s+JSONResponse`
   sits in WRAPPER_NOISE_PATTERNS, which was tested BEFORE the ObjectService
   check - so the commonest pass-through spelling of all,
   `return new JSONResponse($this->objectService->findAll([]));`, was discarded
   as "response wrapping" with the call still inside it. It did not merely miss
   the shape: RESCUE_PATTERNS' `\$this->\w+Service->\w+\(` then matched it and
   returned False. The gate rescued its own subject.

   Blast radius of both changes across openregister/pipelinq/shillinq: 0 new
   findings. The CRUD-name filter and the `@spec exclude` escape hatch are
   untouched.

3. GATE-14 NEVER JUDGED THE TEN ROUTES AppHost SUPPLIES (#265, closed).
   #223 taught invariant 1 about `Routes::standard()`. Invariant 2 asks the
   opposite question and still read route names as literals out of the leaf's
   own appinfo/routes.php, where those ten names never appear. Deleting
   `SettingsController::update()` from shillinq - which keeps its own controller,
   as aliasControllerUnlessLeafDefinesIt explicitly allows - leaves
   PUT /api/settings resolving to nothing. That is not a 404: the router
   matches, ControllerMethodReflector reflects, the request dies 500. shillinq's
   own docblock on update() spells the hazard out. Gate-14's findings log came
   back EMPTY and the gate said PASS.

4. THREE GATES REPORTED PASS OVER A CRASHED CHECKER.
   With python3 replaced by a stub that always exits 1 - so not one file was
   inspected by any python-backed gate:

     gate-12 SKIPPED (wiring)   gate-15 PASS
     gate-17 SKIPPED (wiring)   gate-16 PASS
                                gate-18 PASS
                                gate-19 FAIL - "an unreported number of scenario(s)"

   15/16/18 wrote `2>/dev/null || true` and counted lines in an empty log.
   19 read exit 1 as EXIT_FAIL from a helper that never printed its own summary,
   producing a blocking verdict with a count nobody measured. The three helpers
   now print a terminal `# count=` marker and the runner requires it; gate-18's
   helper contract is "always exit 0", so any non-zero exit is a wiring skip;
   gate-19 treats "EXIT_FAIL with no FAIL - N summary line" as wiring.
   check_spec_coverage.py and check_dashboard_antipattern.py also stop returning
   their finding COUNT as an exit status (#209).

5. GATES 12 AND 13 PASSED OVER A src/ WITH ZERO .vue (#274).
   `[ -d src ]` is not `src/ contains a Vue component`. nldesign's src/ holds one
   manifest.json, and at fef032b both gates print PASS there - the shape that
   let twelve gates certify nldesign in #225, still current, not one `rm` away.
   Now `na`, with the judgement #274 asked for stated in the reason: NcSelect /
   NcModal / NcDialog are Vue SFC components, a PHP template cannot instantiate
   one, so these two stay .vue-only and gate-40 keeps the language-agnostic
   input-label rule for templates/.

6. GATE-22's VERDICT DEPENDED ON WHERE THE GATES WERE CHECKED OUT.
   `require('ajv/dist/2020')` resolves relative to check_manifest.js, never to
   the repo under test. Run from openregister's own root with node_modules/ajv
   PRESENT in that root, the validator printed "Ajv is not resolvable from this
   process (no node_modules, no NODE_PATH)" - false - and gate-22 went FAIL;
   exporting NODE_PATH to that same directory flipped it to PASS. Same tree,
   same package, two verdicts. Resolution is now anchored on the manifest's own
   repo root, then cwd, then this package, and the degradation names every
   directory it searched.

7. GATE-15's SLOT MATCHER HAD AN ATTRIBUTE-ORDER BLIND SPOT.
   `<template #widget-x class="wide">` and `<template #widget-x='{i}'>` did not
   match - the pattern assumed the slot binding was the last thing before `>`.
   Same class as gate-44's first-attribute stop in #272.

TESTS
   test_gate_or_objectservice_surface.sh   NEW - gate-20 and gate-17, each with
                                           the silent sibling that must stay
                                           silent (a mapper's real
                                           createFromArray; a domain-named
                                           method with an identical body), plus
                                           a direct assertion that a grep
                                           pattern starting with `-` is parsed
                                           as options
   test_gate_crashed_checker_is_not_a_finding.sh
                                           all six python-backed gates under a
                                           broken interpreter, generically, so a
                                           future gate inherits the assertion
   test_gate_empty_scope_never_passes.sh   gates 12/13 on an nldesign-shaped
                                           repo, with the one-.vue control
   test_gate_route_registration.sh         new fixture routes-standard-missing-
                                           update/, differing from its sibling
                                           by exactly the deleted method
   test_check_manifest.sh                  ajv resolved from the subject's
                                           node_modules with cwd and NODE_PATH
                                           pointing elsewhere
   test_check_dashboard_antipattern.py     four slot-tag spellings that were
                                           silent misses

   Every one mutation-checked against the pre-fix tree: the crashed-checker arm
   is 7 failures red at cdfbd7a, the 12/13 arm 3 failures red at fef032b, the
   slot-tag arm 4 subtests red, and the ajv anchor red - all green after.
   50 helper suites pass, 59 bin-level assertions pass.
rubenvdlinde added a commit that referenced this pull request Aug 9, 2026
…ver a dead interpreter, and three could not see the defect they exist for (#280)

* fix(gates 45-55): eleven gates passed over an unopened scope, eight over a dead interpreter, and three could not see the defect they exist for

Every gate in this band was given ONE textbook true positive of exactly what
it exists to catch, planted in a real fleet repo, then removed again. Where a
gate could not fail, it was repaired; where it could, the plant is now a
regression test. Measured at package sha 34370f6.

## 1. All eleven reported PASS over a scope they never opened (#242/#240/#258/#268)

On a README-only diff against larpingapp, gates 45-55 printed eleven PASS
lines and the summary read "53 of 53 applicable gates ran". Not one of them
had opened a file. Gates 4/6/7/19/25/28/62/63 have answered the identical
situation with NOT APPLICABLE since #268; this band never adopted it.

Gates 47 and 48 are the sharper case: they can only answer a question about a
CHANGE SET, so on every builder full-repo run in the fleet — no base ref at
all — they printed a co-change verdict they had not formed.

## 2. Eight reported PASS over a crashed interpreter (#147/#249/#262)

A planted defect only fires when the gate runs, so no plant can see this. With
a `python3` on PATH that exits 1 on every call, on a tree carrying real
findings:

  gate-46  PASS — over the 277 unresolved @SPEC findings, across 104 distinct
           targets, it had reported one run earlier on the same files
  gate-47  PASS — on the same diff where it had just reported FAIL
  gate-45/49/50   PASS  (`2>/dev/null` discarded status and traceback)
  gate-51/54/55   PASS  (`|| true` discarded the status)
  gate-52  FAIL — "1 custom-widget finding(s)", a fabricated finding: the
           helper returned its COUNT as its exit status, the same channel
           Python uses for a traceback (#209). The count was also clamped to
           99 to fit in a byte. It now prints `findings=N` on stdout and exits
           boolean; no `findings=` line means the helper died.

gate-54 was the quietest: its advisory WARN half reads the same log, so a dead
helper silenced both halves at once.

## 3. gate-45 was the residue of #272's fix (.github#274)

#272 migrated gates 35/40/42/44 off `[ -d src ]` onto `_a11y_has_markup_dir`
and left the twelfth member of the family behind. On a templates-only app
gate-45 reported NOT APPLICABLE — "this repo ships no frontend" — over a
`<style>` block with `transition:` and no reduced-motion fallback, in the same
file gate-43 FAILED on in the same run. `na` is the one verdict that removes a
gate from coverage accounting.

The regression test was already written and gate-45 was excluded from it by
name, with a comment explaining why. Removing the name from ARM 4's skip list
in test_gate_a11y_markup_scope.sh IS the test; it fails against 34370f6.

## 4. gate-47: prose satisfied it, and a qualified attribute did not

`_ANNOTATION_RE` was an unanchored alternation of string literals, and it was
wrong in both directions from that one regex — the pairing #269 found in
gate-48 and never carried to its sibling.

  FALSE POSITIVE  rewording ONE docblock sentence that merely NAMES the
                  annotation ("becomes `@NoAdminRequired` again, paired with a
                  real ownership check") made the gate demand a test
                  co-change. A gate satisfiable by prose manufactures the
                  appearance of a security review (#191).
  FALSE NEGATIVE  `#[\OCP\AppFramework\Http\Attribute\NoAdminRequired]` was
                  invisible. A commit adding exactly that to a controller —
                  opening an admin-only endpoint to every authenticated user —
                  with no test in the diff reported PASS.

Now position-anchored, by the same rule check_csrf_removal.py already used.

## 5. gate-50: a false positive and a false negative in the same regex

  FALSE NEGATIVE  the app-id argument had to be a QUOTED STRING, so every read
                  written the fleet-standard way — `getValueString(
                  Application::APP_ID, 'listing_register', '')` — was invisible.
                  Identical code with `'larpingapp'` FAILED. Same family as
                  #184. 7 security-relevant reads across 5 repos sit behind a
                  constant today.
  FALSE POSITIVE  the empty-compare guard required a closing paren immediately
                  after the empty string, so the correct compound guard
                  `if ($reg === '' || $sch === '')` was reported as unguarded —
                  twice, on code the gate was asking for. A guard that is a
                  boolean `return` rather than an `if` was rejected too.

Both directions are now asserted, including the opencatalogi#86 shape that
mixes them: one read guarded, the next unguarded two lines later.

## 6. gate-53 did not block the PR that creates larpingapp#286

Reintroducing #286 exactly — the check-in tab deleted from src/manifest.json,
`EventRoster` left registered in src/registry.js — reported PASS. Direction 1
of the registry cross-reference stays advisory for LEGACY orphans, correctly:
the gate cannot tell "wire it" from "delete it". But when the DIFF ITSELF
removed the last reference it can, and that finding now blocks. Pre-existing
orphans are untouched (larpingapp carries one today), so this is prevention,
not a burn-down list nobody can close.

## Verified working, repaired nothing

gate-46 (dangling file, dangling fragment, valid anchor), gate-48 (short and
fully-qualified attribute removal; a comment reword correctly stays green),
gate-49, gate-51 (title, description and nested items.properties independently),
gate-52's ratchet (growth fails, shrink passes), gate-54 (flat, nested and
$ref-carrying), gate-55.

## Deliberately NOT enforced

`title == key` on a schema property is a real gate-51 defect — the renderer
uses `prop.title || key`, so the user sees the raw technical key. Measured
across 10 repos: 148 occurrences, ALL of them in softwarecatalog, where they
are VNG-standardised element names (`identifier`, `type`, `name`) that must
not be renamed. Enforcing it would produce 148 findings with no legitimate end
state in the one repo that has them. Reported rather than gated (#252).

## Divergence to reconcile

gate-45 now answers an empty in-scope set with `na`; gate-40 answers it with
PASS, by a deliberate choice in #272 that cited the invariant test this PR
reworks. The invariant now discriminates on the REASON — the applicability
table's own phrasing must not appear once its prerequisite holds — so both
behaviours are expressible. The family should pick one.

## Testing

New: hydra-gates/scripts/lib/test_gate_45_to_55_acceptance.sh — 31 arms across
six families, discovered by run-helper-suites.sh. Against the package as
merged on main it fails 20 of 31; the 11 that pass are exactly the
anti-widening and no-regression controls. Every mutation asserts its anchor is
present before it plants.

Repos used, chosen for different shapes: larpingapp (register-owning,
manifest-driven, ships registry.js), nldesign (PHP templates, no .vue, no
register), doriath (ships no phpcs SpecTagSniff — the #246 control, held at
81 findings across 46 targets before and after the plant), openconnector
(41 register files).

Full package suite: 52 discovered suites pass, 2 quarantined as documented;
60/60 entry-point invariants.

* fix(gate-50): the fail-mode window started where the call began, not where it ended

The constant-app-id fix in the parent commit made procest's config reads
visible for the first time and immediately produced 3 findings on
lib/Service/AiService.php — all three false positives, and both causes are
ordinary code the window could never have seen:

  multi-line call   PHPCS formats each read across five lines. Two of them
                    plus a blank line put the guard on the ELEVENTH line, one
                    outside a window counted from the line the match BEGAN on.
                    The guard being missed is a textbook
                    `if (empty($registerId) === true || empty($schemaId) === true)
                    { $this->logger->warning(...); return; }` (AiService.php:580, :967).

  same-line guard   `'ai_api_key_set' => ...getValueString(APP_ID, 'ai_api_key', '') !== ''`
                    handles the empty default ON the match line, and the window
                    started after it (AiService.php:710).

The window now anchors to the END of the call expression — parentheses
balanced forward from the `(` — and includes the remainder of that line. A
single-line read keeps exactly the ten lines it always had.

Caught by a before/after sweep of 12 fleet repos: 26 of 121 verdicts changed,
25 of them PASS -> NOT APPLICABLE (the truthfulness correction), and this was
the only one that changed to FAIL. procest is PASS again, correctly.

Three arms added: the multi-line shape, the same-line shape, and the reverse
control — the same multi-line shape with the guard DELETED must still FAIL, so
the window cannot have been widened until the gate finds nothing.

Also: shellcheck SC2181 in gate-45's new status check, and a file-scoped
SC2016 suppression for the acceptance suite, whose PHP fixtures are
single-quoted on purpose.
rubenvdlinde added a commit that referenced this pull request Aug 9, 2026
…or, and gate-20 had never fired at all (#277)

* fix(gates 12-22): four gates could not detect the defect they exist for, and gate-20 had never fired at all

Every gate in the 12-22 band was given one textbook true positive in a real
fleet repo and asked to catch it. Measured against package sha cdfbd7a, and
re-measured on top of fef032b (#272), across openregister (207 .vue, 794 spec
scenarios), pipelinq (202 .vue, type:"dashboard" pages) and shillinq (an ADR-040
AppHost adopter that keeps its own SettingsController).

Seven fired, named the plant, and returned to their exact prior verdict when it
was removed: 12, 13, 14 (both invariants), 15, 16, 18, 19, 21, 22. Four did not.

1. GATE-20 HAD NEVER FIRED. NOT RARELY - NEVER.
   Its search was `grep -nE "->${_pat//(/\\(}" "${_file}"`, and the expanded
   pattern `->findObjects\(` begins with `-`, so grep parses it as OPTIONS:
   `grep: invalid option -- '>'`, exit 2, no output. `2>/dev/null || true`
   discarded the message and the status, every file came back with zero hits,
   and the gate printed PASS. Planting `$this->objectService->findObjects(...)`
   in openregister's ActionsController: PASS.

   Repairing the grep ALONE is not the fix. The receiver test was "the FILE
   mentions ObjectService somewhere", and with the grep working that yields 14
   findings on openregister and 5 on shillinq, ALL FALSE - `createFromArray()`
   is a real method on OpenRegister's *Mappers*. The receiver is now part of the
   pattern. Fleet measurement across the three repos afterwards: ONE finding,
   and it is real - shillinq's BookingNotificationController resolves
   OCA\OpenRegister\Service\ObjectService from the container inside its
   non-admin authorisation guard and calls findObject(), which does not exist
   on it (real surface: find/findAll/saveObject/createObject/updateObject/
   deleteObject). That is a BadMethodCallException in an auth guard, shipped.

2. GATE-17 WAS WRONG ABOUT THE SAME API, IN THE OTHER DIRECTION.
   Four of the six names in OBJECT_SERVICE_CRUD - findObjects, createFromArray,
   updateFromArray, deleteFromId - do not exist on ObjectService; they are
   precisely what gate-20 flags as fabricated. findAll/createObject/
   updateObject/deleteObject were absent. And `^\s*return\s+new\s+JSONResponse`
   sits in WRAPPER_NOISE_PATTERNS, which was tested BEFORE the ObjectService
   check - so the commonest pass-through spelling of all,
   `return new JSONResponse($this->objectService->findAll([]));`, was discarded
   as "response wrapping" with the call still inside it. It did not merely miss
   the shape: RESCUE_PATTERNS' `\$this->\w+Service->\w+\(` then matched it and
   returned False. The gate rescued its own subject.

   Blast radius of both changes across openregister/pipelinq/shillinq: 0 new
   findings. The CRUD-name filter and the `@spec exclude` escape hatch are
   untouched.

3. GATE-14 NEVER JUDGED THE TEN ROUTES AppHost SUPPLIES (#265, closed).
   #223 taught invariant 1 about `Routes::standard()`. Invariant 2 asks the
   opposite question and still read route names as literals out of the leaf's
   own appinfo/routes.php, where those ten names never appear. Deleting
   `SettingsController::update()` from shillinq - which keeps its own controller,
   as aliasControllerUnlessLeafDefinesIt explicitly allows - leaves
   PUT /api/settings resolving to nothing. That is not a 404: the router
   matches, ControllerMethodReflector reflects, the request dies 500. shillinq's
   own docblock on update() spells the hazard out. Gate-14's findings log came
   back EMPTY and the gate said PASS.

4. THREE GATES REPORTED PASS OVER A CRASHED CHECKER.
   With python3 replaced by a stub that always exits 1 - so not one file was
   inspected by any python-backed gate:

     gate-12 SKIPPED (wiring)   gate-15 PASS
     gate-17 SKIPPED (wiring)   gate-16 PASS
                                gate-18 PASS
                                gate-19 FAIL - "an unreported number of scenario(s)"

   15/16/18 wrote `2>/dev/null || true` and counted lines in an empty log.
   19 read exit 1 as EXIT_FAIL from a helper that never printed its own summary,
   producing a blocking verdict with a count nobody measured. The three helpers
   now print a terminal `# count=` marker and the runner requires it; gate-18's
   helper contract is "always exit 0", so any non-zero exit is a wiring skip;
   gate-19 treats "EXIT_FAIL with no FAIL - N summary line" as wiring.
   check_spec_coverage.py and check_dashboard_antipattern.py also stop returning
   their finding COUNT as an exit status (#209).

5. GATES 12 AND 13 PASSED OVER A src/ WITH ZERO .vue (#274).
   `[ -d src ]` is not `src/ contains a Vue component`. nldesign's src/ holds one
   manifest.json, and at fef032b both gates print PASS there - the shape that
   let twelve gates certify nldesign in #225, still current, not one `rm` away.
   Now `na`, with the judgement #274 asked for stated in the reason: NcSelect /
   NcModal / NcDialog are Vue SFC components, a PHP template cannot instantiate
   one, so these two stay .vue-only and gate-40 keeps the language-agnostic
   input-label rule for templates/.

6. GATE-22's VERDICT DEPENDED ON WHERE THE GATES WERE CHECKED OUT.
   `require('ajv/dist/2020')` resolves relative to check_manifest.js, never to
   the repo under test. Run from openregister's own root with node_modules/ajv
   PRESENT in that root, the validator printed "Ajv is not resolvable from this
   process (no node_modules, no NODE_PATH)" - false - and gate-22 went FAIL;
   exporting NODE_PATH to that same directory flipped it to PASS. Same tree,
   same package, two verdicts. Resolution is now anchored on the manifest's own
   repo root, then cwd, then this package, and the degradation names every
   directory it searched.

7. GATE-15's SLOT MATCHER HAD AN ATTRIBUTE-ORDER BLIND SPOT.
   `<template #widget-x class="wide">` and `<template #widget-x='{i}'>` did not
   match - the pattern assumed the slot binding was the last thing before `>`.
   Same class as gate-44's first-attribute stop in #272.

TESTS
   test_gate_or_objectservice_surface.sh   NEW - gate-20 and gate-17, each with
                                           the silent sibling that must stay
                                           silent (a mapper's real
                                           createFromArray; a domain-named
                                           method with an identical body), plus
                                           a direct assertion that a grep
                                           pattern starting with `-` is parsed
                                           as options
   test_gate_crashed_checker_is_not_a_finding.sh
                                           all six python-backed gates under a
                                           broken interpreter, generically, so a
                                           future gate inherits the assertion
   test_gate_empty_scope_never_passes.sh   gates 12/13 on an nldesign-shaped
                                           repo, with the one-.vue control
   test_gate_route_registration.sh         new fixture routes-standard-missing-
                                           update/, differing from its sibling
                                           by exactly the deleted method
   test_check_manifest.sh                  ajv resolved from the subject's
                                           node_modules with cwd and NODE_PATH
                                           pointing elsewhere
   test_check_dashboard_antipattern.py     four slot-tag spellings that were
                                           silent misses

   Every one mutation-checked against the pre-fix tree: the crashed-checker arm
   is 7 failures red at cdfbd7a, the 12/13 arm 3 failures red at fef032b, the
   slot-tag arm 4 subtests red, and the ajv anchor red - all green after.
   50 helper suites pass, 59 bin-level assertions pass.

* fix(tests): the bin suite asserted that src/ existing means gates 12/13 ran

test-hydra-gates-bin.sh looped 12 and 13 into 'with src/ present, every
src-guarded gate really runs'. That fixture's src/ holds one .js file and no
.vue, so the assertion encoded the belief this PR removes: that `[ -d src ]`
is the same as 'the gate has a subject'. Both gates printed PASS there — an
empty glob under an existing src/, nldesign's exact shape.

They are now asserted separately and more strictly: not 'did not say na', but
'said na and NAMED the empty .vue glob'. PASS is called out by name as the
regression it would be. 61 bin assertions pass, 50 helper suites pass.

* fix(gates 12-22): four gates could not detect the defect they exist for, and gate-20 had never fired at all

Every gate in the 12-22 band was given one textbook true positive in a real
fleet repo and asked to catch it. Measured against package sha cdfbd7a, and
re-measured on top of fef032b (#272), across openregister (207 .vue, 794 spec
scenarios), pipelinq (202 .vue, type:"dashboard" pages) and shillinq (an ADR-040
AppHost adopter that keeps its own SettingsController).

Seven fired, named the plant, and returned to their exact prior verdict when it
was removed: 12, 13, 14 (both invariants), 15, 16, 18, 19, 21, 22. Four did not.

1. GATE-20 HAD NEVER FIRED. NOT RARELY - NEVER.
   Its search was `grep -nE "->${_pat//(/\\(}" "${_file}"`, and the expanded
   pattern `->findObjects\(` begins with `-`, so grep parses it as OPTIONS:
   `grep: invalid option -- '>'`, exit 2, no output. `2>/dev/null || true`
   discarded the message and the status, every file came back with zero hits,
   and the gate printed PASS. Planting `$this->objectService->findObjects(...)`
   in openregister's ActionsController: PASS.

   Repairing the grep ALONE is not the fix. The receiver test was "the FILE
   mentions ObjectService somewhere", and with the grep working that yields 14
   findings on openregister and 5 on shillinq, ALL FALSE - `createFromArray()`
   is a real method on OpenRegister's *Mappers*. The receiver is now part of the
   pattern. Fleet measurement across the three repos afterwards: ONE finding,
   and it is real - shillinq's BookingNotificationController resolves
   OCA\OpenRegister\Service\ObjectService from the container inside its
   non-admin authorisation guard and calls findObject(), which does not exist
   on it (real surface: find/findAll/saveObject/createObject/updateObject/
   deleteObject). That is a BadMethodCallException in an auth guard, shipped.

2. GATE-17 WAS WRONG ABOUT THE SAME API, IN THE OTHER DIRECTION.
   Four of the six names in OBJECT_SERVICE_CRUD - findObjects, createFromArray,
   updateFromArray, deleteFromId - do not exist on ObjectService; they are
   precisely what gate-20 flags as fabricated. findAll/createObject/
   updateObject/deleteObject were absent. And `^\s*return\s+new\s+JSONResponse`
   sits in WRAPPER_NOISE_PATTERNS, which was tested BEFORE the ObjectService
   check - so the commonest pass-through spelling of all,
   `return new JSONResponse($this->objectService->findAll([]));`, was discarded
   as "response wrapping" with the call still inside it. It did not merely miss
   the shape: RESCUE_PATTERNS' `\$this->\w+Service->\w+\(` then matched it and
   returned False. The gate rescued its own subject.

   Blast radius of both changes across openregister/pipelinq/shillinq: 0 new
   findings. The CRUD-name filter and the `@spec exclude` escape hatch are
   untouched.

3. GATE-14 NEVER JUDGED THE TEN ROUTES AppHost SUPPLIES (#265, closed).
   #223 taught invariant 1 about `Routes::standard()`. Invariant 2 asks the
   opposite question and still read route names as literals out of the leaf's
   own appinfo/routes.php, where those ten names never appear. Deleting
   `SettingsController::update()` from shillinq - which keeps its own controller,
   as aliasControllerUnlessLeafDefinesIt explicitly allows - leaves
   PUT /api/settings resolving to nothing. That is not a 404: the router
   matches, ControllerMethodReflector reflects, the request dies 500. shillinq's
   own docblock on update() spells the hazard out. Gate-14's findings log came
   back EMPTY and the gate said PASS.

4. THREE GATES REPORTED PASS OVER A CRASHED CHECKER.
   With python3 replaced by a stub that always exits 1 - so not one file was
   inspected by any python-backed gate:

     gate-12 SKIPPED (wiring)   gate-15 PASS
     gate-17 SKIPPED (wiring)   gate-16 PASS
                                gate-18 PASS
                                gate-19 FAIL - "an unreported number of scenario(s)"

   15/16/18 wrote `2>/dev/null || true` and counted lines in an empty log.
   19 read exit 1 as EXIT_FAIL from a helper that never printed its own summary,
   producing a blocking verdict with a count nobody measured. The three helpers
   now print a terminal `# count=` marker and the runner requires it; gate-18's
   helper contract is "always exit 0", so any non-zero exit is a wiring skip;
   gate-19 treats "EXIT_FAIL with no FAIL - N summary line" as wiring.
   check_spec_coverage.py and check_dashboard_antipattern.py also stop returning
   their finding COUNT as an exit status (#209).

5. GATES 12 AND 13 PASSED OVER A src/ WITH ZERO .vue (#274).
   `[ -d src ]` is not `src/ contains a Vue component`. nldesign's src/ holds one
   manifest.json, and at fef032b both gates print PASS there - the shape that
   let twelve gates certify nldesign in #225, still current, not one `rm` away.
   Now `na`, with the judgement #274 asked for stated in the reason: NcSelect /
   NcModal / NcDialog are Vue SFC components, a PHP template cannot instantiate
   one, so these two stay .vue-only and gate-40 keeps the language-agnostic
   input-label rule for templates/.

6. GATE-22's VERDICT DEPENDED ON WHERE THE GATES WERE CHECKED OUT.
   `require('ajv/dist/2020')` resolves relative to check_manifest.js, never to
   the repo under test. Run from openregister's own root with node_modules/ajv
   PRESENT in that root, the validator printed "Ajv is not resolvable from this
   process (no node_modules, no NODE_PATH)" - false - and gate-22 went FAIL;
   exporting NODE_PATH to that same directory flipped it to PASS. Same tree,
   same package, two verdicts. Resolution is now anchored on the manifest's own
   repo root, then cwd, then this package, and the degradation names every
   directory it searched.

7. GATE-15's SLOT MATCHER HAD AN ATTRIBUTE-ORDER BLIND SPOT.
   `<template #widget-x class="wide">` and `<template #widget-x='{i}'>` did not
   match - the pattern assumed the slot binding was the last thing before `>`.
   Same class as gate-44's first-attribute stop in #272.

TESTS
   test_gate_or_objectservice_surface.sh   NEW - gate-20 and gate-17, each with
                                           the silent sibling that must stay
                                           silent (a mapper's real
                                           createFromArray; a domain-named
                                           method with an identical body), plus
                                           a direct assertion that a grep
                                           pattern starting with `-` is parsed
                                           as options
   test_gate_crashed_checker_is_not_a_finding.sh
                                           all six python-backed gates under a
                                           broken interpreter, generically, so a
                                           future gate inherits the assertion
   test_gate_empty_scope_never_passes.sh   gates 12/13 on an nldesign-shaped
                                           repo, with the one-.vue control
   test_gate_route_registration.sh         new fixture routes-standard-missing-
                                           update/, differing from its sibling
                                           by exactly the deleted method
   test_check_manifest.sh                  ajv resolved from the subject's
                                           node_modules with cwd and NODE_PATH
                                           pointing elsewhere
   test_check_dashboard_antipattern.py     four slot-tag spellings that were
                                           silent misses

   Every one mutation-checked against the pre-fix tree: the crashed-checker arm
   is 7 failures red at cdfbd7a, the 12/13 arm 3 failures red at fef032b, the
   slot-tag arm 4 subtests red, and the ajv anchor red - all green after.
   50 helper suites pass, 59 bin-level assertions pass.

* fix(tests): the bin suite asserted that src/ existing means gates 12/13 ran

test-hydra-gates-bin.sh looped 12 and 13 into 'with src/ present, every
src-guarded gate really runs'. That fixture's src/ holds one .js file and no
.vue, so the assertion encoded the belief this PR removes: that `[ -d src ]`
is the same as 'the gate has a subject'. Both gates printed PASS there — an
empty glob under an existing src/, nldesign's exact shape.

They are now asserted separately and more strictly: not 'did not say na', but
'said na and NAMED the empty .vue glob'. PASS is called out by name as the
regression it would be. 61 bin assertions pass, 50 helper suites pass.

* fix(gate-14): a namespaced route name resolves to the app root, not to lib/Controller/

NC's RouteParser::buildControllerName() does not prefix the app namespace when
the route name already contains a backslash, so
`AppHost\Controller\GenericHealth#index` is looked up as the bare class
`AppHost\Controller\GenericHealthController`. PSR-4 maps `OCA\<App>\` onto
`lib/`, so that class lives at lib/AppHost/Controller/ — not under
lib/Controller/, which is the only place the resolver looked.

Reproduced against this package's own gates-23-33/planted fixture:

  lib/Controller/AppHost/Controller/GenericHealthController.php
    route='AppHost\Controller\GenericHealth#index'
    rule=controller-class-not-found

a path that cannot exist, reported as a missing class, INSIDE the repository
that ships the file. Same shape as the gate-30 finding in #276: the path the
gate derives is not the path the app uses.

The false FAIL was only half of it. Where a DI binding rescued the absence the
loop `continue`d, so the method-existence check never ran — measured on two new
fixtures differing by exactly one renamed method: at origin/main BOTH report
PASS, including the one whose route resolves to nothing. That is #265's defect
at a different address.

Both candidate paths are probed and the one that EXISTS wins; when neither does
the lib/Controller/ spelling is reported exactly as before, so a genuinely
missing controller reads the same as it always has.

Also: test_gate_route_registration.sh hardcoded its runner, so pointing
HYDRA_GATES_RUNNER_UNDER_TEST at a pre-fix tree silently kept running the FIXED
one and reported all-green. A mutation check that cannot fail is the same defect
as a gate that cannot fail. With the override honoured, the suite is 5 failures
red against origin/main and green after.

And test-hydra-gates-bin.sh's 'with src/ present, every src-guarded gate really
runs' loop had a stale premise: #276 gave that fixture two real .vue files
(because gates 26/31/32 now distinguish 'src/ exists' from 'src/ contains what I
read'), so gates 12 and 13 correctly run and PASS there. They go back in the
loop, and the loop now asserts its own premise first. The zero-.vue case is
asserted where its premise actually holds — the nldesign-shaped fixture in
test_gate_empty_scope_never_passes.sh.

52 helper suites pass, 60 bin assertions pass.

* fix(gate-16): deleting an @SPEC tag was the one edit the gate could not see

`_overlaps` walks FORWARD from the declaration line through the method body.
The docblock sits ABOVE the declaration, and the docblock is the only place
`@spec` can live — so the scope window never contained the tag.

Measured on a two-method fixture: delete the `@spec openspec/...` line, leave
the body byte-identical, and the helper prints `# count=0` and exits 0. Every
@SPEC tag in a repository can be stripped and gate-16 stays green. `run_gate`
also skipped any file whose `added` set was empty, and a pure deletion produces
exactly that, so the file was never even opened.

Same family as filter_preexisting_methods.py filing an auth-attribute removal as
pre-existing: a body-shaped scope cannot see a change that is not in the body.
Gate-16 does NOT route through that helper — its four call sites are gates 6, 7,
8 and 30, and check_spec_coverage.py references it zero times — it arrives at
the same blind spot by its own path.

The fix keys on 'a tag was TAKEN AWAY', not on 'a docblock was touched'. A file
that lost a tag is evaluated twice — as it is, and as it was at the base, with
the same walkers and therefore the same exemptions — and only the DIFFERENCE is
reported. So:

  delete a tag                          -> the method that lost it, and only it
  typo-fix a legacy untagged docblock   -> clean (no inherited debt surfaced)
  add a new untagged method             -> still caught, unchanged

All three asserted, the second being the anti-widening control.

---------

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
rubenvdlinde added a commit that referenced this pull request Aug 9, 2026
…l class read as absent, and a lazy closure read as an eager one

Second pass over the 56-64 band, planting in a SECOND repo of a different
shape per gate. Four more defects, all measured against gate package
48c88ba.

A CRASHED CHECKER REPORTED PASS — gates 56 and 57
--------------------------------------------------
Both invoked their helper as `>> log 2>/dev/null || true` and then derived
the verdict from `wc -l` on the log. Stderr discarded, exit status
discarded, empty log — so a checker that never started reported PASS.
Measured on shillinq with a python3 shim that exits 1 for exactly these two
helpers:

    [gate-56] register-handler-resolution: PASS      <- 153 registers
    [gate-57] orphaned-write-capability:   PASS      <- 316 services

and gate-57 had reported 20 real findings over that same tree on the
previous run. That is gate-40's defect verbatim. Both helpers ALWAYS exit 0
when they run, by design (#209 — the count goes to stdout, never into the
exit byte), so a non-zero exit can only be a crash and never a finding
count. Both now emit SKIPPED (wiring) and keep the stderr on disk.

Every gate in this band was re-checked for the count-as-exit-status defect
found in gates 19, 26 and 52: none of the nine has it. 56 and 57 put the
count on stdout and the runner counts lines; 58/59/60/61/62/63/64 return
status codes only.

A REAL CLASS READ AS ABSENT — gate 56
--------------------------------------
The PSR-4 path guess handles the conventional layout; the fallback walk
over lib/ is what finds a type living where PSR-4 does NOT predict — a
DI-bound registration, a type not named after its file. That is the shape
gate-30 was caught mis-resolving (`AppHost\Controller\GenericHealth`
PSR-4-maps to lib/Controller/AppHost/Controller/… while openregister
DI-binds it to lib/AppHost/Controller/).

The walk matched `class` only, with at most ONE modifier. So every other
declaration form was a FALSE POSITIVE on a type that genuinely exists — and
the action `guard-class-not-found` invites is to write the class a second
time. Measured, each against a real declaration at a non-conventional path:

    enum ProbeState: string { … }        -> guard-class-not-found
    interface ProbeContract { … }        -> guard-class-not-found
    trait ProbeTrait { … }               -> guard-class-not-found
    final readonly class ReadonlyProbe   -> guard-class-not-found

`final readonly` is ordinary PHP 8.2. Only the DECLARATION FORMS widen; the
line anchor that keeps docblock prose out is unchanged and asserted in both
directions.

A LAZY CLOSURE READ AS AN EAGER REFERENCE — gate 64
----------------------------------------------------
This module's header has always said lazy service closures that merely
MENTION an AppHost class are deliberately NOT flagged, because their bodies
run at resolution time. Both rules ran over the whole file, so they did not.

Measured on launchpad — deliberately a DIFFERENT repo shape from
larpingapp. launchpad's whole composition root resolves OpenRegister
lazily inside closures (it already does this for
AppHost\Observability\ManifestLoader); that is the documented leaf pattern
and the reason launchpad is green. A closure body naming Bootstrap reported
byte-identically to an eager `Bootstrap::register($context, …)`. The gate
would have failed the one repo doing it correctly, for doing it correctly,
and the only remedy is to stop writing the lazy form.

Anonymous and arrow-function bodies are now blanked before both rules.
Named methods are untouched (`public function register(` has an identifier
between `function` and `(`), and an eager reference AFTER or BETWEEN
closures is still caught — both asserted.

GATE 63 ON A CONTROLLER-ONLY DIFF — the reason was an overclaim
---------------------------------------------------------------
Every BLOCKING rule in this gate reads src/manifest.json, src/manifest.d/
or src/menu-layout.json. The two rules that touch lib/Settings are WARNs
and never fail it. So a PR that changes a settings CONTROLLER, or adds or
deletes a lib/Settings/*Admin.php section, lands in the empty-scope branch —
and the line read "this PR introduces no settings placement (ADR-079) to
judge", which on such a diff is false. The author changed the settings
surface; this gate does not adjudicate that half of it.

NOT WIDENED, on purpose. Reading the controller was tried and reverted, and
check_store_and_settings_surface.py records why: a gate that only RUNS when
a manifest changed and then judges code the PR never touched "blocked EVERY
manifest-touching PR in that repo, permanently". The verdict stays `na` —
the correct category, since no change the author could make puts a manifest
into a diff that does not touch one.

What changes is that the line now names which half it looked at, and when
the diff contains lib/Settings or a settings controller it says so
explicitly, so `na` cannot be read as a clearance for the change the author
actually made.

VERIFIED, BOTH ARMS OF THE #270 CONTRACT
-----------------------------------------
  * empty ADR-020 scope   -> NOT APPLICABLE, exit 0 under
                             --require-full-coverage (measured on the
                             controller-only fixture above)
  * genuine structural gap -> SKIPPED (structural), exit 98
                             (test_gate_empty_scope_never_passes.sh ARM 4,
                             gate-33 with --axe-enabled and no report)

And gate-60's three states, all asserted:
real finding · SKIPPED (wiring) naming the missing dependency · clean pass.

FLEET SWEEPS — 21 apps-extra repos, old helper vs new
------------------------------------------------------
  gate-56  zero verdict changes
  gate-64  zero verdict changes; the three pre-existing FAILs (openbuild,
           procest, scholiq) survive, and the three NOTEs (larpingapp,
           hermiq, nldesign) are unchanged

TESTS
-----
  test_gate_crashed_checker_is_not_a_finding.sh  +2 gates, 5 assertions.
      Two arms: the plants must FAIL with a working interpreter, and the
      SAME tree must report SKIPPED (wiring) with a dead one — otherwise
      "always skip" would pass. Against 48c88ba it reports the two PASSes.
  test_check_register_handler_resolution.py      +8 cases incl. the mutant
      that restores the pre-fix pattern and requires all four forms to go
      back to not-found.
  test_check_apphost_autoload_prelude.py         +9 cases, closure and
      anti-widening arms.

REPOS PLANTED IN, by gate
--------------------------
  56  shillinq (register-owning, 153 register.d files) + a synthetic
      DI-bound/non-conventional-path fixture
  57  shillinq (316 services) + fixture
  58  shillinq (60 e2e files); fleet-wide check that no live networkidle
      call in any repo lives outside tests/e2e/ — 193 inside, 0 outside
  59  docudesk (the repo the gate was written against) + fixture
  60  shillinq (233 manifests, fake MDI package) + fixture
  61  shillinq (15 post-event registrations)
  62  shillinq
  63  shillinq + a controller-only fixture
  64  larpingapp (eager class_exists composition root, 3 live probes)
      AND launchpad (lazy-closure composition root, 0 eager references)

NOT MINE, REPORTED NOT FIXED
-----------------------------
test_gate_45_to_55_acceptance.sh fails 4 gate-53 assertions identically on
pristine main (48c88ba) and on this branch — the suite expects blocking
behaviour for pre-existing orphans that #250/#260/#280 deliberately made
advisory. Out of this band; flagged rather than touched.
rubenvdlinde added a commit that referenced this pull request Aug 9, 2026
…could not see the defect they exist to catch (#278)

* fix(gates 56-64): six gates passed over an unopened scope, and three could not see the defect they exist to catch

Acceptance test applied to all nine gates in the 56-64 band: plant one
textbook true positive in a real fleet repo, require the gate to FAIL and
NAME it, remove the plant, require the prior verdict back, and require a
clean fixture to still pass. Five gates passed unchanged (56, 58, 60, 62,
63). Four did not.

AN UNOPENED SCOPE IS NOT A PASS — gates 56, 57, 58, 59, 60, 61
--------------------------------------------------------------
#242/#240 established that a gate must not report PASS over a scope it
never opened, and #268 that an empty ADR-020 scope is `na` rather than
`structural`. Both were applied to gates 19, 25, 62 and 63 and to nothing
else. Measured on shillinq, one docs-only commit, --scope-to-diff:

  [gate-56] register-handler-resolution: PASS   <- 153 registers, 0 opened
  [gate-57] orphaned-write-capability:   PASS   <- 316 services,  0 opened
  [gate-58] e2e-networkidle:             PASS   <-  60 e2e files, 0 opened
  [gate-59] unclosable-gate:             PASS   <- lib/ untouched
  [gate-60] icon-vocabulary:             PASS   <- no manifest in the diff
  [gate-61] listener-work-placement:     PASS   <- all 15 out of scope
  [gate-62] store-plane:                 NOT APPLICABLE   (already fixed)
  [gate-63] settings-surface:            NOT APPLICABLE   (already fixed)

Six gates asserting a verdict about code the run had not looked at, beside
two that had already learned not to — and --require-full-coverage cannot
see a PASS, so nothing reported that six gates had gone quiet. All six now
emit `na` with a reason naming the rule and the count of subjects that
exist but were not inspected.

GATE 59 COULD NOT RUN A FULL-TREE AUDIT AT ALL
----------------------------------------------
CHANGED_FILES is populated only under --scope-to-diff. Gate 59's guard read
`grep -qE '^lib/.*\.php$'` against it unconditionally, so on every unscoped
run the guard was false and the gate printed PASS having walked no PHP.
That is #240's sentence — "a full-tree audit was the one mode this gate
could never reach" — in a gate #240 did not visit. The scoping now applies
only when the caller asked for it.

Gate 61's unconditional --base is DELIBERATE by contrast (it is about new
debt; the fleet's 149-registration backlog is a work-list). Switching it to
--all was tried and reverted: the builder runs unscoped, so it would have
surfaced the whole backlog as blocking findings on every build. Recorded in
the code so the next reader does not repeat it.

GATE 59 WAS WRONG IN BOTH DIRECTIONS AT ONCE (#184's shape)
-----------------------------------------------------------
It caught the textbook case and then failed on the next four mutations,
each reproduced against docudesk:

  false GREEN     a COMMENTED-OUT setter counted as a write. `// TODO:
                  setValueString('app','configuration_version',$v)` closed
                  the finding — and that is the single most likely comment
                  to sit beside a key nobody writes. The gate whose subject
                  is "this guard never closes" was itself closed by a
                  comment promising to close a guard.
  false GREEN     `"key"` in double quotes was invisible on both sides.
  false POSITIVE  read 'key' / write "key" — code that closes its gate
                  correctly reported as never closing it, with no remedy
                  available to the app but changing its quote style.
  false GREEN     a key held in a class constant was invisible on both
                  sides — and a constant is the idiomatic way to write a key
                  used twice, which is the shape a CLOSABLE gate has.

Fixed with the established remedies: source_scope.php_mask for the comment
regions (offsets preserved; string contents kept, because the key literal
is the evidence), both quote styles, and constant resolution that keeps
read and write symmetric. The suppression keeps reading raw text because it
is authored as a comment — and it now requires the key QUOTED ON THE
MARKER'S OWN LINE, because the old four-line window spanned the read and so
suppressed whatever was near it rather than what it named.

Fleet sweep: 21 repos, zero verdict changes.

GATE 57'S MCP SEAM DISSOLVED ON A LEADING BACKSLASH
---------------------------------------------------
Both halves of the attribute seam (#200/#215) matched a namespace prefix as
`(?:[A-Za-z_]\w*\s*\\+\s*)*` — every segment had to start with a letter. So
the fully-qualified spelling, which is how you write a name with no `use`
import for it, matched neither:

    registerServiceAlias('…::app', \OCA\App\Mcp\Impl::class)
    #[\OCA\OpenRegister\Mcp\Attribute\McpTool(name: 'createLead')]

Either miss alone empties the seam and puts every #[McpTool] write method
in the app back on the finding list — which is #200 verbatim, a finding
whose only remedy is deleting a live, curated MCP write tool. Reproduced on
shillinq with the two spellings side by side.

The regression test's mutant restores BOTH pre-fix patterns together,
because the seam needs both and reverting one alone reads as "the fix
changed nothing".

Fleet sweep: 21 repos, zero verdict changes — the trap is latent, not live.

GATE 64 SAW ONE OF PHP'S THREE WAYS TO NAME A CLASS
---------------------------------------------------
Rule 2 required a QUOTED literal, so of these four only the first was a
finding; the other three were injected into larpingapp's register() and the
gate reported OK for every one:

    class_exists('OCA\OpenRegister\AppHost\…\GenericHealthController')
    class_exists(\OCA\OpenRegister\AppHost\…\GenericHealthController::class)
    use …\GenericHealthController;  class_exists(GenericHealthController::class)
    const AH = 'OCA\OpenRegister\AppHost\…';  class_exists(self::AH)

That is #184's lesson in the file where it was learned. All four spellings
now resolve through one resolver, so a spelling that escapes it escapes
both rules rather than whichever was written second. A bare `use` import
stays clean on purpose — `use` is a compile-time alias and does not
autoload; flagging it would newly redden four repos for code that works.

AND A GREEN GATE-64 WAS NOT EVIDENCE ABOUT THE WIDER PROBE
----------------------------------------------------------
The hard rule is scoped to OCA\OpenRegister\AppHost\, but the autoloader
mechanism has nothing to do with AppHost: during register() the whole
OCA\OpenRegister\ prefix is absent for any app sorting earlier, so ANY
class_exists() on it answers FALSE and everything it guards silently never
happens. Measured across apps-extra, no prelude present:

    larpingapp  3  Event\{DeepLinkRegistration,ObjectCreating,ObjectUpdating}
                   — the last two carry larpingapp's server-authoritative
                     skill-requirement / XP-budget enforcement on character
                     writes, which therefore never registers
    hermiq      3  flow-node, leaf-provider and shareable-config registration
    nldesign    1  shareable-config registration

Reported as a non-blocking NOTE, printed by the runner, and deliberately
not a FAIL: this gate is not diff-scoped, so failing it would block every
PR in three repos on code the PR did not touch — the trap
check_store_and_settings_surface.py already records. A probe in boot() is
NOT noted, because boot() runs after every app has registered.

ALSO
----
Gate 64's log path was a hardcoded /tmp/hydra-gate-apphost-autoload-prelude
.log — the shared-path non-determinism HYDRA_GATE_LOG_DIR exists to remove,
left behind in one gate.

TESTS
-----
  test_check_unclosable_gate.py                    NEW — gate 59 shipped
                                                   with no helper suite at
                                                   all; 23 cases, every arm
                                                   of the matrix above plus
                                                   the mutant that proves
                                                   the comment mask is
                                                   load-bearing
  test_gate_5661_empty_scope_is_not_a_pass.sh      NEW — 28 assertions over
                                                   gates 56-61: clean
                                                   in-scope subject PASSes,
                                                   empty scope is `na` with
                                                   a reason and exit 0 under
                                                   --require-full-coverage,
                                                   and a planted true
                                                   positive per gate still
                                                   FAILs. Against the
                                                   pre-fix package it
                                                   reports 13 failures,
                                                   naming the six PASSes
  test_check_apphost_autoload_prelude.py           +14 cases
  test_check_orphaned_write_capability.py          +5 cases
  test_check_listener_placement.py                 EXIT_EMPTY_SCOPE

51 discovered helper suites green (2 quarantined, unchanged). Full-tree run
on shillinq before/after: verdicts byte-identical for all nine gates.

* fix(gates 56-64): six gates passed over an unopened scope, and three could not see the defect they exist to catch

Acceptance test applied to all nine gates in the 56-64 band: plant one
textbook true positive in a real fleet repo, require the gate to FAIL and
NAME it, remove the plant, require the prior verdict back, and require a
clean fixture to still pass. Five gates passed unchanged (56, 58, 60, 62,
63). Four did not.

AN UNOPENED SCOPE IS NOT A PASS — gates 56, 57, 58, 59, 60, 61
--------------------------------------------------------------
#242/#240 established that a gate must not report PASS over a scope it
never opened, and #268 that an empty ADR-020 scope is `na` rather than
`structural`. Both were applied to gates 19, 25, 62 and 63 and to nothing
else. Measured on shillinq, one docs-only commit, --scope-to-diff:

  [gate-56] register-handler-resolution: PASS   <- 153 registers, 0 opened
  [gate-57] orphaned-write-capability:   PASS   <- 316 services,  0 opened
  [gate-58] e2e-networkidle:             PASS   <-  60 e2e files, 0 opened
  [gate-59] unclosable-gate:             PASS   <- lib/ untouched
  [gate-60] icon-vocabulary:             PASS   <- no manifest in the diff
  [gate-61] listener-work-placement:     PASS   <- all 15 out of scope
  [gate-62] store-plane:                 NOT APPLICABLE   (already fixed)
  [gate-63] settings-surface:            NOT APPLICABLE   (already fixed)

Six gates asserting a verdict about code the run had not looked at, beside
two that had already learned not to — and --require-full-coverage cannot
see a PASS, so nothing reported that six gates had gone quiet. All six now
emit `na` with a reason naming the rule and the count of subjects that
exist but were not inspected.

GATE 59 COULD NOT RUN A FULL-TREE AUDIT AT ALL
----------------------------------------------
CHANGED_FILES is populated only under --scope-to-diff. Gate 59's guard read
`grep -qE '^lib/.*\.php$'` against it unconditionally, so on every unscoped
run the guard was false and the gate printed PASS having walked no PHP.
That is #240's sentence — "a full-tree audit was the one mode this gate
could never reach" — in a gate #240 did not visit. The scoping now applies
only when the caller asked for it.

Gate 61's unconditional --base is DELIBERATE by contrast (it is about new
debt; the fleet's 149-registration backlog is a work-list). Switching it to
--all was tried and reverted: the builder runs unscoped, so it would have
surfaced the whole backlog as blocking findings on every build. Recorded in
the code so the next reader does not repeat it.

GATE 59 WAS WRONG IN BOTH DIRECTIONS AT ONCE (#184's shape)
-----------------------------------------------------------
It caught the textbook case and then failed on the next four mutations,
each reproduced against docudesk:

  false GREEN     a COMMENTED-OUT setter counted as a write. `// TODO:
                  setValueString('app','configuration_version',$v)` closed
                  the finding — and that is the single most likely comment
                  to sit beside a key nobody writes. The gate whose subject
                  is "this guard never closes" was itself closed by a
                  comment promising to close a guard.
  false GREEN     `"key"` in double quotes was invisible on both sides.
  false POSITIVE  read 'key' / write "key" — code that closes its gate
                  correctly reported as never closing it, with no remedy
                  available to the app but changing its quote style.
  false GREEN     a key held in a class constant was invisible on both
                  sides — and a constant is the idiomatic way to write a key
                  used twice, which is the shape a CLOSABLE gate has.

Fixed with the established remedies: source_scope.php_mask for the comment
regions (offsets preserved; string contents kept, because the key literal
is the evidence), both quote styles, and constant resolution that keeps
read and write symmetric. The suppression keeps reading raw text because it
is authored as a comment — and it now requires the key QUOTED ON THE
MARKER'S OWN LINE, because the old four-line window spanned the read and so
suppressed whatever was near it rather than what it named.

Fleet sweep: 21 repos, zero verdict changes.

GATE 57'S MCP SEAM DISSOLVED ON A LEADING BACKSLASH
---------------------------------------------------
Both halves of the attribute seam (#200/#215) matched a namespace prefix as
`(?:[A-Za-z_]\w*\s*\\+\s*)*` — every segment had to start with a letter. So
the fully-qualified spelling, which is how you write a name with no `use`
import for it, matched neither:

    registerServiceAlias('…::app', \OCA\App\Mcp\Impl::class)
    #[\OCA\OpenRegister\Mcp\Attribute\McpTool(name: 'createLead')]

Either miss alone empties the seam and puts every #[McpTool] write method
in the app back on the finding list — which is #200 verbatim, a finding
whose only remedy is deleting a live, curated MCP write tool. Reproduced on
shillinq with the two spellings side by side.

The regression test's mutant restores BOTH pre-fix patterns together,
because the seam needs both and reverting one alone reads as "the fix
changed nothing".

Fleet sweep: 21 repos, zero verdict changes — the trap is latent, not live.

GATE 64 SAW ONE OF PHP'S THREE WAYS TO NAME A CLASS
---------------------------------------------------
Rule 2 required a QUOTED literal, so of these four only the first was a
finding; the other three were injected into larpingapp's register() and the
gate reported OK for every one:

    class_exists('OCA\OpenRegister\AppHost\…\GenericHealthController')
    class_exists(\OCA\OpenRegister\AppHost\…\GenericHealthController::class)
    use …\GenericHealthController;  class_exists(GenericHealthController::class)
    const AH = 'OCA\OpenRegister\AppHost\…';  class_exists(self::AH)

That is #184's lesson in the file where it was learned. All four spellings
now resolve through one resolver, so a spelling that escapes it escapes
both rules rather than whichever was written second. A bare `use` import
stays clean on purpose — `use` is a compile-time alias and does not
autoload; flagging it would newly redden four repos for code that works.

AND A GREEN GATE-64 WAS NOT EVIDENCE ABOUT THE WIDER PROBE
----------------------------------------------------------
The hard rule is scoped to OCA\OpenRegister\AppHost\, but the autoloader
mechanism has nothing to do with AppHost: during register() the whole
OCA\OpenRegister\ prefix is absent for any app sorting earlier, so ANY
class_exists() on it answers FALSE and everything it guards silently never
happens. Measured across apps-extra, no prelude present:

    larpingapp  3  Event\{DeepLinkRegistration,ObjectCreating,ObjectUpdating}
                   — the last two carry larpingapp's server-authoritative
                     skill-requirement / XP-budget enforcement on character
                     writes, which therefore never registers
    hermiq      3  flow-node, leaf-provider and shareable-config registration
    nldesign    1  shareable-config registration

Reported as a non-blocking NOTE, printed by the runner, and deliberately
not a FAIL: this gate is not diff-scoped, so failing it would block every
PR in three repos on code the PR did not touch — the trap
check_store_and_settings_surface.py already records. A probe in boot() is
NOT noted, because boot() runs after every app has registered.

ALSO
----
Gate 64's log path was a hardcoded /tmp/hydra-gate-apphost-autoload-prelude
.log — the shared-path non-determinism HYDRA_GATE_LOG_DIR exists to remove,
left behind in one gate.

TESTS
-----
  test_check_unclosable_gate.py                    NEW — gate 59 shipped
                                                   with no helper suite at
                                                   all; 23 cases, every arm
                                                   of the matrix above plus
                                                   the mutant that proves
                                                   the comment mask is
                                                   load-bearing
  test_gate_5661_empty_scope_is_not_a_pass.sh      NEW — 28 assertions over
                                                   gates 56-61: clean
                                                   in-scope subject PASSes,
                                                   empty scope is `na` with
                                                   a reason and exit 0 under
                                                   --require-full-coverage,
                                                   and a planted true
                                                   positive per gate still
                                                   FAILs. Against the
                                                   pre-fix package it
                                                   reports 13 failures,
                                                   naming the six PASSes
  test_check_apphost_autoload_prelude.py           +14 cases
  test_check_orphaned_write_capability.py          +5 cases
  test_check_listener_placement.py                 EXIT_EMPTY_SCOPE

51 discovered helper suites green (2 quarantined, unchanged). Full-tree run
on shillinq before/after: verdicts byte-identical for all nine gates.

* fix(gates 56,57,63,64): a crashed checker read as a clean tree, a real class read as absent, and a lazy closure read as an eager one

Second pass over the 56-64 band, planting in a SECOND repo of a different
shape per gate. Four more defects, all measured against gate package
48c88ba.

A CRASHED CHECKER REPORTED PASS — gates 56 and 57
--------------------------------------------------
Both invoked their helper as `>> log 2>/dev/null || true` and then derived
the verdict from `wc -l` on the log. Stderr discarded, exit status
discarded, empty log — so a checker that never started reported PASS.
Measured on shillinq with a python3 shim that exits 1 for exactly these two
helpers:

    [gate-56] register-handler-resolution: PASS      <- 153 registers
    [gate-57] orphaned-write-capability:   PASS      <- 316 services

and gate-57 had reported 20 real findings over that same tree on the
previous run. That is gate-40's defect verbatim. Both helpers ALWAYS exit 0
when they run, by design (#209 — the count goes to stdout, never into the
exit byte), so a non-zero exit can only be a crash and never a finding
count. Both now emit SKIPPED (wiring) and keep the stderr on disk.

Every gate in this band was re-checked for the count-as-exit-status defect
found in gates 19, 26 and 52: none of the nine has it. 56 and 57 put the
count on stdout and the runner counts lines; 58/59/60/61/62/63/64 return
status codes only.

A REAL CLASS READ AS ABSENT — gate 56
--------------------------------------
The PSR-4 path guess handles the conventional layout; the fallback walk
over lib/ is what finds a type living where PSR-4 does NOT predict — a
DI-bound registration, a type not named after its file. That is the shape
gate-30 was caught mis-resolving (`AppHost\Controller\GenericHealth`
PSR-4-maps to lib/Controller/AppHost/Controller/… while openregister
DI-binds it to lib/AppHost/Controller/).

The walk matched `class` only, with at most ONE modifier. So every other
declaration form was a FALSE POSITIVE on a type that genuinely exists — and
the action `guard-class-not-found` invites is to write the class a second
time. Measured, each against a real declaration at a non-conventional path:

    enum ProbeState: string { … }        -> guard-class-not-found
    interface ProbeContract { … }        -> guard-class-not-found
    trait ProbeTrait { … }               -> guard-class-not-found
    final readonly class ReadonlyProbe   -> guard-class-not-found

`final readonly` is ordinary PHP 8.2. Only the DECLARATION FORMS widen; the
line anchor that keeps docblock prose out is unchanged and asserted in both
directions.

A LAZY CLOSURE READ AS AN EAGER REFERENCE — gate 64
----------------------------------------------------
This module's header has always said lazy service closures that merely
MENTION an AppHost class are deliberately NOT flagged, because their bodies
run at resolution time. Both rules ran over the whole file, so they did not.

Measured on launchpad — deliberately a DIFFERENT repo shape from
larpingapp. launchpad's whole composition root resolves OpenRegister
lazily inside closures (it already does this for
AppHost\Observability\ManifestLoader); that is the documented leaf pattern
and the reason launchpad is green. A closure body naming Bootstrap reported
byte-identically to an eager `Bootstrap::register($context, …)`. The gate
would have failed the one repo doing it correctly, for doing it correctly,
and the only remedy is to stop writing the lazy form.

Anonymous and arrow-function bodies are now blanked before both rules.
Named methods are untouched (`public function register(` has an identifier
between `function` and `(`), and an eager reference AFTER or BETWEEN
closures is still caught — both asserted.

GATE 63 ON A CONTROLLER-ONLY DIFF — the reason was an overclaim
---------------------------------------------------------------
Every BLOCKING rule in this gate reads src/manifest.json, src/manifest.d/
or src/menu-layout.json. The two rules that touch lib/Settings are WARNs
and never fail it. So a PR that changes a settings CONTROLLER, or adds or
deletes a lib/Settings/*Admin.php section, lands in the empty-scope branch —
and the line read "this PR introduces no settings placement (ADR-079) to
judge", which on such a diff is false. The author changed the settings
surface; this gate does not adjudicate that half of it.

NOT WIDENED, on purpose. Reading the controller was tried and reverted, and
check_store_and_settings_surface.py records why: a gate that only RUNS when
a manifest changed and then judges code the PR never touched "blocked EVERY
manifest-touching PR in that repo, permanently". The verdict stays `na` —
the correct category, since no change the author could make puts a manifest
into a diff that does not touch one.

What changes is that the line now names which half it looked at, and when
the diff contains lib/Settings or a settings controller it says so
explicitly, so `na` cannot be read as a clearance for the change the author
actually made.

VERIFIED, BOTH ARMS OF THE #270 CONTRACT
-----------------------------------------
  * empty ADR-020 scope   -> NOT APPLICABLE, exit 0 under
                             --require-full-coverage (measured on the
                             controller-only fixture above)
  * genuine structural gap -> SKIPPED (structural), exit 98
                             (test_gate_empty_scope_never_passes.sh ARM 4,
                             gate-33 with --axe-enabled and no report)

And gate-60's three states, all asserted:
real finding · SKIPPED (wiring) naming the missing dependency · clean pass.

FLEET SWEEPS — 21 apps-extra repos, old helper vs new
------------------------------------------------------
  gate-56  zero verdict changes
  gate-64  zero verdict changes; the three pre-existing FAILs (openbuild,
           procest, scholiq) survive, and the three NOTEs (larpingapp,
           hermiq, nldesign) are unchanged

TESTS
-----
  test_gate_crashed_checker_is_not_a_finding.sh  +2 gates, 5 assertions.
      Two arms: the plants must FAIL with a working interpreter, and the
      SAME tree must report SKIPPED (wiring) with a dead one — otherwise
      "always skip" would pass. Against 48c88ba it reports the two PASSes.
  test_check_register_handler_resolution.py      +8 cases incl. the mutant
      that restores the pre-fix pattern and requires all four forms to go
      back to not-found.
  test_check_apphost_autoload_prelude.py         +9 cases, closure and
      anti-widening arms.

REPOS PLANTED IN, by gate
--------------------------
  56  shillinq (register-owning, 153 register.d files) + a synthetic
      DI-bound/non-conventional-path fixture
  57  shillinq (316 services) + fixture
  58  shillinq (60 e2e files); fleet-wide check that no live networkidle
      call in any repo lives outside tests/e2e/ — 193 inside, 0 outside
  59  docudesk (the repo the gate was written against) + fixture
  60  shillinq (233 manifests, fake MDI package) + fixture
  61  shillinq (15 post-event registrations)
  62  shillinq
  63  shillinq + a controller-only fixture
  64  larpingapp (eager class_exists composition root, 3 live probes)
      AND launchpad (lazy-closure composition root, 0 eager references)

NOT MINE, REPORTED NOT FIXED
-----------------------------
test_gate_45_to_55_acceptance.sh fails 4 gate-53 assertions identically on
pristine main (48c88ba) and on this branch — the suite expects blocking
behaviour for pre-existing orphans that #250/#260/#280 deliberately made
advisory. Out of this band; flagged rather than touched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant