Skip to content

refactor: split backend domains into app/features - #84

Merged
dimslaev merged 8 commits into
refactor/backend-core-dbfrom
refactor/backend-features
Oct 19, 2026
Merged

dimslaev merged 8 commits into
refactor/backend-core-dbfrom
refactor/backend-features

Conversation

@dimslaev

Copy link
Copy Markdown
Owner

Step 3 of the backend architecture plan. Stacked on #83.

What moved

  • app/features/ holds seven feature folders: catalog, stories, curation, collect, newsletter, audience, analytics. Each one has model.py (shapes and errors) and service.py (the verbs the outside calls).
  • catalog, audience, newsletter and analytics came from app/<name>. stories/ was split out of catalog/stories.py, and its shapes out of catalog/models.py.
  • curation came from pipeline/curation.py, steps/persist.py and steps/enrich.py (now publish.py), tags.py, and fetching/page_facts.py (now links.py). The feed and ledger row loading behind similar and stories is in coverage.py. Story (coverage) is now Coverage.
  • collect came from the rest of pipeline/: run.py became service.py (run, run_nightly, send_report), steps/* became fetch.py, filter.py and queue.py, sources/substack.py became feeds.py, and sources/email.py became newsletters.py. RunStats and SourceStats moved to model.py.
  • pipeline/run.py and pipeline/report.py are now two thin modules that call run_nightly and send_report. The systemd units are unchanged and need no reinstall.
  • The email templates now sit beside the feature that sends them: audience/sign_in.html, audience/welcome.html and audience/email.py (the sign-in and welcome builders), and newsletter/weekly_issue.html. core/email.py keeps rendering and sending.
  • subscribe moved into newsletter/service.py. DEDUP_DIST_THRESHOLD and RELATED_DIST moved to core/embedding.py, because curation and the newsletter both read them.
  • The routes moved to app/entry/api/ now (login, users, likes, articles, stories, analytics, deps), not in step 4. The login and likes routes compose audience with newsletter and catalog, so leaving them in a feature folder would have meant a feature importing another feature. Features now import only core.
  • The analytics ingest route body moved into analytics.service.record_event.
  • The tests for core modules moved from tests/pipeline/ to tests/core/, together with the pure embedding maths tests (tests/core/test_embedding.py). The best_per_url tests moved to curation/tests/test_publish.py.
  • Config: the T201 per-file ignore now covers app/features/collect/**. pipeline is gone from testpaths and the coverage source. The end-of-file-fixer exclude now points at the moved templates.

Behaviour changes

  • None in the API or MCP. The OpenAPI document is byte-identical, scripts/generate-client.sh leaves frontend/src/client unchanged, and a dump of the MCP tool list is identical before and after.
  • In the newsletter source, a Jev answer that is missing or not a number now counts as 0, so the link is dropped as off-topic. Before, it raised TypeError outside the per-link try, and that ended the source.

Bug fixes

  • A link on a shared platform's subdomain (old.reddit.com, m.youtube.com, mobile.twitter.com, gist.github.com) or on youtu.be used to get a new publisher named after that host. It now stays with the aggregator, like links on the bare host. Subdomains of substack.com and medium.com are still one writer's site.
  • In the newsletter source, a SelectProductLink index past the list of search results raised IndexError and ended the whole Newsletter source for the night. That product is now dropped.

Tests added

  • approve and reject against the real schema: one article per approval with sanitized title, kind, tags, found_via and publisher naming; the pending row deleted; approving twice refused; a refused approval writes nothing; a failed embedding still publishes; a rejected candidate cannot then be approved; the same URL twice in one reject_many.
  • Publisher resolution: shared-host subdomains, quarantining an unknown source once per run, slug collisions for a new author, and credit by URL over the feed that carried it.
  • The collect run: a failed source rolls its session back, the errors a fetch reports reach the bookkeeping, one source failing leaves the next one running, run_nightly exit codes (crash, still recorded, recording failure), and send_report failing.
  • The newsletter product pick, including indexes past the list.

Types

cd backend && uv run mypy app/features app/core (strict) reports 0 errors, tests included, with no per-module ignores. Root mypy and ty pass too.

How I verified

  • ruff check and format, mypy (root and strict on app/features plus app/core), ty, typos, the banned-names hook, and all of prek over this branch's range.
  • Backend tests on a fresh Postgres 16 + pgvector db: 638 passed, 1 failed. The one failure is /search, which needs the Hugging Face model download that this sandbox blocks. It passes in CI. Coverage is 87%.
  • python -m pipeline.run and python -m pipeline.report against the local db: both exit 0 and record or report as before.
  • OpenAPI diff and MCP tools/list diff are both empty.

Follow-ups (not in this PR)

  • GET /articles?min_score= is capped at 10 (le=10), but scores run 1-100. Fixing it changes the generated client.
  • article.url and publisher.slug have no unique index. Approve and _free_slug guard against duplicates in code only.
  • docs/backend.md, the glossary and the deploy/README.md env table still name the old paths. That is step 8.

Generated by Claude Code

* refactor: expose what entry needs through each feature's service

* refactor: split the mcp tools into app/entry/mcp and fold the router into main

* refactor: move scripts and the timer entrypoints into app/entry/cli

* test: cover the mcp token gate end to end over http

* test: cover the cli entrypoints' targets and exit codes

* fix: fail the backup when pg_dump writes nothing

* refactor: make app/entry pass strict mypy

* fix: keep sentry off in development

* chore: name the seed module in restore-db's hint without backticks

* docs: say what tests-start.sh does with its argument

* test: return the tool result's error flag and text from the gate tests' helper

* fix: stop importing the email module from configuring the root logger

* fix: require the alembic that reads its config from pyproject

* test: check the collect run leaves the root logger alone

* docs: say where command-line entrypoints live

* refactor: name the newsletter service alike in every mcp tool

* test: colocate backend tests beside their modules (#86)

* test: colocate backend tests beside their modules

* test: cover the curation tools, sign-in refusals, publisher lookups and the known-URL filter

* fix: fail the daily report when its mail does not send

* test: pin that the report reaches the send, and clean up the filter's publisher

* ci: enforce backend import layers (#87)

* fix: run mypy and ty in pre-commit with the backend config

* ci: enforce backend import layers

* fix: name the exact claude-code-action tag in its pin comments

* ci: deploy only what the box runs (#88)

* docs: point the deploy README's env table at app/features

* ci: deploy only what the box runs

* test: check the deploy filter ships every path the units and the job use

* docs: record the layered backend (#89)

* docs: record the layered backend

* fix: address the backend migration review (#91)

* fix: configure logging for the api process

* fix: let a publisher's link claim a platform subdomain

* fix: fail a link loudly on a non-numeric jev answer

* docs: point stale docstrings at where the code moved

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
@dimslaev
dimslaev merged commit 2ff5845 into refactor/backend-core-db Oct 19, 2026
6 checks passed
@dimslaev
dimslaev deleted the refactor/backend-features branch October 19, 2026 22:42
dimslaev added a commit that referenced this pull request Oct 19, 2026
* fix: percent-encode database credentials in the dsn

* fix: declare the indexes and column types the migrations built

* refactor: move tables into app/core/db

* test: cover the schema against migrations, the engine and ledger writes

* refactor: split backend domains into app/features (#84)

* refactor: split backend domains into app/features

* refactor: type app/features for mypy strict

* fix: keep links on a shared platform's subdomains with the aggregator

* fix: drop a newsletter product whose picked link is past the search results

* test: cover approve and reject against the schema, and publisher resolution edges

* chore: point the end-of-file exclude at the moved email templates

* refactor: tidy comments and test helpers from the features split

* refactor: move mcp tools and scripts into app/entry (#85)

* refactor: expose what entry needs through each feature's service

* refactor: split the mcp tools into app/entry/mcp and fold the router into main

* refactor: move scripts and the timer entrypoints into app/entry/cli

* test: cover the mcp token gate end to end over http

* test: cover the cli entrypoints' targets and exit codes

* fix: fail the backup when pg_dump writes nothing

* refactor: make app/entry pass strict mypy

* fix: keep sentry off in development

* chore: name the seed module in restore-db's hint without backticks

* docs: say what tests-start.sh does with its argument

* test: return the tool result's error flag and text from the gate tests' helper

* fix: stop importing the email module from configuring the root logger

* fix: require the alembic that reads its config from pyproject

* test: check the collect run leaves the root logger alone

* docs: say where command-line entrypoints live

* refactor: name the newsletter service alike in every mcp tool

* test: colocate backend tests beside their modules (#86)

* test: colocate backend tests beside their modules

* test: cover the curation tools, sign-in refusals, publisher lookups and the known-URL filter

* fix: fail the daily report when its mail does not send

* test: pin that the report reaches the send, and clean up the filter's publisher

* ci: enforce backend import layers (#87)

* fix: run mypy and ty in pre-commit with the backend config

* ci: enforce backend import layers

* fix: name the exact claude-code-action tag in its pin comments

* ci: deploy only what the box runs (#88)

* docs: point the deploy README's env table at app/features

* ci: deploy only what the box runs

* test: check the deploy filter ships every path the units and the job use

* docs: record the layered backend (#89)

* docs: record the layered backend

* fix: address the backend migration review (#91)

* fix: configure logging for the api process

* fix: let a publisher's link claim a platform subdomain

* fix: fail a link loudly on a non-numeric jev answer

* docs: point stale docstrings at where the code moved

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
dimslaev added a commit that referenced this pull request Oct 19, 2026
* refactor: move shared backend mechanism into app/core

* refactor: reword the check_link module docstring

* fix: read a feed date with no timezone as utc

* test: cover token rejection, the daily mail send and github lookups

* test: spell unparsable the way typos expects

* refactor: move tables into app/core/db (#83)

* fix: percent-encode database credentials in the dsn

* fix: declare the indexes and column types the migrations built

* refactor: move tables into app/core/db

* test: cover the schema against migrations, the engine and ledger writes

* refactor: split backend domains into app/features (#84)

* refactor: split backend domains into app/features

* refactor: type app/features for mypy strict

* fix: keep links on a shared platform's subdomains with the aggregator

* fix: drop a newsletter product whose picked link is past the search results

* test: cover approve and reject against the schema, and publisher resolution edges

* chore: point the end-of-file exclude at the moved email templates

* refactor: tidy comments and test helpers from the features split

* refactor: move mcp tools and scripts into app/entry (#85)

* refactor: expose what entry needs through each feature's service

* refactor: split the mcp tools into app/entry/mcp and fold the router into main

* refactor: move scripts and the timer entrypoints into app/entry/cli

* test: cover the mcp token gate end to end over http

* test: cover the cli entrypoints' targets and exit codes

* fix: fail the backup when pg_dump writes nothing

* refactor: make app/entry pass strict mypy

* fix: keep sentry off in development

* chore: name the seed module in restore-db's hint without backticks

* docs: say what tests-start.sh does with its argument

* test: return the tool result's error flag and text from the gate tests' helper

* fix: stop importing the email module from configuring the root logger

* fix: require the alembic that reads its config from pyproject

* test: check the collect run leaves the root logger alone

* docs: say where command-line entrypoints live

* refactor: name the newsletter service alike in every mcp tool

* test: colocate backend tests beside their modules (#86)

* test: colocate backend tests beside their modules

* test: cover the curation tools, sign-in refusals, publisher lookups and the known-URL filter

* fix: fail the daily report when its mail does not send

* test: pin that the report reaches the send, and clean up the filter's publisher

* ci: enforce backend import layers (#87)

* fix: run mypy and ty in pre-commit with the backend config

* ci: enforce backend import layers

* fix: name the exact claude-code-action tag in its pin comments

* ci: deploy only what the box runs (#88)

* docs: point the deploy README's env table at app/features

* ci: deploy only what the box runs

* test: check the deploy filter ships every path the units and the job use

* docs: record the layered backend (#89)

* docs: record the layered backend

* fix: address the backend migration review (#91)

* fix: configure logging for the api process

* fix: let a publisher's link claim a platform subdomain

* fix: fail a link loudly on a non-numeric jev answer

* docs: point stale docstrings at where the code moved

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
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