check_contract_coverage.py::_method_is_public_endpoint decides whether a controller method is network-facing by regex-searching a fixed 20-line window above the declaration:
def _method_is_public_endpoint(lines, decl_idx):
start = max(0, decl_idx - 20)
head = "\n".join(lines[start : decl_idx + 1])
return bool(_PUBLIC_AUTH_RE.search(head))
The window is not bounded by the previous declaration and does not distinguish an attribute from prose. Two consequences, both live on decidesk development:
1. Neighbour leak — the window reaches into the previous method
SettingsController::setPublicationConfig (declaration L203) is #[AuthorizedAdminSetting(AdminSettings::class)] — admin only. The #[NoAdminRequired] at L183 belongs to getPublicationConfig, the method above it, and L183 is exactly the first line of the 20-line window. Gate-25 reports the admin-only method as a new public endpoint.
2. Prose leak — a sentence about the attribute is read as the attribute
SettingsController::update is admin-only, and its docblock explains precisely that:
* below; it is NOT the #[NoAdminRequired] posture of the read routes.
That sentence, whose whole content is that the method is not NoAdminRequired, makes the gate classify it as public. Same family as .github#358 (gate-19 and gate-26 parse prose) — now in gate-25.
⚠️ The remedy this shape invites is the harmful one. The obvious way to silence #2 is to delete the accurate sentence — degrading documentation to dodge a regex, which is the failure mode x-external-register and x-relation-schema-field were added to gate-54 to avoid.
Direction and blast radius
Ran gate-25's own helper over all 178 public controller methods in decidesk:
- 4 methods classified public with no public marker anywhere in their own docblock or attributes (3 constructors — harmless, they are not routes — plus
setPublicationConfig, which is).
- 0 false negatives. A PHP attribute must sit adjacent to its declaration, so the window never falls short; it only over-reaches.
So this over-reports rather than opening a green hole, which is the safe direction — but it means "new public endpoint" is not reliable evidence about a method's auth posture, and an author trusting it could change an endpoint's posture to match the gate.
Suggested fix
Bound the lookback at the previous function declaration rather than at a fixed line count, and mask docblock/comment text before matching (the same _strip_ts_comments / _code_mask idea check_e2e_coverage.py already has, applied to PHP). An attribute is adjacent to its declaration by language rule, so the window can be much tighter than 20 lines.
Found while closing decidesk's four gate-25 findings; two of the four were getPublicationConfig (genuinely public) and setPublicationConfig (this false positive). Both are now covered by a real Newman contract collection regardless, so the finding was still worth acting on.
check_contract_coverage.py::_method_is_public_endpointdecides whether a controller method is network-facing by regex-searching a fixed 20-line window above the declaration:The window is not bounded by the previous declaration and does not distinguish an attribute from prose. Two consequences, both live on decidesk
development:1. Neighbour leak — the window reaches into the previous method
SettingsController::setPublicationConfig(declaration L203) is#[AuthorizedAdminSetting(AdminSettings::class)]— admin only. The#[NoAdminRequired]at L183 belongs togetPublicationConfig, the method above it, and L183 is exactly the first line of the 20-line window. Gate-25 reports the admin-only method as a new public endpoint.2. Prose leak — a sentence about the attribute is read as the attribute
SettingsController::updateis admin-only, and its docblock explains precisely that:That sentence, whose whole content is that the method is not
NoAdminRequired, makes the gate classify it as public. Same family as .github#358 (gate-19 and gate-26 parse prose) — now in gate-25.x-external-registerandx-relation-schema-fieldwere added to gate-54 to avoid.Direction and blast radius
Ran gate-25's own helper over all 178 public controller methods in decidesk:
setPublicationConfig, which is).So this over-reports rather than opening a green hole, which is the safe direction — but it means "new public endpoint" is not reliable evidence about a method's auth posture, and an author trusting it could change an endpoint's posture to match the gate.
Suggested fix
Bound the lookback at the previous
functiondeclaration rather than at a fixed line count, and mask docblock/comment text before matching (the same_strip_ts_comments/_code_maskideacheck_e2e_coverage.pyalready has, applied to PHP). An attribute is adjacent to its declaration by language rule, so the window can be much tighter than 20 lines.Found while closing decidesk's four gate-25 findings; two of the four were
getPublicationConfig(genuinely public) andsetPublicationConfig(this false positive). Both are now covered by a real Newman contract collection regardless, so the finding was still worth acting on.