Skip to content

gate-25: the request-NAME arm matches a JSON syntax the extraction removed — 41 new findings, 25 of them unclosable #430

Description

@rubenvdlinde

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:

  1. write a PHPUnit test duplicating an integration test that already exists and passes, or
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions