Skip to content

refactor(dashboard): move the route table into ordered slices - #3160

Merged
bolichen97 merged 1 commit into
mainfrom
refactor/dashboard-route-registration
Aug 13, 2026
Merged

refactor(dashboard): move the route table into ordered slices#3160
bolichen97 merged 1 commit into
mainfrom
refactor/dashboard-route-registration

Conversation

@CrysisDeu

Copy link
Copy Markdown
Collaborator

Problem

start_dashboard carried all 447 HTTP route registrations inline: 1,889 lines of
a 4,180-line module, in which 501 of 594 statements were bare
app.router.add_* calls. Finding where a path is served meant scrolling a
676-line wall of registrations, and the function that binds the port, builds
middleware, starts services and wires shutdown hooks was mostly a route table
written longhand.

Why it matters

The registration order in that block is load-bearing and undocumented outside
comments
. aiohttp resolves a request against its routes in registration order,
397 of the 1,306 runtime routes carry a pattern segment, and several literals are
deliberately registered before a pattern that would swallow them — the inline
comments said so:

# NOTE: /search must be registered before /{key} to avoid the path being swallowed
# Static segment BEFORE the {slot} routes below … aiohttp resolves in registration order
# registered before the catch-all {name:.+} so aiohttp reaches them first

Nothing enforced any of it. A route added in the wrong place becomes silently
unreachable: the pattern matches first and its handler answers a path it was
never meant to serve, so the symptom is a wrong response body or a 404 for a path
that is still in the table.

Fix (symptom → root cause → change)

The root cause is that the table had no home of its own, so it grew inside the
startup function. It now lives in dashboard/routes/, one module per section of
the original table, each exposing register(app), invoked by register_all().

server.py drops to 3,463 lines and start_dashboard to 1,218.

The move was cheap because the block was already well-formed: the registrations
were contiguous (L2399–3074, no gap over 25 lines) and captured exactly one local,
app. The four other names they referenced are each defined and used inside the
block. Everything else resolved to a module-level handler import, so each slice
imports from the true origin and no slice imports back from server.py
there is no cycle, verified in all four import orders in separate interpreters.

Ordering is the property the change had to protect, so the slices are
contiguous cuts at the author's own section boundaries, called in the original
sequence
. Global order is then preserved by construction rather than by
per-pair reasoning. register_all(app) also sits at the same position relative to
_register_mcp_routes(app) as the old block, so paths registered in both places
(/api/notifications) keep their resolution order.

Tests

test/test_dashboard_route_table.py (new) pins what nothing checked before:

  • the registrar tuple is explicit and not alphabetical
  • the effective registration order matches it, read off the live router
  • no literal route is shadowed by an earlier matching pattern
  • no pattern is shadowed by a different earlier pattern
  • every slice exposes register, and the files on disk match the registrar list

Two details worth the reviewer's attention, both found by the tests misbehaving:

  • The tuple pin alone could not fail. A mutation that changed
    register_all's iteration to reversed(...) left the tuple untouched and every
    test green. That is why the effective-order check against the live router
    exists. Both order failure modes are now mutation-verified, and each is caught
    by a different test.
  • The shadowing guards read each resource's own compiled pattern, not a regex
    rebuilt from canonicalcanonical drops a pattern's inner regex, rendering
    /{name:manifest\.json|sw\.js|icon-\d+\.png|pcm-worklet\.js} as bare
    /{name}, which reports a four-file allowlist as a catch-all. They also use
    fullmatch, because a prefix match reports /api/channels/{id} as shadowing
    /api/channels/weixin/qr/status.

Ownership is defined as "a slice registers this path", not by the handler's
module: the table registers /api/suggestions and /api/tips/* whose handlers
live outside dashboard/, and filtering on the handler module would have
silently excluded exactly the routes the check exists for.

Four existing tests scanned server.py's source for a route line or for the
sso_login_handler seam; each now scans the slices too, so they hold wherever a
route lives.

Manual verification

The complete runtime route set is byte-identical — 1,306 entries of method,
path, resolved handler, in router order — captured by building the real app
through the suite's own _dashboard harness and diffed against origin/main.
Deliberately unsorted: a sorted snapshot would prove the same set of routes
exists while hiding a shadowing regression, which is the actual risk of moving
registrations between modules. Re-verified after the rebase and after pruning
imports, since a wrongly-removed import would break a registration.

Also verified: all four import orders (slice first, server first, package-only
then dispatch, every slice standalone); and that the 41 imports the move left dead
in server.py are dead — checked by intersection against every name any test
rebinds on the module, not by eye.

Full suite: 49,742 pass. Compared against a same-commit baseline, the delta
was 7 candidate failures; 2 were real and are fixed here (both source-text scans
of server.py), and 5 are environmental — 4 pass in isolation and the fifth is a
different test in that class asserting on inherited environment variables.

Screenshots

N/A — no user-visible surface changes. Every route resolves to the same handler as
before, in the same order.

One cost a maintainer should decide knowingly

I scanned all 195 open PRs (gh pr list defaults to 30, which would have
checked a sixth of them). 20 of them add route lines to the exact block this PR
removes.
None rewrites the file — the largest is #2713 at +54/−1 — so none can
re-home this work, and the file-overlap gate clears. But merging this will make
all 20 conflict
. Each conflict is trivial (move the added line into the right
slice) and it is a one-time cost that permanently stops the file growing, but it
is 20 branches of friction and worth timing deliberately.

Local adversarial review

Two model-pinned reviewers mirroring the CI lanes (gpt-5.6-sol /
codex-review.yml, claude-opus-4.8 / claude-review.yml + base-ref
AUTOSDE.yaml) ran before this was pushed. Both returned no blocking
findings.
Opus verified the claim rather than accepting it — reconstructed the
declared slice order as byte-for-byte the original sequence, confirmed the App
Platform tail is preserved verbatim, and re-checked all 41 import removals with
named explanations for the three greps that look like hits but are comments and a
CSP literal.

Its three advisory findings are all fixed in this commit:

  • Ownership blind spot in the shadowing guard — filtering by handler module
    excluded /api/suggestions and /api/tips/*, whose handlers live outside
    dashboard/. A real hole in a test written to prevent that class. Ownership is
    now "a slice registers this path".
  • Pattern-vs-pattern shadowing was uncovered — the third way to break
    ordering, and the likeliest future mistake. Added as its own check, flagging
    only overlaps that resolve to different handlers, since the table
    legitimately points several methods of one pattern at one handler.
  • In-function import without justificationsystem.py's
    from kiro_crew.apps.routes import register_app_routes was a verbatim move.
    Tested for a cycle in both import orders, found none, so it is hoisted to
    module scope rather than annotated.

Its fourth item is informational and accepted-and-deferred: it independently
confirmed the /api/apps/{name}/config overlap is real for the apps whose API
base is two segments deep (and that crew_companion's three-segment
/reminders/config is not affected), and judged that scoping the guard and
documenting it is the right disposition, since failing on a pre-existing defect
would block an unrelated pure refactor. It belongs in its own PR against
apps/routes.py.

@CrysisDeu
CrysisDeu requested a review from a team as a code owner August 12, 2026 23:46
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 12, 2026
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ human override accepted

Human judgment by @CrysisDeu overrides the GPT 5.6 finding for f72ff78ad51b6a405d0271b2ec002708680afc89; the recorded reason is authoritative for this commit.

This comment is updated in place on each push.

The model was not re-run because an authorized human decision supersedes it.

False positive or not applicable? A repository writer can comment:
/ai-review override gpt f72ff78ad51b6a405d0271b2ec002708680afc89: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Advisory design-level review of f72ff78ad51b6a405d0271b2ec002708680afc89 — updated in place on each push; does not block merge.

Design-Verdict: PASS

A mechanical, order-preserving extraction that turns an undocumented load-bearing invariant into an enforced one — root cause fixed, no new surface, fully reversible.

[DESIGN-REVIEWED] f72ff78

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 12, 2026
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed f72ff78ad51b6a405d0271b2ec002708680afc89 — this comment is updated in place on each push.

Review details

The candidate file contains no candidates — the discovery pass found nothing. My role is strictly a filter (Step 2: I may not add findings of my own). With no candidates to falsify, there are no survivors.

No findings.

[OPUS-REVIEWED] f72ff78

Verdict parsed from the review's SHA-scoped output markers for commit f72ff78ad51b6a405d0271b2ec002708680afc89.

False positive or not applicable? A repository writer can comment:
/ai-review override fable f72ff78ad51b6a405d0271b2ec002708680afc89: <one-sentence reason>

@CrysisDeu
CrysisDeu force-pushed the refactor/dashboard-route-registration branch from 641ae51 to 49227d9 Compare August 13, 2026 00:10
@github-actions github-actions Bot added readiness: checking Automated validation is still running merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed readiness: action required A blocking check or review needs attention labels Aug 13, 2026
start_dashboard carried all 447 route registrations inline, 1,889 lines of a
4,180-line module, of which 501 of 594 statements were bare registration calls.
They now live in dashboard/routes/, one module per section of the original
table, each exposing register(app). server.py drops to 3,463 lines and
start_dashboard to 1,218.

The registrations were already contiguous (L2399-3074, no gap over 25 lines)
and captured exactly one local, `app`; the four other names they referenced are
each defined and used within the block. Everything else resolved to a
module-level handler import, so each slice imports from the true origin and no
slice imports back from server.py -- there is no cycle.

Ordering is the property this change had to protect. aiohttp resolves a request
against its routes in REGISTRATION order, 397 of the 1,306 runtime routes carry
a pattern segment, and the table depends on that in several places: a literal
path is deliberately registered before a pattern that would swallow it, and the
inline comments said so. The slices are therefore contiguous cuts at the
author's own section boundaries, called in the original sequence, so global
order is preserved by construction rather than by per-pair reasoning.

Verified behaviourally, not structurally: the complete runtime route set --
1,306 entries of method, path, resolved handler, in router order -- is
byte-identical before and after, captured by building the real app through the
suite's own harness. The 41 imports the move left dead in server.py were
removed only after checking none is a name a test rebinds on the module.

test_dashboard_route_table.py adds the guards the split makes necessary:

- the registrar tuple is explicit and not alphabetical
- one test builds the live router ONCE and checks all three ordering properties
  against it: the EFFECTIVE registration order matches the tuple, no literal is
  shadowed by an earlier matching pattern, and no pattern is shadowed by a
  DIFFERENT earlier pattern. One build rather than three, since _dashboard runs
  a full startup per use and the router is identical in all three cases
- every slice exposes register(), and the files on disk match the registrar list

The tuple pin alone cannot fail when register_all's iteration changes, which a
mutation confirmed -- reversing the loop left the tuple untouched and every test
green until the live-router check existed. Both order failure modes are
mutation-verified. The pattern-vs-pattern case is the third way to break
ordering and the likeliest future mistake: a new /api/chat/slots/{x}/... variant
added to the wrong slice would take its sibling's traffic; it is flagged only
when the two resolve to different handlers, since the table legitimately points
several methods of one pattern at one handler.

The shadowing checks read each resource's own compiled pattern instead of
rebuilding one from `canonical`, which drops a pattern's inner regex and would
report a four-file allowlist as a catch-all, and they use fullmatch, since a
prefix match reports `/api/channels/{id}` as shadowing
`/api/channels/weixin/qr/status`. Ownership is defined as "a slice registers
this path" rather than by the handler's module, because the table registers
/api/suggestions and /api/tips/* whose handlers live outside dashboard/ -- and
filtering on the handler module would have silently excluded exactly the routes
the check exists for.

It also surfaced a pre-existing overlap this change does not introduce or fix:
the app platform's /api/apps/{name}/config is registered before the literal
/config routes of the builtin apps whose own API base is two segments deep, so
the generic handler answers instead of theirs and skips their enabled-gating.
The check is scoped to paths the dashboard table owns and the finding is called
out in its docstring rather than silently allowlisted.

Two tests scanned start_dashboard's source for routes and for the
sso_login_handler seam; both now scan the slices too, so they hold wherever a
route lives. Two more that scan for a specific route line were widened the same
way.
@CrysisDeu
CrysisDeu force-pushed the refactor/dashboard-route-registration branch from 49227d9 to f72ff78 Compare August 13, 2026 00:22
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 13, 2026
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

Rebutted — the eager import of kiro_crew.apps.routes is not introduced by this PR. It is already in dashboard.server's import graph on main, through a path this PR does not touch.

I traced the first import of kiro_crew.apps.routes in a fresh interpreter for import kiro_crew.dashboard.server, on main (6825ee17a) and on this branch. The chains are identical:

kiro_crew.dashboard.chat
  -> kiro_crew.dashboard.chat_handlers
    -> kiro_crew.dashboard.chat_orchestrator
      -> kiro_crew.dashboard.chat_runner
        -> kiro_crew.dashboard.handlers
          -> kiro_crew.dashboard.handlers.security
            -> kiro_crew.apps.routes        <-- module-level, pre-existing

dashboard/handlers/security.py imports apps.routes at module scope, and server.py has always imported handlers. So apps.routes is resolved before any route slice is reached — verified directly: importing dashboard.handlers.security first, 'kiro_crew.apps.routes' in sys.modules is already True before dashboard.routes is imported at all. The register_app_routes import in routes/system.py adds a second edge to a module the graph already contains, not a new subsystem.

Measured cost of import kiro_crew.dashboard.server, 5 runs each, same interpreter and machine:

median
main 6825ee1 836.1 ms
this branch 823.4 ms

The branch is marginally faster, i.e. within noise. There is no readiness delay to remove because there is no added import.

On the suggested fix specifically: making slice loading lazy inside register_all would put the import back inside a function, which is what this PR deliberately moved out. That import was in-function in start_dashboard before, and it was hoisted after checking for a cycle in both import orders (routes.system first and apps.routes first, in separate interpreters, both clean). Reverting it would restore an unexplained in-function import to buy an import that has already happened.

One correction to the report's premise: --slack-only does not skip this module. It sets no_dashboard (cli.py:537) inside the same gateway process, so dashboard.server is imported on that path either way — again, identically before and after this PR.

Happy to be shown otherwise if there is a path to apps.routes that this PR adds and the trace missed.

@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

/ai-review override gpt f72ff78: apps.routes is already imported at module scope by dashboard/handlers/security.py on main, so this PR adds no eager import — traced identical chains and measured 823ms vs 836ms for import dashboard.server, evidence in the preceding comment.

@github-actions

Copy link
Copy Markdown
Contributor

Human judgment recorded

@CrysisDeu marked the gpt AI finding as false positive, not applicable, or explicitly accepted for f72ff78ad51b6a405d0271b2ec002708680afc89.

apps.routes is already imported at module scope by dashboard/handlers/security.py on main, so this PR adds no eager import — traced identical chains and measured 823ms vs 836ms for import dashboard.server, evidence in the preceding comment.

This decision applies only to this commit. A new push requires a new judgment.

@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention readiness: passed Eligible automated validation passed for the current revision and removed readiness: action required A blocking check or review needs attention merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: checking Automated validation is still running labels Aug 13, 2026
@CrysisDeu

Copy link
Copy Markdown
Collaborator Author

Review record at f72ff78ad

All 59 checks pass, readiness passed, all four AI review lanes clean (GPT 5.6, Opus 4.8, Design Review, UX Review). Route table verified byte-identical against main at 6825ee17a: 1,305 routes, same method, path, handler and order.

Four things were fixed or answered along the way. Recording them so a reviewer does not have to reconstruct them from the run history.

Fixed — brand gate. A # KiroCrew Agent CRUD comment moved verbatim out of server.py:2733. Pre-existing text, but moving a line counts as adding it, so the gate correctly flagged it. Corrected to Kiro Crew.

Fixed — 15 test_mcp_core_more_coverage.py AttributeErrors. Not caused by this PR: the branch predated #3156 (94b135a0d), which repointed those patches off mcp_core onto the modules that own the symbols. Confirmed by running the file on pristine main (111 pass) versus the pre-rebase branch (15 fail). The rebase resolved all 15.

Fixed — conflict with #2810. That PR merged while this one was open and edited three lines inside the block this PR deletes: it removed /api/mcp-gateway/apps-enable and renamed servers/poolableservers/stub. Both old handlers are gone from main, so keeping this branch's version verbatim would have raised AttributeError during route registration and broken dashboard startup. The edit is mirrored into routes/agent_config.py and api_mcp_gateway_set_stub verified present at handlers/mcp.py:2253. Equivalence was re-proven against a regenerated baseline — the route count correctly moved 1,306 → 1,305.

Answered — GPT 5.6's blocking eager-import finding. Rebutted with an import trace and timings, then cleared with a human override; full evidence is in the thread above. Summary: dashboard/handlers/security.py already imports kiro_crew.apps.routes at module scope on main, so this PR adds no import that was not already in the graph.

Not fixed here, deliberately

A pre-existing flake, unrelated to this PR. test_lesson_contradiction.py::TestWriteLessonAttachesNegativeToStoredRule::test_a_sharp_s_case_variant_inserts_rather_than_enriching builds its stub as [1.0 if i == hash(t) % 384 else 0.0 ...]. Python randomizes str hashing per process, so "Straße" and "STRASSE" collide on hash(t) % 384 for about one seed in 384; colliding inputs embed identically, the second write enriches instead of inserting, and the stored-spelling assertion fails.

Reproduced on pristine main 6825ee17a: PYTHONHASHSEED=146 fails, PYTHONHASHSEED=1 passes. A seed sweep put the rate at 0.25%, matching 1/384. I reran the shard rather than patching it — a fix here would be scope creep in a pure refactor — but it will keep failing random PRs repo-wide until someone replaces the randomized hash with a stable one (zlib.crc32(t.encode())).

The /api/apps/{name}/config overlap, as stated in the description: pre-existing, unaffected by this change, belongs in its own PR against apps/routes.py.

One cost to time deliberately

The description notes 20 open PRs adding lines to the block this removes. One (#2810) has already merged and conflicted, which is the pattern: each of the remaining ~19 that lands re-conflicts this branch, and resolution is not a mechanical line move — it requires checking whether the handlers still exist. The cost accrues while this sits rather than staying fixed.

@bolichen97
bolichen97 enabled auto-merge (squash) August 13, 2026 01:01

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tier 1 auto-approve: refactor (19 files). Criteria: no conflict, no requested changes, no security surface, AI reviewers green. Category: route table reorganization — mechanical code motion, no behavior change.

@bolichen97
bolichen97 merged commit ef10fbc into main Aug 13, 2026
89 of 92 checks passed
@bolichen97
bolichen97 deleted the refactor/dashboard-route-registration branch August 13, 2026 01:01
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 13, 2026
encomjp pushed a commit to encomjp/kirocrew-customapi that referenced this pull request Aug 22, 2026
…tdev#3160)

start_dashboard carried all 447 route registrations inline, 1,889 lines of a
4,180-line module, of which 501 of 594 statements were bare registration calls.
They now live in dashboard/routes/, one module per section of the original
table, each exposing register(app). server.py drops to 3,463 lines and
start_dashboard to 1,218.

The registrations were already contiguous (L2399-3074, no gap over 25 lines)
and captured exactly one local, `app`; the four other names they referenced are
each defined and used within the block. Everything else resolved to a
module-level handler import, so each slice imports from the true origin and no
slice imports back from server.py -- there is no cycle.

Ordering is the property this change had to protect. aiohttp resolves a request
against its routes in REGISTRATION order, 397 of the 1,306 runtime routes carry
a pattern segment, and the table depends on that in several places: a literal
path is deliberately registered before a pattern that would swallow it, and the
inline comments said so. The slices are therefore contiguous cuts at the
author's own section boundaries, called in the original sequence, so global
order is preserved by construction rather than by per-pair reasoning.

Verified behaviourally, not structurally: the complete runtime route set --
1,306 entries of method, path, resolved handler, in router order -- is
byte-identical before and after, captured by building the real app through the
suite's own harness. The 41 imports the move left dead in server.py were
removed only after checking none is a name a test rebinds on the module.

test_dashboard_route_table.py adds the guards the split makes necessary:

- the registrar tuple is explicit and not alphabetical
- one test builds the live router ONCE and checks all three ordering properties
  against it: the EFFECTIVE registration order matches the tuple, no literal is
  shadowed by an earlier matching pattern, and no pattern is shadowed by a
  DIFFERENT earlier pattern. One build rather than three, since _dashboard runs
  a full startup per use and the router is identical in all three cases
- every slice exposes register(), and the files on disk match the registrar list

The tuple pin alone cannot fail when register_all's iteration changes, which a
mutation confirmed -- reversing the loop left the tuple untouched and every test
green until the live-router check existed. Both order failure modes are
mutation-verified. The pattern-vs-pattern case is the third way to break
ordering and the likeliest future mistake: a new /api/chat/slots/{x}/... variant
added to the wrong slice would take its sibling's traffic; it is flagged only
when the two resolve to different handlers, since the table legitimately points
several methods of one pattern at one handler.

The shadowing checks read each resource's own compiled pattern instead of
rebuilding one from `canonical`, which drops a pattern's inner regex and would
report a four-file allowlist as a catch-all, and they use fullmatch, since a
prefix match reports `/api/channels/{id}` as shadowing
`/api/channels/weixin/qr/status`. Ownership is defined as "a slice registers
this path" rather than by the handler's module, because the table registers
/api/suggestions and /api/tips/* whose handlers live outside dashboard/ -- and
filtering on the handler module would have silently excluded exactly the routes
the check exists for.

It also surfaced a pre-existing overlap this change does not introduce or fix:
the app platform's /api/apps/{name}/config is registered before the literal
/config routes of the builtin apps whose own API base is two segments deep, so
the generic handler answers instead of theirs and skips their enabled-gating.
The check is scoped to paths the dashboard table owns and the finding is called
out in its docstring rather than silently allowlisted.

Two tests scanned start_dashboard's source for routes and for the
sso_login_handler seam; both now scan the slices too, so they hold wherever a
route lives. Two more that scan for a specific route line were widened the same
way.

Co-authored-by: t <t@t>
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.

2 participants