From 573b764bbb271c5ff0951d41757bab57774d6ed5 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Thu, 17 Sep 2026 14:04:39 +0200 Subject: [PATCH] fix(ranker): scope a region to the file that defines its handler (M1b 12.4) A route-registration module registers handlers that other modules define. app/routes/index.js requires seven handler modules and defines none, yet regions there were scoped to handleContributionsUpdate and its siblings. The graph rightly held no such method, so every question in those regions was unresolvable: 138 across six families on that one repository. The entry record still names the handler. The authorization question is which middleware guards which handler, and that is cross-file by nature. It now also records whether the file defines what it names, and a region built from an entry that does not is scoped to the file instead of to a method the file lacks. The handler's own module still gets its function-scoped region, so nothing is lost. My first attempt changed the mapper to stop naming cross-file handlers at all. An existing control caught it by asserting that a multi-line registration names the handler its middleware guards, which is exactly the attribution the authorization family needs. The evidence was against me and the fix belonged one layer up, in region construction. 1,407 tests passed, nine skipped. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo --- .kiro/specs/pre-push-safety-net/tasks.md | 8 +++++++ src/openultrasast/mapping.py | 15 ++++++++++++- src/openultrasast/model/regions.py | 7 ++++++ tests/test_model_regions.py | 28 ++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/.kiro/specs/pre-push-safety-net/tasks.md b/.kiro/specs/pre-push-safety-net/tasks.md index 9bbc788..4bfc1f7 100644 --- a/.kiro/specs/pre-push-safety-net/tasks.md +++ b/.kiro/specs/pre-push-safety-net/tasks.md @@ -432,6 +432,14 @@ Preserve all completed task evidence below. - _Requirements: 2.1, 2.2, 6.3, 8.1_ - _Verified 2026-09-17:_ 1,406 tests passed, nine skipped; Ruff/format/mypy passed. The cause is recorded rather than guessed: `app/routes/index.js` requires seven handler modules and defines none of them, so a question naming `handleContributionsUpdate` there names a function that file genuinely does not hold and the empty scope is correct. Taint and configuration now separate the two causes, reporting `context_function_unresolved` with the name when a question names a function the file does not define, and `context_scope_empty` only when the file has no methods at all. The driver no longer adds its generic empty-scope gap on top of a query that already explained itself, and the new gap is question-owned so it cannot block another question's comparison. Completion is unchanged by design: a question whose function the graph does not hold stays unresolved, and nothing completes by widening its scope to the whole file. +- [x] 12.4 Attribute a region to the file that defines its handler + - **The region-attribution half of 12.3.** A route-registration module registers handlers other modules define. `app/routes/index.js` requires seven handler modules and defines none, yet regions there were scoped to `handleContributionsUpdate` and its siblings, so the graph rightly held no such method and every question in those regions was unresolvable: 138 across six families. + - The entry record still names the handler, because the authorization question is which middleware guards which handler and that is cross-file by nature. It now also records whether the file defines what it names, and a region built from an entry that does not is scoped to the file rather than to a method the file lacks. + - _Verified 2026-09-17:_ 1,407 tests passed, nine skipped; Ruff/format/mypy passed. A first attempt changed the mapper to stop naming cross-file handlers and was reverted: an existing control proved the name carries the authorization attribution, so the fix belongs in region construction, not in the mapper. Controls assert the registering file asks about itself with no function-scoped region, while the defining module keeps its function-scoped region. + - _Boundary: Ranker Scope Contract, contributor-scan entry mapping_ + - _Depends: 12.3_ + - _Requirements: 2.1, 2.2, 8.1_ + ## Implementation Notes - Task5.2 measured PMPro timing out after both snapshots were materialized but before change context/graph analysis. Group6 must account for compatible snapshot/discovery reuse as well as graph reuse; identical-tip hits cannot substitute for the later representative changed-code latency gate. No new detector, scope heuristic or incremental CPG mutation is implied. diff --git a/src/openultrasast/mapping.py b/src/openultrasast/mapping.py index 0a27307..d9bd4e9 100644 --- a/src/openultrasast/mapping.py +++ b/src/openultrasast/mapping.py @@ -74,6 +74,15 @@ class EntryPointRecord: conditions: list[str] provenance: str rationale: str + # Does the file this record belongs to actually DEFINE the handler it names? + # + # A route-registration module registers handlers that live elsewhere: NodeGoat's `index.js` + # requires seven handler modules and defines none. The record still names the handler, because + # the authorization question is about which middleware guards which handler and that is + # inherently cross-file. But a REGION scoped to a function its file does not hold is a question + # the graph cannot answer, and 138 questions across six families were unresolvable for exactly + # that reason on one repository. The region builder uses this to scope such an entry to the file. + defines_handler: bool = True # Did the access level come from a DECLARATION, or from the absence of one? # # The distinction decides whether "public" can be trusted. An OpenAPI operation with no `security` block @@ -424,18 +433,20 @@ def _js_entry_points(target: FileTarget, text: str) -> list[EntryPointRecord]: handler, middleware = _js_registration(stripped, bounds) access, evidence = _js_route_access(middleware, stripped) start, end = bounds.get(handler or "", (number, number)) + named = handler or _js_handler_name(stripped) records.append( _entry( target, start, end, - handler or _js_handler_name(stripped), + named, "http_handler", "route", access, "http_request", evidence, _line_conditions(stripped), + defines_handler=bool(named) and named in bounds, ) ) if "process.argv" in stripped: @@ -762,6 +773,7 @@ def _entry( access_evidence: list[str], conditions: list[str], access_declared: bool = False, + defines_handler: bool = True, ) -> EntryPointRecord: return EntryPointRecord( path=target.path, @@ -773,6 +785,7 @@ def _entry( access_level=access_level, trust_boundary=trust_boundary, access_evidence=access_evidence, + defines_handler=defines_handler, conditions=conditions, provenance=f"entrypoint:{kind}:{access_level}", rationale=f"{kind} surface classified as {access_level} at {trust_boundary}", diff --git a/src/openultrasast/model/regions.py b/src/openultrasast/model/regions.py index 59bbbc0..d420c29 100644 --- a/src/openultrasast/model/regions.py +++ b/src/openultrasast/model/regions.py @@ -99,6 +99,13 @@ def regions_for(entries: Sequence[object], targets: Sequence[object], *, shipped if not families: continue function = getattr(entry, "function_name", None) or None + if function is not None and not getattr(entry, "defines_handler", True): + # The entry names a handler this file registers but does not define, which is what a + # route-registration module does. Scoping a region to it asks the graph about a method + # the file does not hold, and every question in that region is then unresolvable. The + # handler's own module gets its own regions, so the route is scoped to the file here and + # the record keeps the name for the authorization question, which is cross-file by nature. + function = None module_body = function in _MODULE_MARKERS if module_body: # A module body is not a handler, whatever the mapper called it, so it is offered the families a diff --git a/tests/test_model_regions.py b/tests/test_model_regions.py index ddfe0c5..57a957c 100644 --- a/tests/test_model_regions.py +++ b/tests/test_model_regions.py @@ -272,3 +272,31 @@ def test_an_inferred_public_endpoint_keeps_the_obligation() -> None: handler = next(r for r in regions if r.function == "handler") assert "access_control" in handler.families + + +def test_a_route_registered_but_not_defined_here_is_scoped_to_its_file(): + """12.4: a registration module names handlers other modules define; the graph holds none of them.""" + from types import SimpleNamespace + + from openultrasast.model.regions import regions_for + + target = SimpleNamespace(path="app/routes/index.js", language="javascript") + registered = SimpleNamespace( + path="app/routes/index.js", + function_name="handleContributionsUpdate", + access_level="public", + defines_handler=False, + ) + defined = SimpleNamespace( + path="app/routes/contributions.js", + function_name="handleContributionsUpdate", + access_level="public", + defines_handler=True, + ) + other = SimpleNamespace(path="app/routes/contributions.js", language="javascript") + scoped = {(region.path, region.function) for region in regions_for([registered, defined], [target, other])} + # The registering file asks about itself, not about a method it does not hold. + assert ("app/routes/index.js", None) in scoped + assert not any(path == "app/routes/index.js" and function for path, function in scoped) + # The module that defines it still gets the function-scoped region. + assert ("app/routes/contributions.js", "handleContributionsUpdate") in scoped