Summary
d33cd36 (#425) rebuilt gate-25's Newman haystack from the raw bytes of each *.postman_collection.json into a newline-joined list of extracted values. That correctly stopped a Postman description from standing in for a request. It did not update is_covered, whose second Newman arm still matches JSON key syntax:
re.search(rf'"name"\s*:\s*"[^"]*\b{re.escape(method)}\b', newman)
against a haystack that is no longer JSON. The arm stopped seeing anything a collection declares; where it still fires at all it is on incidental JSON inside a captured request body.
Three defects compound, and together they produced findings no correct app change could close.
Measured — eighteen core apps, identical trees, only the package moving
fa555a2 (the commit before #420) vs a316aa5 (tip of main), driven through bin/hydra-gates --full:
|
gate-25 |
| total findings |
239 → 280 |
| apps that went PASS → FAIL |
9 |
| of the 41 new findings, previously carried by the request-NAME arm |
36 |
| of the 41, unclosable by any correct app change |
25 |
Per app (before → after): openregister 66→69 · procest 116→120 · shillinq 52→53 · launchpad 0→9 · pipelinq 0→5 · softwarecatalog 0→5 · openconnector 0→4 · decidesk 0→3 · doriath 0→3 · docudesk 0→1 · opencatalogi 0→1 · openbuild 0→1 · zaakafhandelapp 0→1.
Only 5 of the 41 are the fix working as intended: softwarecatalog's four settings#get*UserGroups, whose only mention in tests/ is a docblock route table in SettingsControllerUserGroupsConfigAuthTest.php:47, and openregister's ui#reports, "covered" by the English verb reports inside a Postman description.
The three causes
1. The request-name arm matches a syntax the extraction removed
Empirically, on docudesk's own collections:
AFTER name-arm regex fires for method 'versions'? False
BEFORE name-arm regex fires for method 'versions'? True
36 of the 41 new findings were covered by this arm and by nothing else. openconnector's requests are literally named synchronizations#test (…), mappings#test (…) — the arm was doing precise work and stopped.
2. _url_signature concatenates across placeholders, producing a path that cannot exist
parts = [seg for seg in u.split("/") if seg and "{" not in seg]
return "/".join(parts)
/api/pos-transactions/{id}/confirm -> "api/pos-transactions/confirm"
No correctly-written collection url contains that string, because a correct url has an id between those two segments. 16 endpoints were reported "missing a contract test" while the app's own collection contained a request for exactly that route, e.g.
| app |
endpoint |
the request that exists |
| docudesk |
api/templates/{id}/versions |
…/api/templates/{{templateId}}/versions |
| launchpad |
/api/dashboard/{id}/activate |
…/api/dashboard/{{fixtureDashboardId}}/activate |
| openconnector |
/api/synchronizations/{id}/test |
…/api/synchronizations/{{syncId}}/test |
| pipelinq |
/api/pos-transactions/{id}/settle |
…/api/pos-transactions/does-not-exist-00000000/settle |
| zaakafhandelapp |
/api/drc/enkelvoudiginformatieobjecten/{id}/download |
…/enkelvoudiginformatieobjecten/{{createdDocId}}/download |
(11 more: launchpad ×2, openconnector ×1, openregister ×1, pipelinq ×2, procest ×2, decidesk ×2, doriath ×1.)
It also silently pairs routes that share their literals: /api/things/{id} and /api/{kind}/things both reduce to api/things.
3. _ROUTE_ENTRY_RE cannot read a route entry containing a nested array
_ROUTE_ENTRY_RE = re.compile(r"\[(?P<body>[^\[\]]*?'name'…)\]")
A bracket pair that forbids brackets inside it. Every route declaring 'requirements' => [...] fails the entry match and falls through to the name-only sweep, which records it with an empty url — an arm nothing can satisfy. 9 endpoints across the fleet reached is_covered that way. The url was in the file the whole time:
['name' => 'page#deepLink', 'url' => '/{deepLink}', 'verb' => 'GET',
'requirements' => ['deepLink' => '(?!api(?:/|$)).+']],
Why this is a gate defect and not app debt
For the 16 false positives and the 9 url-less ones — 25 of 41 — the remaining moves were:
- write a PHPUnit test duplicating an integration test that already exists and passes, or
- add
@contract exclude <reason> to an endpoint that is contract-tested — a false exemption written to silence a false positive.
That is the unclosable-gate shape gate-59 exists to forbid (#252).
Fix
PR follows. _newman_evidence tags each extracted value with the field it hung off, so the name arm can ask its question of name: lines and the url arm of url-bearing lines; _url_signature becomes a regex with placeholders as wildcard segments; parse_routes recovers the entry by balancing brackets over a string-masked copy.
Measured 2026-08-13. Full three-column re-measurement in the PR.
Summary
d33cd36(#425) rebuilt gate-25's Newman haystack from the raw bytes of each*.postman_collection.jsoninto a newline-joined list of extracted values. That correctly stopped a Postmandescriptionfrom standing in for a request. It did not updateis_covered, whose second Newman arm still matches JSON key syntax:against a haystack that is no longer JSON. The arm stopped seeing anything a collection declares; where it still fires at all it is on incidental JSON inside a captured request body.
Three defects compound, and together they produced findings no correct app change could close.
Measured — eighteen core apps, identical trees, only the package moving
fa555a2(the commit before #420) vsa316aa5(tip ofmain), driven throughbin/hydra-gates --full:Per app (before → after): openregister 66→69 · procest 116→120 · shillinq 52→53 · launchpad 0→9 · pipelinq 0→5 · softwarecatalog 0→5 · openconnector 0→4 · decidesk 0→3 · doriath 0→3 · docudesk 0→1 · opencatalogi 0→1 · openbuild 0→1 · zaakafhandelapp 0→1.
Only 5 of the 41 are the fix working as intended: softwarecatalog's four
settings#get*UserGroups, whose only mention intests/is a docblock route table inSettingsControllerUserGroupsConfigAuthTest.php:47, and openregister'sui#reports, "covered" by the English verb reports inside a Postman description.The three causes
1. The request-name arm matches a syntax the extraction removed
Empirically, on docudesk's own collections:
36 of the 41 new findings were covered by this arm and by nothing else. openconnector's requests are literally named
synchronizations#test (…),mappings#test (…)— the arm was doing precise work and stopped.2.
_url_signatureconcatenates across placeholders, producing a path that cannot existNo correctly-written collection url contains that string, because a correct url has an id between those two segments. 16 endpoints were reported "missing a contract test" while the app's own collection contained a request for exactly that route, e.g.
api/templates/{id}/versions…/api/templates/{{templateId}}/versions/api/dashboard/{id}/activate…/api/dashboard/{{fixtureDashboardId}}/activate/api/synchronizations/{id}/test…/api/synchronizations/{{syncId}}/test/api/pos-transactions/{id}/settle…/api/pos-transactions/does-not-exist-00000000/settle/api/drc/enkelvoudiginformatieobjecten/{id}/download…/enkelvoudiginformatieobjecten/{{createdDocId}}/download(11 more: launchpad ×2, openconnector ×1, openregister ×1, pipelinq ×2, procest ×2, decidesk ×2, doriath ×1.)
It also silently pairs routes that share their literals:
/api/things/{id}and/api/{kind}/thingsboth reduce toapi/things.3.
_ROUTE_ENTRY_REcannot read a route entry containing a nested arrayA bracket pair that forbids brackets inside it. Every route declaring
'requirements' => [...]fails the entry match and falls through to the name-only sweep, which records it with an empty url — an arm nothing can satisfy. 9 endpoints across the fleet reachedis_coveredthat way. The url was in the file the whole time:Why this is a gate defect and not app debt
For the 16 false positives and the 9 url-less ones — 25 of 41 — the remaining moves were:
@contract exclude <reason>to an endpoint that is contract-tested — a false exemption written to silence a false positive.That is the unclosable-gate shape gate-59 exists to forbid (#252).
Fix
PR follows.
_newman_evidencetags each extracted value with the field it hung off, so the name arm can ask its question ofname:lines and the url arm of url-bearing lines;_url_signaturebecomes a regex with placeholders as wildcard segments;parse_routesrecovers the entry by balancing brackets over a string-masked copy.Measured 2026-08-13. Full three-column re-measurement in the PR.