Flask monolith refactor
Skill prithvi020397/agent-refactor-skills/skills/flask-monolith-refactor
Community agent skills for safe, behavior-preserving refactoring (OpenCode, Claude Code, and any SKILL.md-compatible agent)
npx -y skills add prithvi020397/agent-refactor-skills --skill flask-monolith-refactorAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 18 days oldThe repository was created 18 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use this skill whenever the user asks to refactor, split, restructure, or clean up a single-file Flask (or similar WSGI) backend — one large app.py with many @app.route definitions, module-level state, and no blueprints or package structure. Covers adding failure observability first, removing duplicate routes, characterization tests, extracting pure helpers and services, and converting routes to blueprints — all WITHOUT changing behavior, response shapes, or route contracts, and without touching the frontend.
SKILL.md
10.9 KB, as published. Nobody here has run it
Flask Monolith Refactor
Safely restructure a single-file Flask backend (one giant app.py) into
core/ + services/ + routes/ blueprints, keeping the app fully runnable
and deployable after every phase. Structure + observability only — never a
behavior change.
Scope
- Backend only. NEVER touch frontend assets, templates' markup, or static files.
- Keep every JSON response shape byte-compatible. Clients depend on them; "cleaner" response formats are a behavior change, not a refactor.
- No new third-party dependencies without explicit user approval. Logging
uses stdlib (
logging,logging.handlers) only. - Env-var-driven modes (feature flags, auth toggles, legacy modes) keep identical semantics. If the app runs single-worker because module-level state is not concurrency-safe, do NOT raise the worker count.
- Persistence changes (JSON files → DB) are OUT of scope unless explicitly requested, and only after the structure phases are done.
Non-Negotiable Rules
1. Read the whole file first
Read all of app.py before editing anything. Map: routes, helpers, shared
module-level state, and which functions each route calls. Fragile fixes live
in monoliths (truncation repair, schema-agnostic helpers, retry wrappers) —
identify them BEFORE moving code and preserve them intact. Ask the user
which functions are known-fragile if the history isn't visible.
2. Observability before restructuring
except Exception as e: return jsonify({"error": str(e)}) with no logging
means production failures are invisible — and invisible failures make a
refactor unverifiable. Phase 0 is always logging, never code movement.
3. One phase = one commit = green tests
Run the full test suite after every phase. Do not proceed on red. Never push without explicit user approval.
4. Characterize before you move
No function moves files until a test pins its current behavior — including its weird cases (both input schemas, malformed JSON, boundary values). The tests must pass unchanged before AND after the move.
5. Moves are verbatim
Extraction = cut, paste, import. Zero logic edits in a move commit. Any "while I'm here" improvement goes in its own later commit, flagged to the user first. If a behavior change is unavoidable, STOP and flag it before doing it.
Migration Order
Copy CHECKLIST.md from this skill folder into the target repo and log each phase there.
-
Phase 0 — Logging / failure observability (DO FIRST).
- One module logger with
RotatingFileHandler(size-capped + backups) plus stderr. Format includes timestamp,%(levelname)s %(name)s %(module)s:%(lineno)d. - Every
exceptblock callslog.exception("context: <what failed>")BEFORE returning the error response. Prioritize routes that call LLMs, parsers, external APIs, and auth. - Convert raw
print()debug dumps tolog.debug(...). - Info-level entry logging on the riskiest routes: method, path, provider/model, duration.
- Verify: trigger a real error → full traceback in log file + stderr;
rotation works at the size cap;
import apploads clean.
- One module logger with
-
Phase 1 — Kill duplicate routes. Flask silently keeps only one handler per rule; find duplicates (
grep -n '@app.route' app.py | sortby path), determine which is live, delete the dead one. Add a test asserting each rule maps to exactly one endpoint. -
Phase 2 — Characterization tests (no refactor). Pin current behavior of every pure helper that will move: schema-handling functions with ALL schemas they accept, scoring/banding boundaries, scheduling predicates, JSON-repair functions with truncated input. These tests must stay green through every later phase.
-
Phase 3 — Extract pure helpers into
core/. Group by domain (e.g.core/questions.py,core/schedule.py,core/concepts.py). Pure functions only — no Flask imports, no module state. Verbatim moves, covered entirely by Phase 2 tests. -
Phase 4 — Extract services into
services/. Side-effectful collaborators: LLM client + prompt builders (keep existing timeout/retry values), grading/judging, code execution, document extraction + fallback chains, persistence (save functions, atomic-write helper, the module-level dicts — a singleStoreobject is acceptable if it changes nothing observable). -
Phase 5 — Blueprints in
routes/. Group routes by domain (auth, practice, documents, analytics, pages, ...).app.pybecomes a thin entrypoint: app factory + blueprint registration + logging setup. URL rules must be identical — no url_prefix that changes any path.Moving route functions: include the preceding
@app.route(...)/@app.<decorator>lines in the moved block, then rewrite them to@bp.route(...). A function-extraction that starts atdefleaves the decorator orphaned inapp.py→SyntaxError(a bare decorator followed by module-level code) and silently drops the route in the blueprint. Capture from the first decorator abovedefto the line before the nextdef/@app./ top-level CONSTANT assignment.Lazy namespace injection (CRITICAL). A route function that moved to
routes/X.pyno longer has the old module-levelfrom app import ...symbols (request,jsonify,g,session, the shared dicts, helper funcs). Each moved route function MUST get the full app namespace at request time, with zero call-site edits:def some_route(): import app as _app # lazy: entire app namespace (request-time) globals().update({k: v for k, v in vars(_app).items() if not k.startswith("__")}) # ... original body unchanged ...Why this exact form (hard-won):
from app import *is illegal inside a function — Python raisesSyntaxError: import * only allowed at module level. So you cannot use it.- A static
from app import NAME, NAME, ...list is fragile: it fails the moment a name is referenced but only re-exported fromapp(e.g. pulled intoapp.pyviafrom core.constants import *), givingImportError: cannot import name 'CAPABILITY_CONCEPT_KEYWORDS'. The wholevars(_app)form imports re-exports too, so it just works. - Inject at the top of each function body (after the signature + decorators),
not at module level — module-level
import appinroutes/X.pywould circular-import (app imports the blueprint at its end). Request-time import is safe becauseappis fully initialized by then. - This keeps every move a pure verbatim relocate (Rule 5): no per-function import surgery, no risk of missing a symbol.
-
Phase 6 — OPTIONAL (explicit user opt-in required). Persistence upgrade (files → DB) to lift the single-worker cap. Default: STOP before this phase.
Risks — surface these to the user BEFORE starting
- Invisible failures — swallowed exceptions mean you can't tell a refactor regression from a pre-existing bug. Mitigation: Phase 0 first; record pre-existing errors as a baseline.
- Import-time side effects — module-level code (client init, file loads, dict population) runs on import; splitting files reorders it. Mitigation: keep initialization explicit in the entrypoint/factory.
- Circular imports — routes ↔ services ↔ state. Mitigation: strict
dependency direction
routes → services → core; state lives in one module. - Shared mutable state by reference —
from app import PROGRESSin a new module can rebind instead of share, or duplicate the dict. Mitigation: import the module (or Store), never the bare dict. - Fragile fixes silently dropped — hand-tuned constants (max_tokens, timeouts, retry counts) and repair functions look like cruft and get "cleaned up". Mitigation: Rule 1 inventory + Phase 2 tests.
Verification (after EVERY phase)
- Full test suite green (
python -m pytest). - App boots; hit 2–3 representative endpoints with curl and diff the JSON against a pre-refactor capture.
- Route inventory unchanged: dump
app.url_mapbefore Phase 0 and diff it after every phase (same rules, same methods, same endpoints count). - Grep for leftover top-level executable statements in extracted modules — extracted files should only define.
KPI / Results (measure on every real refactor)
Track these so the refactored app is provably equivalent to the monolith.
| KPI | How to measure | Pass criterion |
|---|---|---|
| Route contract unchanged | Diff app.url_map dump (rules+methods) against Phase-0 baseline | 100% identical rule count + paths + methods |
| Response shape unchanged | curl 2–3 endpoints, diff JSON vs pre-refactor capture | byte-identical body |
| Behavior unchanged | Full characterization + feature test suite | green before AND after every phase |
| Fragile fixes preserved | Grep each fragile fix from the Rule-1 inventory in its new home | all present, unedited |
| Import-time side effects intact | import app boots; no init errors | boots clean |
| Test count | pytest after each phase | no net loss |
Measured on The Loop (pawscode) — real run
- Before:
app.py= 5,719 lines, single file, no package structure. - After (Phases 0–5):
app.py= 1,278 lines (thin entrypoint);core/(3 files: constants, questions, concepts),services/(5 files: llm, extraction, execution, grading, persistence),routes/(6 blueprints: pages, auth, documents, analytics, practice, interview). - Routes: 71
@app.routefunctions → blueprints; url_map 72 rules, identical before/after (one is the static favicon/after-request pair). - Tests: 63 passing after every phase (19 new characterization tests in
Phase 2 pinning
topic_forboth schemas,_repair_truncated_json5 cases,hire_verdictbands,is_solved/is_due/schedule_review). - Fragile fixes preserved:
topic_for()schema-agnostic (classicprompt- v2
persona/triggers/rubric);run_judgemax_tokens=4096+_repair_truncated_json()depth-aware brace rebalance; gunicornworkers 1; removed a duplicate/api/transcribe(Whisper dup was serving in prod and breaking transcription — a live bug surfaced by Phase 1).
- v2
- Live smoke test (LEGACY_MODE):
/api/questions,/api/start,/api/streak→ all HTTP 200; log shows method/path/status/ms per request. - Time: Phases 0–5 completed in one session, every phase a green-commit, zero pushes until explicit approval (push gate honored).
Gives 1 of the 12 instructions most refactoring skills give
Counted across 521 of the 525 authors here whose files we hold, read 2026-08-06
- run tests after each changehere, and in 59 of 521, across 56 files
- write tests before refactoringin 27 of 521, across 24 files
- preserve external behaviorin 26 of 521, across 22 files
- remove dead codein 25 of 521, across 24 files
- make small incremental changesin 20 of 521, across 17 files
- break the implementation into tiny commitsin 18 of 521, across 5 files
- ask the user about alternative optionsin 17 of 521, across 4 files
- create a GitHub issue with the planin 17 of 521, across 4 files
- explore the repository to verify assertionsin 17 of 521, across 4 files
- interview the user about the refactorin 16 of 521, across 3 files
- check the codebase for test coveragein 16 of 521, across 3 files
- refactor one thing at a timein 16 of 521, across 12 files
Said here and by no other author read
- preserve fragile functions and constants intact
- add logging to exception handlers before moving code
- pin function behavior with characterization tests before extraction
- move code verbatim with zero logic edits
- group extracted pure helpers by domain
- group extracted services by domain
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once.