Debtmap, scanned by Debtmap
This is the report Debtmap produced for its own repository. Two findings had their evidence overwritten by a bug in our duplication check (since fixed); they are shown here with the evidence restored from the code.
Executive summary
Overall risk is Medium. The codebase is small and well tested for its age, and the change history shows it was built quickly by a single author. The debt is concentrated in a few places: `back/services/github.py` (34 KB, with a TODO admitting it needs splitting), `back/routers/app.py` (scan orchestration and an in-process job runner mixed with HTTP endpoints), `back/main.py` (the most-changed file), and a backend-to-frontend analysis schema that is kept in sync by hand. The main business risks are slower feature work in the scan pipeline, reliability limits from running scans inside the web process, and 100% single-author ownership of every directory in the history. History is thin (27 non-merge commits over roughly three months), so all confidence is capped at medium.
Quantified impact
Roughly 10-20% slower delivery on scan-pipeline and results-UI features, mainly from cross-stack coupling and a large mixed-responsibility `github.py`. Other areas such as the tracking integrations are small and well isolated.
How we got here: The core scan-pipeline change loop touches many files together. `back/services/analysis.py` and `back/services/github.py` share 6 commits (coupling 0.86). `back/schemas/scan.py`, `front/types/scan.ts` and `front/components/ScanResults.tsx` also share 6 to 7 commits. A typical change to the analysis output therefore means editing roughly 4 to 5 files across two languages. Fix density is low (0 to 14% on the top files), so this is friction rather than instability. The range is kept modest because the repo is young, has one author, and the largest file is 34 KB, not a multi-thousand-line monolith.
About 1-3 extra weeks before a new engineer can safely change the scan pipeline. Stale top-level docs and the unsplit `github.py` are the main contributors.
How we got here: A newcomer must understand how the pipeline moves: `routers/app.py` (scan lifecycle, cache, timeouts), then `services/github.py` (snapshot building, sampling, import resolution, GitHub source), then `git_metrics.py`, `analysis.py` and `verify.py`, plus the frontend polling logic in `front/app/scans/page.tsx`. The root README still lists Next.js 15, shadcn/ui, React Query and Zustand, none of which appear in `front/package.json`. Mitigations: tests are extensive (about 20 test files), and docstrings explain intent.
AI-generated debt
There are clear signs of an AI-assisted workflow (`CLAUDE_PROJECT_INSTRUCTIONS.md`, `front/CLAUDE.md` and `front/AGENTS.md` are in the tree). Code quality is above typical 'vibe code': comments explain why, and tests are meaningful. The AI-flavored debt is localized: several small helpers are re-implemented per module (webhook signing, test fakes), and the README has drifted from the actual stack.
Near-identical webhook signature verification implemented three times
`tracking/jira.py::webhook_signature_ok` and `github_app.py::verify_webhook_signature` have the same HMAC-SHA256 body under different names; `tracking/linear.py::webhook_signature_ok` repeats it with a bare-hex header format. `sign_oauth_state` and `oauth_state_is_valid` are also copied between jira.py and linear.py.
back/services/tracking/jira.pyback/services/github_app.pyback/services/tracking/linear.pyCopy-pasted in-memory fake Supabase tables across test files
✓ verified`FakeTable` in `back/tests/test_upload_endpoint.py`, `_Table` in `back/tests/test_linear_export.py` and `_Table` in `back/tests/test_jira_export.py` each implement the same chainable select/insert/update/eq/limit/execute fake. The linear and jira `_Table` and `_DB` bodies are nearly identical. `_Chain` in `back/tests/test_readiness.py` is a fourth, simpler variant. (verified: definitions found in both files)
back/tests/test_upload_endpoint.pyback/tests/test_linear_export.pyback/tests/test_jira_export.pyback/tests/test_readiness.pyDocumentation drift: README describes a stack that does not match the code
Root `README.md` lists 'Next.js 15', 'Tailwind CSS + shadcn/ui' and 'React Query + Zustand', while its own project-structure section says Next.js 16. `front/package.json` shows `next` 16.2.10 and only `next`, `react`, `react-dom` and `server-only` as runtime dependencies. The README also says `git clone .../yourusername/debtwise.git`.
README.mdfront/package.jsonInconsistent dependency declarations
`back/services/github_app.py` imports `jwt` (PyJWT) lazily, but neither `back/pyproject.toml` nor `back/requirements.txt` lists it. The two manifests are also unpinned and differ: `pytest` is in `requirements.txt` but only an optional `dev` extra in `pyproject.toml`.
back/services/github_app.pyback/pyproject.tomlback/requirements.txtTech stack
Debt hotspots
`github.py` is a god module: snapshot builder, GitHub client, sampler and import resolver in one file
higharchitecture
medium confidence✓ verifiedback/services/github.pyback/services/git_metrics.py10 commits · 1 authors · 10% fixes — last 12 mo
34,317 bytes, the largest source file. The module docstring describes a snapshot builder, but the file also defines `GitHubSource`, `resolve_repo`, token-override contextvars, tiered sampling (`_plan_source_sample`, `_select_diversity`, `_rank_candidates`), blob fetching with a thread pool, and import-resolution helpers. A TODO in the header says: 'split it into snapshot.py + github_source.py'. `back/services/git_metrics.py` imports the private `_is_source_file` from it, and `github.py` uses a `TYPE_CHECKING` guard to avoid a circular import. The same helper, `_dirname`, is defined in both files with identical bodies. (verified: definitions found in both files)
Impact: This is the center of the product's core value: snapshot quality drives report quality. Every change to sampling or evidence selection lands in a file mixing four concerns, and the circular import with `git_metrics` is a sign of misplaced boundaries. The cost is slower, riskier changes to the feature customers pay for.
Scan orchestration, caching and HTTP endpoints share one router; scan jobs run inside the web process
higharchitecture
medium confidenceback/routers/app.py8 commits · 1 authors · 12% fixes — last 12 mo
17,485 bytes. Besides endpoints, it holds `_analyze`, `_url_job`, `_zip_job`, `_execute_scan` (semaphore, a per-scan `ThreadPoolExecutor` for the deadline, DB status updates), `_mark_failed`, cache lookup and insert helpers, rate-limit charge/rollback, and temp-directory ownership handling in `create_scan_upload`. Comments call the in-process timeout a 'stopgap' pending a Redis/arq queue. On timeout the worker thread is abandoned rather than cancelled.
Impact: Scans are the product. A restart or crash abandons running work (hence the stale-scan sweep in `main.py`), and capacity is bounded by `MAX_CONCURRENT_ANALYSES = 2` in one process. Lifecycle logic is hard to test or evolve while it lives beside route handlers.
`main.py` is the highest-churn file and mixes app wiring, boot diagnostics and health endpoints
mediumarchitecture
low confidenceback/main.pyback/core/config.py12 commits · 1 authors · 0% fixes — last 12 mo
Boot validation (`_validate_supabase_url`) and the `/ready` probe each run the same scheme/hostname and DNS checks on `SUPABASE_URL`, written out twice. `main.py` also holds the stale-scan sweep, the git-binary check, the upload-size middleware and router wiring.
Impact: Every new router or config option touches this file, so it attracts merge conflicts once a second engineer joins. The two copies of the URL check can drift, producing a boot log that disagrees with the readiness probe.
Backend-to-frontend analysis contract is maintained by hand across two languages
mediumarchitecture
medium confidenceback/schemas/scan.pyfront/types/scan.tsfront/components/ScanResults.tsxback/services/analysis.py7 commits · 1 authors · 14% fixes — last 12 mo
`front/components/ScanResults.tsx` and `front/types/scan.ts` share 7 commits (coupling 1.00). `back/schemas/scan.py` shares 6 commits with `analysis.py`, `ScanResults.tsx` and `scan.ts`. `ScanResults.tsx` contains compatibility code for old rows (`MaybeRange`, `rangeText`). `ScanForm.tsx` hard-codes `MAX_UPLOAD_BYTES` with a comment that it mirrors `MAX_ARCHIVE_BYTES` in `back/services/archive.py`.
Impact: Any change to report fields requires coordinated edits in 4 to 5 files with no compile-time guarantee they match. Silent drift becomes a runtime UI bug.
`scans/page.tsx` combines polling state machine, two submit paths, PDF export and layout in one client component
mediumcomplexity
medium confidencefront/app/scans/page.tsxfront/components/ScanForm.tsx9 commits · 1 authors · 0% fixes — last 12 mo
11,066 bytes. One component holds `useActionState` for URL scans, separate `useState` for uploads, three more states for polled scan, poll error and poll expiry, a hand-rolled `setTimeout` polling loop with backoff, visibility pause and error tolerance, and the `downloadPdf`/`scanSlug` helpers. The logic has no test file in the tree.
Impact: This is the only user-facing flow, and there are no frontend tests. The polling and dual-source state logic is the kind that breaks subtly when a third source or a new status is added.
Prioritized backlog
Split `services/github.py` along the seams its own TODO describes
highCreate `services/snapshot.py`, `services/github_source.py` and `services/import_resolution.py`. Move `_is_source_file` and `_dirname` to `services/repo_source.py` so `git_metrics.py` imports them from there, removing the private-name import and the circular-import guard. Keep re-exports temporarily so `routers/app.py` and the tests need no changes in the first PR.
back/services/github.pyback/services/git_metrics.pyback/services/repo_source.pyThe core snapshot logic becomes changeable in isolation, so a new source type or a sampling tweak no longer lands in a 34 KB mixed file.
ROI: 10 commits touched this file in about 3 months. If even half of future snapshot changes become simpler, 3 to 5 days of mechanical moves pays back in one to three months.
Extract scan lifecycle from the router into a runner service, as the step before a real job queue
highMove `_analyze`, `_url_job`, `_zip_job`, `_execute_scan`, `_mark_failed` and the cache helpers into `services/scan_runner.py` behind one `ScanRunner` interface with the current thread-based implementation.
back/routers/app.pyback/main.pyback/services/analysis.pyScans survive restarts once a queue is plugged in, and the GitHub App and Action get one execution path instead of a third.
ROI: The queue migration is already acknowledged in 3 places. Doing the extraction first reduces that migration's blast radius.
Generate frontend types from the backend schema and add a contract test
mediumExport the OpenAPI schema and generate `front/types/scan.ts` from it, with a CI step that fails on drift. Normalize legacy bare-number estimates once at the API boundary.
back/schemas/scan.pyfront/types/scan.tsfront/components/ScanResults.tsxReport-format changes become a one-place edit with compile-time checking on the frontend.
ROI: `scan.py` and `scan.ts` have shared 6 commits in about 3 months; each future change saves manual syncing plus avoided drift bugs.
Slim down `main.py`: move boot checks and health endpoints out, dedupe URL validation
mediumCreate a health router holding `/health`, `/ready` and the boot checks, with one shared storage-config check. Move the upload-size middleware to `core/middleware.py`.
back/main.pyback/core/config.pyback/tests/test_readiness.pyThe highest-churn file stops being the default place for every change, and boot logs can no longer disagree with the readiness probe.
ROI: Roughly 1 to 2 days of work, already protected by `test_readiness.py` and `test_supabase_dns.py`.
Consolidate webhook signature and OAuth state helpers into one signing module
mediumCreate one signing module used by `jira.py`, `linear.py` and `github_app.py`, keeping Linear's bare-hex header format. Add one parametrized test covering all three call sites.
back/services/tracking/jira.pyback/services/tracking/linear.pyback/services/github_app.pyA security fix is made once, and a fourth integration does not mean a fourth copy of auth code.
ROI: Under a day of mostly mechanical work; payback is risk reduction on webhook endpoints.
Extract a shared fake Supabase fixture and a polling hook
lowMove one configurable fake table into the test fixtures and replace the four copies. On the frontend, extract the polling effect into a `useScanPolling(activeId)` hook so it can be tested alone.
back/tests/conftest.pyback/tests/test_linear_export.pyback/tests/test_jira_export.pyfront/app/scans/page.tsxNew route and integration tests become cheaper to write, and the only user-facing flow becomes testable.
ROI: A couple of days of work; each new endpoint test otherwise re-copies a ~40-line fake.
Fix documentation and dependency hygiene
mediumUpdate the root README stack section and clone URL. Add PyJWT to the backend dependencies, pick one manifest as the source of truth, and pin versions.
README.mdback/pyproject.tomlback/requirements.txtNew engineers get a correct first impression, and a fresh deploy of the GitHub App path will not fail on a missing library.
ROI: Hours of work that prevent an onboarding stumble and one class of deploy failure.
Quick wins
- Add PyJWT to `back/pyproject.toml` and `back/requirements.txt`. `services/github_app.py` imports `jwt` lazily, so the GitHub App path fails on a clean install.
- Correct the root `README.md`: it lists Next.js 15, shadcn/ui, React Query and Zustand, but `front/package.json` uses Next 16.2.10 and none of the last three. Replace the placeholder clone URL too.
- Move one fake Supabase table into the shared test fixtures and replace the duplicates in `test_linear_export.py` and `test_jira_export.py` first; those two are nearly identical.